Can finalize Matrix authentication

This commit is contained in:
2025-11-05 19:32:11 +01:00
parent 37fad9ff55
commit 1eaec9d319
13 changed files with 180 additions and 18 deletions

View File

@@ -1,8 +1,9 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::controllers::{HttpFailure, HttpResult}; use crate::controllers::{HttpFailure, HttpResult};
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
use crate::extractors::session_extractor::MatrixGWSession; use crate::extractors::session_extractor::MatrixGWSession;
use crate::users::{ExtendedUserInfo, User, UserEmail}; use crate::users::{User, UserEmail};
use actix_remote_ip::RemoteIP; use actix_remote_ip::RemoteIP;
use actix_web::{HttpResponse, web}; use actix_web::{HttpResponse, web};
use light_openid::primitives::OpenIDConfig; use light_openid::primitives::OpenIDConfig;
@@ -107,8 +108,8 @@ pub async fn finish_oidc(
} }
/// Get current user information /// Get current user information
pub async fn auth_info(auth: AuthExtractor) -> HttpResult { pub async fn auth_info(client: MatrixClientExtractor) -> HttpResult {
Ok(HttpResponse::Ok().json(ExtendedUserInfo::from_user(auth.user).await?)) Ok(HttpResponse::Ok().json(client.to_extended_user_info().await?))
} }
/// Sign out user /// Sign out user

View File

@@ -1,6 +1,8 @@
use crate::controllers::HttpResult; use crate::controllers::HttpResult;
use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
use crate::matrix_connection::matrix_client::FinishMatrixAuth;
use actix_web::HttpResponse; use actix_web::HttpResponse;
use anyhow::Context;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct StartAuthResponse { struct StartAuthResponse {
@@ -12,3 +14,13 @@ pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult {
let url = client.client.initiate_login().await?.to_string(); let url = client.client.initiate_login().await?.to_string();
Ok(HttpResponse::Ok().json(StartAuthResponse { url })) Ok(HttpResponse::Ok().json(StartAuthResponse { url }))
} }
/// Finish user authentication on Matrix server
pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult {
client
.client
.finish_login(client.auth.decode_json_body::<FinishMatrixAuth>()?)
.await
.context("Failed to finalize Matrix authentication!")?;
Ok(HttpResponse::Accepted().finish())
}

View File

@@ -28,7 +28,9 @@ impl ResponseError for HttpFailure {
} }
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).body(self.to_string()) HttpResponse::build(self.status_code())
.content_type("text/plain")
.body(self.to_string())
} }
} }

View File

