Add implicit authentication flow #255
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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<Arc<ClientManager>>, | ||||
|     sessions: web::Data<Addr<OpenIDSessionsActor>>, | ||||
|     logger: ActionLogger, | ||||
| ) -> impl Responder { | ||||
|     jwt_signer: web::Data<JWTSigner>, | ||||
| ) -> actix_web::Result<HttpResponse> { | ||||
|     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!", | ||||
|             ) | ||||
|             )) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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:?}"), | ||||
|  | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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")] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user