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_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;

View File

@ -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!",
)
))
}
}
}

View File

@ -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:?}"),
}
}
}

View File

@ -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")]