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")]
|
||||
|
Loading…
Reference in New Issue
Block a user