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_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;
|
||||||
|
|
||||||
|
@ -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!",
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:?}"),
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")]
|
||||||
|
Loading…
Reference in New Issue
Block a user