@@ -11,6 +11,8 @@ use anyhow::Context;
use bytes::Bytes; use bytes::Bytes;
use jwt_simple::common::VerificationOptions; use jwt_simple::common::VerificationOptions;
use jwt_simple::prelude::{Duration, HS256Key, MACLike}; use jwt_simple::prelude::{Duration, HS256Key, MACLike};
use jwt_simple::reexports::serde_json;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fmt::Display; use std::fmt::Display;
use std::net::IpAddr; use std::net::IpAddr;
@@ -32,6 +34,16 @@ pub struct AuthExtractor {
pub payload: Option<Vec<u8>>, pub payload: Option<Vec<u8>>,
} }
impl AuthExtractor {
pub fn decode_json_body<E: DeserializeOwned + Send>(&self) -> anyhow::Result<E> {
let payload = self
.payload
.as_ref()
.context("Failed to decode request as json: missing payload!")?;
serde_json::from_slice(payload).context("Failed to decode request json!")
}
}
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct MatrixJWTKID { pub struct MatrixJWTKID {
pub user_email: UserEmail, pub user_email: UserEmail,

View File

@@ -1,6 +1,7 @@
use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::auth_extractor::AuthExtractor;
use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_client::MatrixClient;
use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
use crate::users::ExtendedUserInfo;
use actix_web::dev::Payload; use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest, web}; use actix_web::{FromRequest, HttpRequest, web};
use ractor::ActorRef; use ractor::ActorRef;
@@ -10,6 +11,16 @@ pub struct MatrixClientExtractor {
pub client: MatrixClient, pub client: MatrixClient,
} }
impl MatrixClientExtractor {
pub async fn to_extended_user_info(&self) -> anyhow::Result<ExtendedUserInfo> {
Ok(ExtendedUserInfo {
user: self.auth.user.clone(),
matrix_user_id: self.client.client.user_id().map(|id| id.to_string()),
matrix_device_id: self.client.client.device_id().map(|id| id.to_string()),
})
}
}
impl FromRequest for MatrixClientExtractor { impl FromRequest for MatrixClientExtractor {
type Error = actix_web::Error; type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>; type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;

View File

@@ -55,7 +55,7 @@ async fn main() -> std::io::Result<()> {
let cors = Cors::default() let cors = Cors::default()
.allowed_origin(&AppConfig::get().website_origin) .allowed_origin(&AppConfig::get().website_origin)
.allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) .allowed_methods(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
.allowed_header(constants::API_AUTH_HEADER) .allowed_header(constants::API_AUTH_HEADER)
.allow_any_header() .allow_any_header()
.supports_credentials() .supports_credentials()
@@ -94,6 +94,10 @@ async fn main() -> std::io::Result<()> {
"/api/matrix_link/start_auth", "/api/matrix_link/start_auth",
web::post().to(matrix_link_controller::start_auth), web::post().to(matrix_link_controller::start_auth),
) )
.route(
"/api/matrix_link/finish_auth",
web::post().to(matrix_link_controller::finish_auth),
)
}) })
.workers(4) .workers(4)
.bind(&AppConfig::get().listen_address)? .bind(&AppConfig::get().listen_address)?

View File

@@ -1,8 +1,8 @@
use crate::app_config::AppConfig; use crate::app_config::AppConfig;
use crate::users::UserEmail; use crate::users::UserEmail;
use crate::utils::rand_utils::rand_string; use crate::utils::rand_utils::rand_string;
use matrix_sdk::authentication::oauth::OAuthError;
use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError;
use matrix_sdk::authentication::oauth::{OAuthError, UrlOrQuery};
use matrix_sdk::ruma::serde::Raw; use matrix_sdk::ruma::serde::Raw;
use matrix_sdk::{Client, ClientBuildError}; use matrix_sdk::{Client, ClientBuildError};
use url::Url; use url::Url;
@@ -28,6 +28,14 @@ enum MatrixClientError {
ParseAuthRedirectURL(url::ParseError), ParseAuthRedirectURL(url::ParseError),
#[error("Failed to build auth request! {0}")] #[error("Failed to build auth request! {0}")]
BuildAuthRequest(OAuthError), BuildAuthRequest(OAuthError),
#[error("Failed to finalize authentication! {0}")]
FinishLogin(matrix_sdk::Error),
}
#[derive(serde::Deserialize)]
pub struct FinishMatrixAuth {
code: String,
state: String,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -91,7 +99,7 @@ impl MatrixClient {
todo!() todo!()
} }
/// Initiate oauth authentication /// Initiate OAuth authentication
pub async fn initiate_login(&self) -> anyhow::Result<Url> { pub async fn initiate_login(&self) -> anyhow::Result<Url> {
let oauth = self.client.oauth(); let oauth = self.client.oauth();
@@ -112,4 +120,23 @@ impl MatrixClient {
Ok(auth.url) Ok(auth.url)
} }
/// Finish OAuth authentication
pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> {
let oauth = self.client.oauth();
oauth
.finish_login(UrlOrQuery::Query(format!(
"state={}&code={}",
info.state, info.code
)))
.await
.map_err(MatrixClientError::FinishLogin)?;
log::info!(
"User successfully authenticated as {}!",
self.client.user_id().unwrap()
);
Ok(())
}
} }

View File

@@ -172,13 +172,5 @@ pub struct ExtendedUserInfo {
#[serde(flatten)] #[serde(flatten)]
pub user: User, pub user: User,
pub matrix_user_id: Option<String>, pub matrix_user_id: Option<String>,
} pub matrix_device_id: Option<String>,
impl ExtendedUserInfo {
pub async fn from_user(user: User) -> anyhow::Result<Self> {
Ok(Self {
user,
matrix_user_id: None, // TODO
})
}
} }

