diff --git a/matrixgw_backend/src/controllers/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs index 6cbab4c..42d759f 100644 --- a/matrixgw_backend/src/controllers/auth_controller.rs +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -1,8 +1,9 @@ use crate::app_config::AppConfig; use crate::controllers::{HttpFailure, HttpResult}; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::extractors::session_extractor::MatrixGWSession; -use crate::users::{ExtendedUserInfo, User, UserEmail}; +use crate::users::{User, UserEmail}; use actix_remote_ip::RemoteIP; use actix_web::{HttpResponse, web}; use light_openid::primitives::OpenIDConfig; @@ -107,8 +108,8 @@ pub async fn finish_oidc( } /// Get current user information -pub async fn auth_info(auth: AuthExtractor) -> HttpResult { - Ok(HttpResponse::Ok().json(ExtendedUserInfo::from_user(auth.user).await?)) +pub async fn auth_info(client: MatrixClientExtractor) -> HttpResult { + Ok(HttpResponse::Ok().json(client.to_extended_user_info().await?)) } /// Sign out user diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs index 38fea49..084597a 100644 --- a/matrixgw_backend/src/controllers/matrix_link_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -1,6 +1,8 @@ use crate::controllers::HttpResult; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::matrix_connection::matrix_client::FinishMatrixAuth; use actix_web::HttpResponse; +use anyhow::Context; #[derive(serde::Serialize)] struct StartAuthResponse { @@ -12,3 +14,13 @@ pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult { let url = client.client.initiate_login().await?.to_string(); 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::()?) + .await + .context("Failed to finalize Matrix authentication!")?; + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 8a581e9..8ddf218 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -28,7 +28,9 @@ impl ResponseError for HttpFailure { } 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()) } } diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs index 8c75a29..36003e6 100644 --- a/matrixgw_backend/src/extractors/auth_extractor.rs +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -11,6 +11,8 @@ use anyhow::Context; use bytes::Bytes; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::{Duration, HS256Key, MACLike}; +use jwt_simple::reexports::serde_json; +use serde::de::DeserializeOwned; use sha2::{Digest, Sha256}; use std::fmt::Display; use std::net::IpAddr; @@ -32,6 +34,16 @@ pub struct AuthExtractor { pub payload: Option>, } +impl AuthExtractor { + pub fn decode_json_body(&self) -> anyhow::Result { + 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)] pub struct MatrixJWTKID { pub user_email: UserEmail, diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index fbe6d3c..5de4f7f 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -1,6 +1,7 @@ use crate::extractors::auth_extractor::AuthExtractor; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use crate::users::ExtendedUserInfo; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest, web}; use ractor::ActorRef; @@ -10,6 +11,16 @@ pub struct MatrixClientExtractor { pub client: MatrixClient, } +impl MatrixClientExtractor { + pub async fn to_extended_user_info(&self) -> anyhow::Result { + 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 { type Error = actix_web::Error; type Future = futures_util::future::LocalBoxFuture<'static, Result>; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 2571cdc..6c7e4fd 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -55,7 +55,7 @@ async fn main() -> std::io::Result<()> { let cors = Cors::default() .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) .allow_any_header() .supports_credentials() @@ -94,6 +94,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_link/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) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index f6dcc86..c6e88cb 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -1,8 +1,8 @@ use crate::app_config::AppConfig; use crate::users::UserEmail; 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::{OAuthError, UrlOrQuery}; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError}; use url::Url; @@ -28,6 +28,14 @@ enum MatrixClientError { ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] BuildAuthRequest(OAuthError), + #[error("Failed to finalize authentication! {0}")] + FinishLogin(matrix_sdk::Error), +} + +#[derive(serde::Deserialize)] +pub struct FinishMatrixAuth { + code: String, + state: String, } #[derive(Clone)] @@ -91,7 +99,7 @@ impl MatrixClient { todo!() } - /// Initiate oauth authentication + /// Initiate OAuth authentication pub async fn initiate_login(&self) -> anyhow::Result { let oauth = self.client.oauth(); @@ -112,4 +120,23 @@ impl MatrixClient { 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(()) + } } diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index aab18ed..7eca242 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -172,13 +172,5 @@ pub struct ExtendedUserInfo { #[serde(flatten)] pub user: User, pub matrix_user_id: Option, -} - -impl ExtendedUserInfo { - pub async fn from_user(user: User) -> anyhow::Result { - Ok(Self { - user, - matrix_user_id: None, // TODO - }) - } + pub matrix_device_id: Option, } diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 9988388..7cd3001 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -14,6 +14,7 @@ import { MatrixLinkRoute } from "./routes/MatrixLinkRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; +import { MatrixAuthCallback } from "./routes/MatrixAuthCallback"; interface AuthContext { signedIn: boolean; @@ -39,6 +40,7 @@ export function App(): React.ReactElement { }> } /> } /> + } /> } /> ) : ( diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index c0fc286..e2e10ac 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -7,6 +7,7 @@ export interface UserInfo { name: string; email: string; matrix_user_id?: string; + matrix_device_id?: string; } const TokenStateKey = "auth-state"; diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index 6a05a3c..3796189 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -12,4 +12,15 @@ export class MatrixLinkApi { }) ).data; } + + /** + * Finish Matrix Account login + */ + static async FinishAuth(code: string, state: string): Promise { + await APIClient.exec({ + uri: "/matrix_link/finish_auth", + method: "POST", + jsonData: { code, state }, + }); + } } diff --git a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx new file mode 100644 index 0000000..ae9906c --- /dev/null +++ b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx @@ -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); + + 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 ( + + + Failed to finalize Matrix authentication! +
+
+ {error} +
+ + +
+ ); + + return ( +
+ +
+ ); +} diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 7a33ed7..23a3b27 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -79,9 +79,17 @@ function ConnectedCard(): React.ReactElement {

- MatrixGW is currently connected to your account with ID{" "} - {user.info.matrix_user_id}. + MatrixGW is currently connected to your account with the following + information:

+
    +
  • + User id: {user.info.matrix_user_id} +
  • +
  • + Device id: {user.info.matrix_device_id} +
  • +

If you encounter issues with your Matrix account you can try to disconnect and connect back again.