Add implicit authentication flow #255

Merged
pierre merged 4 commits from implicit_flow into master 2024-03-28 21:13:27 +00:00
4 changed files with 71 additions and 18 deletions
Showing only changes of commit a600d4af71 - Show all commits

View File

@ -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_AUTHORIZATION_CODE_TIMEOUT: u64 = 300;
pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50; pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50;
pub const OPEN_ID_ACCESS_TOKEN_TIMEOUT: u64 = 3600; 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_LEN: usize = 120;
pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000; pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000;

View File

@ -131,6 +131,7 @@ fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> Htt
.finish() .finish()
} }
#[allow(clippy::too_many_arguments)]
pub async fn authorize( pub async fn authorize(
req: HttpRequest, req: HttpRequest,
user: CurrentUser, user: CurrentUser,
@ -139,10 +140,13 @@ pub async fn authorize(
clients: web::Data<Arc<ClientManager>>, clients: web::Data<Arc<ClientManager>>,
sessions: web::Data<Addr<OpenIDSessionsActor>>, sessions: web::Data<Addr<OpenIDSessionsActor>>,
logger: ActionLogger, logger: ActionLogger,
) -> impl Responder { jwt_signer: web::Data<JWTSigner>,
) -> actix_web::Result<HttpResponse> {
let client = match clients.find_by_id(&query.client_id) { let client = match clients.find_by_id(&query.client_id) {
None => { 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, Some(c) => c,
}; };
@ -150,31 +154,42 @@ pub async fn authorize(
// Check if 2FA is required // Check if 2FA is required
if client.enforce_2fa_auth && user.should_request_2fa_for_critical_functions() { if client.enforce_2fa_auth && user.should_request_2fa_for_critical_functions() {
let uri = get_2fa_url(&LoginRedirect::from_req(&req), true); 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(); let redirect_uri = query.redirect_uri.trim().to_string();
if !redirect_uri.starts_with(&client.redirect_uri) { 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") { 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) { 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() { let code_challenge = match query.0.code_challenge.clone() {
Some(chal) => { Some(chal) => {
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain"); let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
if !meth.eq("S256") && !meth.eq("plain") { if !meth.eq("S256") && !meth.eq("plain") {
return error_redirect( return Ok(error_redirect(
&query, &query,
"invalid_request", "invalid_request",
"Only S256 and plain code challenge methods are supported!", "Only S256 and plain code challenge methods are supported!",
); ));
} }
Some(CodeChallenge { Some(CodeChallenge {
code_challenge: chal, code_challenge: chal,
@ -186,16 +201,20 @@ pub async fn authorize(
// Check if user is authorized to access the application // Check if user is authorized to access the application
if !user.can_access_app(&client) { if !user.can_access_app(&client) {
return error_redirect( return Ok(error_redirect(
&query, &query,
"invalid_request", "invalid_request",
"User is not authorized to access this application!", "User is not authorized to access this application!",
); ));
} }
// Check that requested authorization flow is supported // Check that requested authorization flow is supported
if query.response_type != "code" && query.response_type != "id_token" { 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()) { match (client.auth_flow(), query.response_type.as_str()) {
@ -224,7 +243,7 @@ pub async fn authorize(
log::trace!("New OpenID session: {:#?}", session); log::trace!("New OpenID session: {:#?}", session);
logger.log(Action::NewOpenIDSession { client: &client }); logger.log(Action::NewOpenIDSession { client: &client });
HttpResponse::Found() Ok(HttpResponse::Found()
.append_header(( .append_header((
"Location", "Location",
format!( format!(
@ -238,10 +257,40 @@ pub async fn authorize(
urlencoding::encode(&session.authorization_code) 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) => { (flow, code) => {
log::warn!( log::warn!(
"For client {:?}, configured with flow {:?}, made request with code {}", "For client {:?}, configured with flow {:?}, made request with code {}",
@ -249,11 +298,11 @@ pub async fn authorize(
flow, flow,
code code
); );
error_redirect( Ok(error_redirect(
&query, &query,
"invalid_request", "invalid_request",
"Requested authentication flow is unsupported / not configured for this client!", "Requested authentication flow is unsupported / not configured for this client!",
) ))
} }
} }
} }

View File

@ -90,6 +90,9 @@ pub enum Action<'a> {
NewOpenIDSession { NewOpenIDSession {
client: &'a Client, client: &'a Client,
}, },
NewOpenIDSuccessfulImplicitAuth {
client: &'a Client,
},
ChangedHisPassword, ChangedHisPassword,
ClearedHisLoginHistory, ClearedHisLoginHistory,
AddNewFactor(&'a TwoFactor), AddNewFactor(&'a TwoFactor),
@ -199,6 +202,7 @@ impl<'a> Action<'a> {
Action::NewOpenIDSession { client } => { Action::NewOpenIDSession { client } => {
format!("opened a new OpenID session with {:?}", client.id) 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::ChangedHisPassword => "changed his password".to_string(),
Action::ClearedHisLoginHistory => "cleared his login history".to_string(), Action::ClearedHisLoginHistory => "cleared his login history".to_string(),
Action::AddNewFactor(factor) => format!( Action::AddNewFactor(factor) => format!(
@ -206,7 +210,6 @@ impl<'a> Action<'a> {
factor.quick_description(), factor.quick_description(),
), ),
Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"), Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"),
} }
} }
} }

View File

@ -1,7 +1,7 @@
use jwt_simple::claims::Audiences; use jwt_simple::claims::Audiences;
use jwt_simple::prelude::{Duration, JWTClaims}; use jwt_simple::prelude::{Duration, JWTClaims};
#[derive(serde::Serialize)] #[derive(serde::Serialize, Debug)]
pub struct IdToken { 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. /// 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")] #[serde(rename = "iss")]