From 6fdef1a099883cdf43a03de1f6e53b2063f40495 Mon Sep 17 00:00:00 2001 From: Luc Date: Wed, 4 Dec 2024 19:26:34 +0100 Subject: [PATCH] Update login & callback routes --- compose.yaml | 2 +- engine/.build/realm-export.json | 2 +- engine/.env.example | 2 +- engine/README.md | 2 +- engine/src/routes/mod.rs | 4 +- engine/src/routes/oauth/callback.rs | 127 +++++++++++++++------------- engine/src/routes/oauth/login.rs | 12 ++- web/src/components/Navbar.tsx | 2 +- web/src/routes/sessions.tsx | 2 +- 9 files changed, 86 insertions(+), 69 deletions(-) diff --git a/compose.yaml b/compose.yaml index 91d5e3e..cd6d6b2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: # Engine engine: - image: ghcr.io/v3xlabs/v3x-property-engine:master + image: ghcr.io/v3xlabs/v3x-property/engine:master ports: - "3000:3000" env_file: .env diff --git a/engine/.build/realm-export.json b/engine/.build/realm-export.json index 051e6bf..3343f79 100644 --- a/engine/.build/realm-export.json +++ b/engine/.build/realm-export.json @@ -35,7 +35,7 @@ "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "v3x-property-secret", - "redirectUris": ["http://localhost:3000/callback"], + "redirectUris": ["http://localhost:3000/api/callback"], "webOrigins": ["http://localhost:5173"], "notBefore": 0, "bearerOnly": false, diff --git a/engine/.env.example b/engine/.env.example index 3e44621..a19ffa6 100644 --- a/engine/.env.example +++ b/engine/.env.example @@ -1,7 +1,7 @@ # OpenID Connect OAuth 2.0 OPENID_CLIENT_ID=v3x-property OPENID_CLIENT_SECRET=v3x-property-secret -OPENID_REDIRECT=http://localhost:3000/callback +OPENID_REDIRECT=http://localhost:3000/api/callback OPENID_ISSUER=http://localhost:8080/realms/v3x-property # Postgres Database diff --git a/engine/README.md b/engine/README.md index eb2c790..a9da9a7 100644 --- a/engine/README.md +++ b/engine/README.md @@ -31,6 +31,6 @@ This can easily be done by heading to the `Clients` tab in the admin console. Then you can click on `Create client` and create a basic new OpenID Connect client. Choose a Client ID, and press `Next`. -Enable `Client Authentication` and specify the `Redirect URIs` to be `http://localhost:3000/callback`. +Enable `Client Authentication` and specify the `Redirect URIs` to be `http://localhost:3000/api/callback`. Once done you can head to the `Credentials` tab to see your Client Secret, insert this in your `.env` file. diff --git a/engine/src/routes/mod.rs b/engine/src/routes/mod.rs index 19e76af..a8fa970 100644 --- a/engine/src/routes/mod.rs +++ b/engine/src/routes/mod.rs @@ -4,7 +4,7 @@ use instance::InstanceApi; use items::ItemsApi; use me::MeApi; use media::MediaApi; -use oauth::login::LoginApi; +use oauth::{callback::CallbackApi, login::LoginApi}; use poem::{ get, handler, listener::TcpListener, middleware::Cors, web::Html, EndpointExt, Route, Server, }; @@ -52,6 +52,7 @@ fn get_api() -> impl OpenApi { SessionsApi, InstanceApi, LoginApi, + CallbackApi, ) } @@ -69,7 +70,6 @@ pub async fn serve(state: AppState) -> Result<(), poem::Error> { let state = Arc::new(state); let app = Route::new() - .at("/callback", get(oauth::callback::callback)) .nest("/api", api_service) .nest("/openapi.json", spec) .at("/docs", get(get_openapi_docs)) diff --git a/engine/src/routes/oauth/callback.rs b/engine/src/routes/oauth/callback.rs index 057e624..2a638f7 100644 --- a/engine/src/routes/oauth/callback.rs +++ b/engine/src/routes/oauth/callback.rs @@ -4,83 +4,96 @@ use openid::Token; use poem::{ handler, http::HeaderMap, - web::{Data, Query, RealIp, Redirect, WithHeader}, + web::{Data, RealIp, Redirect, WithHeader}, IntoResponse, Result, }; +use poem_openapi::{ + param::Query, + payload::{PlainText, Response}, + ApiResponse, Object, OpenApi, +}; +use serde::Deserialize; use tracing::info; use url::Url; use uuid::Uuid; +use super::super::ApiTags; use crate::{ auth::hash::hash_session, models::{sessions::Session, user::userentry::UserEntry}, state::AppState, }; -#[handler] -pub async fn callback( - state: Query>, - scope: Query>, - hd: Query>, - authuser: Query>, - code: Query, - prompt: Query>, - app_state: Data<&Arc>, - ip: RealIp, - headers: &HeaderMap, -) -> Result> { - let mut token = app_state.openid.request_token(&code).await.map_err(|_| { - poem::Error::from_response( - Redirect::temporary(app_state.openid.redirect_url()).into_response(), - ) - })?; +pub struct CallbackApi; - let mut token = Token::from(token); +#[OpenApi] +impl CallbackApi { + #[oai(path = "/callback", method = "get", tag = "ApiTags::Auth")] + pub async fn callback( + &self, + state: Query>, + scope: Query>, + hd: Query>, + authuser: Query>, + code: Query, + prompt: Query>, + app_state: Data<&Arc>, + ip: RealIp, + headers: &HeaderMap, + ) -> Result<()> { + let mut token = app_state.openid.request_token(&code).await.map_err(|_| { + poem::Error::from_response( + Redirect::temporary(app_state.openid.redirect_url()).into_response(), + ) + })?; - let mut id_token = token.id_token.take().unwrap(); + let mut token = Token::from(token); - app_state.openid.decode_token(&mut id_token).unwrap(); - app_state - .openid - .validate_token(&id_token, None, None) - .unwrap(); + let mut id_token = token.id_token.take().unwrap(); + + app_state.openid.decode_token(&mut id_token).unwrap(); + app_state + .openid + .validate_token(&id_token, None, None) + .unwrap(); + + let oauth_userinfo = app_state.openid.request_userinfo(&token).await.unwrap(); - let oauth_userinfo = app_state.openid.request_userinfo(&token).await.unwrap(); + // Now we must verify the user information, decide wether they deserve access, and if so return a token. + let user = UserEntry::upsert(&oauth_userinfo, None, &app_state.database) + .await + .unwrap(); - // Now we must verify the user information, decide wether they deserve access, and if so return a token. - let user = UserEntry::upsert(&oauth_userinfo, None, &app_state.database) + let user_agent = headers.get("user-agent").unwrap().to_str().unwrap(); + let user_ip = ip.0.unwrap(); + + let token = Uuid::new_v4().to_string(); + let hash = hash_session(&token).unwrap(); + + let _session = Session::new( + &app_state.database, + &hash, + user.user_id, + user_agent, + &user_ip.into(), + ) .await .unwrap(); - let user_agent = headers.get("user-agent").unwrap().to_str().unwrap(); - let user_ip = ip.0.unwrap(); - - let token = Uuid::new_v4().to_string(); - let hash = hash_session(&token).unwrap(); - - let _session = Session::new( - &app_state.database, - &hash, - user.user_id, - user_agent, - &user_ip.into(), - ) - .await - .unwrap(); - - info!("Issued session token for user {}", user.user_id); - - let mut redirect_url: Url = state - .0 - .clone() - .unwrap_or("http://localhost:3000/me".to_string()) - .parse() - .unwrap(); + info!("Issued session token for user {}", user.user_id); + + let mut redirect_url: Url = state + .0 + .clone() + .unwrap_or("http://localhost:3000/me".to_string()) + .parse() + .unwrap(); - redirect_url.set_query(Some(&format!("token={}", token))); + redirect_url.set_query(Some(&format!("token={}", token))); + redirect_url.set_query(Some(&format!("token={}", token))); - Ok(Redirect::temporary(redirect_url).with_header( - "Set-Cookie", - format!("property.v3x.token={}; Secure; HttpOnly", token), - )) + Err(poem::Error::from_response( + Redirect::temporary(redirect_url.to_string()).into_response(), + )) + } } diff --git a/engine/src/routes/oauth/login.rs b/engine/src/routes/oauth/login.rs index 71d2c52..4212dd8 100644 --- a/engine/src/routes/oauth/login.rs +++ b/engine/src/routes/oauth/login.rs @@ -1,11 +1,13 @@ use std::{collections::HashSet, sync::Arc}; use openid::{Options, Prompt}; -use poem::web::Data; +use poem::{web::{Data, Redirect}, IntoResponse, Result}; use poem_openapi::{param::Query, payload::PlainText, ApiResponse, OpenApi}; use crate::state::AppState; +use super::super::ApiTags; + pub struct LoginApi; #[derive(ApiResponse)] @@ -16,12 +18,12 @@ enum RedirectResponse { #[OpenApi] impl LoginApi { - #[oai(path = "/login", method = "get")] + #[oai(path = "/login", method = "get", tag = "ApiTags::Auth")] pub async fn login( &self, redirect: Query>, state: Data<&Arc>, - ) -> RedirectResponse { + ) -> Result<()> { // let discovery_url = "http://localhost:8080/realms/master/.well-known/openid-configuration"; // let http_client = reqwest::Client::new(); @@ -48,6 +50,8 @@ impl LoginApi { println!("OpenID Connect Authorization URL: {}", authorize_url); // redirect to the authorization URL - RedirectResponse::Redirect(PlainText(authorize_url.as_str().to_string())) + Err(poem::Error::from_response( + Redirect::temporary(authorize_url.as_str().to_string()).into_response(), + )) } } diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index 58cda23..14cb9b6 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -9,7 +9,7 @@ import * as DropdownMenu from '@/components/ui/Dropdown'; import { AvatarHolder, getInitials } from './UserProfile'; -const LOGIN_URL = 'http://localhost:3000/login'; +const LOGIN_URL = 'http://localhost:3000/api/login'; export const Navbar = () => { const { token, clearAuthToken } = useAuth(); diff --git a/web/src/routes/sessions.tsx b/web/src/routes/sessions.tsx index 835716a..21fc411 100644 --- a/web/src/routes/sessions.tsx +++ b/web/src/routes/sessions.tsx @@ -29,7 +29,7 @@ export const Route = createFileRoute('/sessions')({ // eslint-disable-next-line no-undef window.location.href = - 'http://localhost:3000/login?redirect=' + + 'http://localhost:3000/api/login?redirect=' + encodeURIComponent(location.href); } },