View File

@@ -14,6 +14,7 @@ import { MatrixLinkRoute } from "./routes/MatrixLinkRoute";
import { NotFoundRoute } from "./routes/NotFoundRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute";
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage";
import { MatrixAuthCallback } from "./routes/MatrixAuthCallback";
interface AuthContext { interface AuthContext {
signedIn: boolean; signedIn: boolean;
@@ -39,6 +40,7 @@ export function App(): React.ReactElement {
<Route path="*" element={<BaseAuthenticatedPage />}> <Route path="*" element={<BaseAuthenticatedPage />}>
<Route path="" element={<HomeRoute />} /> <Route path="" element={<HomeRoute />} />
<Route path="matrix_link" element={<MatrixLinkRoute />} /> <Route path="matrix_link" element={<MatrixLinkRoute />} />
<Route path="matrix_auth_cb" element={<MatrixAuthCallback />} />
<Route path="*" element={<NotFoundRoute />} /> <Route path="*" element={<NotFoundRoute />} />
</Route> </Route>
) : ( ) : (

View File

@@ -7,6 +7,7 @@ export interface UserInfo {
name: string; name: string;
email: string; email: string;
matrix_user_id?: string; matrix_user_id?: string;
matrix_device_id?: string;
} }
const TokenStateKey = "auth-state"; const TokenStateKey = "auth-state";

View File

@@ -12,4 +12,15 @@ export class MatrixLinkApi {
}) })
).data; ).data;
} }
/**
* Finish Matrix Account login
*/
static async FinishAuth(code: string, state: string): Promise<void> {
await APIClient.exec({
uri: "/matrix_link/finish_auth",
method: "POST",
jsonData: { code, state },
});
}
} }

View File

@@ -0,0 +1,79 @@
import { Alert, Box, Button, CircularProgress } from "@mui/material";
import React from "react";
import { useNavigate, useSearchParams } from "react-router";
import { MatrixLinkApi } from "../api/MatrixLinkApi";
import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage";
import { RouterLink } from "../widgets/RouterLink";
import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider";
export function MatrixAuthCallback(): React.ReactElement {
const navigate = useNavigate();
const snackbar = useSnackbar();
const info = useUserInfo();
const [error, setError] = React.useState<null | string>(null);
const [searchParams] = useSearchParams();
const code = searchParams.get("code");
const state = searchParams.get("state");
const count = React.useRef("");
React.useEffect(() => {
const load = async () => {
try {
if (count.current === code) {
return;
}
count.current = code!;
await MatrixLinkApi.FinishAuth(code!, state!);
info.reloadUserInfo();
snackbar("Successfully linked to Matrix account!");
navigate("/matrix_link");
} catch (e) {
console.error(e);
setError(String(e));
}
};
load();
});
if (error)
return (
<Box
component="div"
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
flex: "1",
flexDirection: "column",
}}
>
<Alert
variant="outlined"
severity="error"
style={{ margin: "0px 15px 15px 15px" }}
>
Failed to finalize Matrix authentication!
<br />
<br />
{error}
</Alert>
<Button>
<RouterLink to="/matrix_link">Go back</RouterLink>
</Button>
</Box>
);
return (
<div style={{ textAlign: "center" }}>
<CircularProgress />
</div>
);
}

View File

@@ -79,9 +79,17 @@ function ConnectedCard(): React.ReactElement {
<Typography variant="body1" gutterBottom> <Typography variant="body1" gutterBottom>
<p> <p>
MatrixGW is currently connected to your account with ID{" "} MatrixGW is currently connected to your account with the following
<i>{user.info.matrix_user_id}</i>. information:
</p> </p>
<ul>
<li>
User id: <i>{user.info.matrix_user_id}</i>
</li>
<li>
Device id: <i>{user.info.matrix_device_id}</i>
</li>
</ul>
<p> <p>
If you encounter issues with your Matrix account you can try to If you encounter issues with your Matrix account you can try to
disconnect and connect back again. disconnect and connect back again.