Add implicit authentication flow (#255)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #255 Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org> Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
This commit is contained in:
@ -16,7 +16,7 @@ use crate::constants::*;
|
||||
use crate::controllers::base_controller::{build_fatal_error_page, redirect_user};
|
||||
use crate::data::action_logger::{Action, ActionLogger};
|
||||
use crate::data::app_config::AppConfig;
|
||||
use crate::data::client::{ClientID, ClientManager};
|
||||
use crate::data::client::{AuthenticationFlow, ClientID, ClientManager};
|
||||
use crate::data::code_challenge::CodeChallenge;
|
||||
use crate::data::current_user::CurrentUser;
|
||||
use crate::data::id_token::IdToken;
|
||||
@ -97,7 +97,7 @@ pub struct AuthorizeQuery {
|
||||
redirect_uri: String,
|
||||
|
||||
/// RECOMMENDED. Opaque value used to maintain state between the request and the callback. Typically, Cross-Site Request Forgery (CSRF, XSRF) mitigation is done by cryptographically binding the value of this parameter with a browser cookie.
|
||||
state: String,
|
||||
state: Option<String>,
|
||||
|
||||
/// OPTIONAL. String value used to associate a Client session with an ID Token, and to mitigate replay attacks. The value is passed through unmodified from the Authentication Request to the ID Token. Sufficient entropy MUST be present in the nonce values used to prevent attackers from guessing values.
|
||||
nonce: Option<String>,
|
||||
@ -118,16 +118,20 @@ fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> Htt
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}?error={}?error_description={}&state={}",
|
||||
"{}?error={}?error_description={}{}",
|
||||
query.redirect_uri,
|
||||
urlencoding::encode(error),
|
||||
urlencoding::encode(description),
|
||||
urlencoding::encode(&query.state)
|
||||
match &query.state {
|
||||
Some(s) => format!("&state={}", urlencoding::encode(s)),
|
||||
None => "".to_string(),
|
||||
}
|
||||
),
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn authorize(
|
||||
req: HttpRequest,
|
||||
user: CurrentUser,
|
||||
@ -136,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,
|
||||
};
|
||||
@ -147,39 +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!"));
|
||||
}
|
||||
|
||||
if !query.scope.split(' ').any(|x| x == "openid") {
|
||||
return error_redirect(&query, "invalid_request", "openid scope missing!");
|
||||
}
|
||||
|
||||
if !query.response_type.eq("code") {
|
||||
return error_redirect(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Only code response type is supported!",
|
||||
return Ok(
|
||||
HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"))
|
||||
);
|
||||
}
|
||||
|
||||
if query.state.is_empty() {
|
||||
return error_redirect(&query, "invalid_request", "State is empty!");
|
||||
if !query.scope.split(' ').any(|x| x == "openid") {
|
||||
return Ok(error_redirect(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"openid scope missing!",
|
||||
));
|
||||
}
|
||||
|
||||
if query.state.as_ref().map(String::is_empty).unwrap_or(false) {
|
||||
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,
|
||||
@ -191,49 +201,110 @@ 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!",
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
// Save all authentication information in memory
|
||||
let session = Session {
|
||||
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
|
||||
client: client.id.clone(),
|
||||
user: user.uid.clone(),
|
||||
auth_time: SessionIdentity(Some(&id)).auth_time(),
|
||||
redirect_uri,
|
||||
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
|
||||
authorization_code_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
|
||||
access_token: None,
|
||||
access_token_expire_at: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
|
||||
refresh_token: "".to_string(),
|
||||
refresh_token_expire_at: 0,
|
||||
nonce: query.0.nonce,
|
||||
code_challenge,
|
||||
};
|
||||
sessions
|
||||
.send(openid_sessions_actor::PushNewSession(session.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
// Check that requested authorization flow is supported
|
||||
if query.response_type != "code" && query.response_type != "id_token" {
|
||||
return Ok(error_redirect(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Unsupported authorization flow!",
|
||||
));
|
||||
}
|
||||
|
||||
log::trace!("New OpenID session: {:#?}", session);
|
||||
logger.log(Action::NewOpenIDSession { client: &client });
|
||||
match (client.auth_flow(), query.response_type.as_str()) {
|
||||
(AuthenticationFlow::AuthorizationCode, "code") => {
|
||||
// Save all authentication information in memory
|
||||
let session = Session {
|
||||
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
|
||||
client: client.id.clone(),
|
||||
user: user.uid.clone(),
|
||||
auth_time: SessionIdentity(Some(&id)).auth_time(),
|
||||
redirect_uri,
|
||||
authorization_code: rand_str(OPEN_ID_AUTHORIZATION_CODE_LEN),
|
||||
authorization_code_expire_at: time() + OPEN_ID_AUTHORIZATION_CODE_TIMEOUT,
|
||||
access_token: None,
|
||||
access_token_expire_at: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT,
|
||||
refresh_token: "".to_string(),
|
||||
refresh_token_expire_at: 0,
|
||||
nonce: query.0.nonce,
|
||||
code_challenge,
|
||||
};
|
||||
sessions
|
||||
.send(openid_sessions_actor::PushNewSession(session.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}?state={}&session_state={}&code={}",
|
||||
session.redirect_uri,
|
||||
urlencoding::encode(&query.0.state),
|
||||
urlencoding::encode(&session.session_id.0),
|
||||
urlencoding::encode(&session.authorization_code)
|
||||
),
|
||||
))
|
||||
.finish()
|
||||
log::trace!("New OpenID session: {:#?}", session);
|
||||
logger.log(Action::NewOpenIDSession { client: &client });
|
||||
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header((
|
||||
"Location",
|
||||
format!(
|
||||
"{}?{}session_state={}&code={}",
|
||||
session.redirect_uri,
|
||||
match &query.0.state {
|
||||
Some(state) => format!("state={}&", urlencoding::encode(state)),
|
||||
None => "".to_string(),
|
||||
},
|
||||
urlencoding::encode(&session.session_id.0),
|
||||
urlencoding::encode(&session.authorization_code)
|
||||
),
|
||||
))
|
||||
.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())
|
||||
}
|
||||
|
||||
(flow, code) => {
|
||||
log::warn!(
|
||||
"For client {:?}, configured with flow {:?}, made request with code {}",
|
||||
client.id,
|
||||
flow,
|
||||
code
|
||||
);
|
||||
Ok(error_redirect(
|
||||
&query,
|
||||
"invalid_request",
|
||||
"Requested authentication flow is unsupported / not configured for this client!",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@ -344,7 +415,8 @@ pub async fn token(
|
||||
.find_by_id(&client_id)
|
||||
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
|
||||
|
||||
if !client.secret.eq(&client_secret) {
|
||||
// Retrieving token requires the client to have a defined secret
|
||||
if client.secret != Some(client_secret) {
|
||||
return Ok(error_response(
|
||||
&query,
|
||||
"invalid_request",
|
||||
|
Reference in New Issue
Block a user