diff --git a/src/constants.rs b/src/constants.rs index 0b1aec4..beb970d 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -69,6 +69,7 @@ pub const OPEN_ID_AUTHORIZATION_CODE_LEN: usize = 120; pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300; pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50; pub const OPEN_ID_ACCESS_TOKEN_TIMEOUT: u64 = 3600; +pub const OPEN_ID_ID_TOKEN_TIMEOUT: u64 = 3600; pub const OPEN_ID_REFRESH_TOKEN_LEN: usize = 120; pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000; diff --git a/src/controllers/openid_controller.rs b/src/controllers/openid_controller.rs index edaa063..3e22f00 100644 --- a/src/controllers/openid_controller.rs +++ b/src/controllers/openid_controller.rs @@ -131,6 +131,7 @@ fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> Htt .finish() } +#[allow(clippy::too_many_arguments)] pub async fn authorize( req: HttpRequest, user: CurrentUser, @@ -139,10 +140,13 @@ pub async fn authorize( clients: web::Data>, sessions: web::Data>, logger: ActionLogger, -) -> impl Responder { + jwt_signer: web::Data, +) -> actix_web::Result { let client = match clients.find_by_id(&query.client_id) { None => { - return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!")); + return Ok( + HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!")) + ); } Some(c) => c, }; @@ -150,31 +154,42 @@ pub async fn authorize( // Check if 2FA is required if client.enforce_2fa_auth && user.should_request_2fa_for_critical_functions() { let uri = get_2fa_url(&LoginRedirect::from_req(&req), true); - return redirect_user(&uri); + return Ok(redirect_user(&uri)); } + // Validate specified redirect URI let redirect_uri = query.redirect_uri.trim().to_string(); if !redirect_uri.starts_with(&client.redirect_uri) { - return HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!")); + return Ok( + HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!")) + ); } if !query.scope.split(' ').any(|x| x == "openid") { - return error_redirect(&query, "invalid_request", "openid scope missing!"); + return Ok(error_redirect( + &query, + "invalid_request", + "openid scope missing!", + )); } if query.state.as_ref().map(String::is_empty).unwrap_or(false) { - return error_redirect(&query, "invalid_request", "State is specified but empty!"); + return Ok(error_redirect( + &query, + "invalid_request", + "State is specified but empty!", + )); } let code_challenge = match query.0.code_challenge.clone() { Some(chal) => { let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain"); if !meth.eq("S256") && !meth.eq("plain") { - return error_redirect( + return Ok(error_redirect( &query, "invalid_request", "Only S256 and plain code challenge methods are supported!", - ); + )); } Some(CodeChallenge { code_challenge: chal, @@ -186,16 +201,20 @@ pub async fn authorize( // Check if user is authorized to access the application if !user.can_access_app(&client) { - return error_redirect( + return Ok(error_redirect( &query, "invalid_request", "User is not authorized to access this application!", - ); + )); } // Check that requested authorization flow is supported if query.response_type != "code" && query.response_type != "id_token" { - return error_redirect(&query, "invalid_request", "Unsupported authorization flow!"); + return Ok(error_redirect( + &query, + "invalid_request", + "Unsupported authorization flow!", + )); } match (client.auth_flow(), query.response_type.as_str()) { @@ -224,7 +243,7 @@ pub async fn authorize( log::trace!("New OpenID session: {:#?}", session); logger.log(Action::NewOpenIDSession { client: &client }); - HttpResponse::Found() + Ok(HttpResponse::Found() .append_header(( "Location", format!( @@ -238,10 +257,40 @@ pub async fn authorize( urlencoding::encode(&session.authorization_code) ), )) - .finish() + .finish()) + } + + (AuthenticationFlow::Implicit, "id_token") => { + let id_token = IdToken { + issuer: AppConfig::get().website_origin.to_string(), + subject_identifier: user.uid.0.clone(), + audience: client.id.0.to_string(), + expiration_time: time() + OPEN_ID_ID_TOKEN_TIMEOUT, + issued_at: time(), + auth_time: SessionIdentity(Some(&id)).auth_time(), + nonce: query.nonce.clone(), + email: user.email.clone(), + }; + + log::trace!("New OpenID id token: {:#?}", &id_token); + logger.log(Action::NewOpenIDSuccessfulImplicitAuth { client: &client }); + + Ok(HttpResponse::Found() + .append_header(( + "Location", + format!( + "{}?{}token_type=bearer&id_token={}&expires_in={OPEN_ID_ID_TOKEN_TIMEOUT}", + client.redirect_uri, + match &query.0.state { + Some(state) => format!("state={}&", urlencoding::encode(state)), + None => "".to_string(), + }, + jwt_signer.sign_token(id_token.to_jwt_claims())? + ), + )) + .finish()) } - //(AuthenticationFlow::Implicit, "id_token") => {} (flow, code) => { log::warn!( "For client {:?}, configured with flow {:?}, made request with code {}", @@ -249,11 +298,11 @@ pub async fn authorize( flow, code ); - error_redirect( + Ok(error_redirect( &query, "invalid_request", "Requested authentication flow is unsupported / not configured for this client!", - ) + )) } } } diff --git a/src/data/action_logger.rs b/src/data/action_logger.rs index 46e9278..a973185 100644 --- a/src/data/action_logger.rs +++ b/src/data/action_logger.rs @@ -90,6 +90,9 @@ pub enum Action<'a> { NewOpenIDSession { client: &'a Client, }, + NewOpenIDSuccessfulImplicitAuth { + client: &'a Client, + }, ChangedHisPassword, ClearedHisLoginHistory, AddNewFactor(&'a TwoFactor), @@ -199,6 +202,7 @@ impl<'a> Action<'a> { Action::NewOpenIDSession { client } => { format!("opened a new OpenID session with {:?}", client.id) } + Action::NewOpenIDSuccessfulImplicitAuth { client } => format!("finished an implicit flow connection for client {:?}", client.id), Action::ChangedHisPassword => "changed his password".to_string(), Action::ClearedHisLoginHistory => "cleared his login history".to_string(), Action::AddNewFactor(factor) => format!( @@ -206,7 +210,6 @@ impl<'a> Action<'a> { factor.quick_description(), ), Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"), - } } } diff --git a/src/data/id_token.rs b/src/data/id_token.rs index a0e51ea..dc9c197 100644 --- a/src/data/id_token.rs +++ b/src/data/id_token.rs @@ -1,7 +1,7 @@ use jwt_simple::claims::Audiences; use jwt_simple::prelude::{Duration, JWTClaims}; -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Debug)] pub struct IdToken { /// REQUIRED. Issuer Identifier for the Issuer of the response. The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components. #[serde(rename = "iss")]