Merge factors type for authentication

This commit is contained in:
2022-11-11 12:26:02 +01:00
parent 8d231c0b45
commit af383720b7
44 changed files with 1177 additions and 674 deletions

View File

@ -2,12 +2,12 @@ use std::fmt::Debug;
use actix::Addr;
use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use actix_web::error::ErrorUnauthorized;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use crate::actors::{openid_sessions_actor, users_actor};
use crate::actors::openid_sessions_actor::{OpenIDSessionsActor, Session, SessionID};
use crate::actors::users_actor::UsersActor;
use crate::actors::{openid_sessions_actor, users_actor};
use crate::constants::*;
use crate::controllers::base_controller::build_fatal_error_page;
use crate::data::app_config::AppConfig;
@ -15,7 +15,7 @@ use crate::data::client::{ClientID, ClientManager};
use crate::data::code_challenge::CodeChallenge;
use crate::data::current_user::CurrentUser;
use crate::data::id_token::IdToken;
use crate::data::jwt_signer::{JsonWebKey, JWTSigner};
use crate::data::jwt_signer::{JWTSigner, JsonWebKey};
use crate::data::open_id_user_info::OpenIDUserInfo;
use crate::data::openid_config::OpenIDConfig;
use crate::data::session_identity::SessionIdentity;
@ -24,7 +24,9 @@ use crate::utils::string_utils::rand_str;
use crate::utils::time::time;
pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>) -> impl Responder {
let is_secure_request = req.headers().get("HTTP_X_FORWARDED_PROTO")
let is_secure_request = req
.headers()
.get("HTTP_X_FORWARDED_PROTO")
.map(|v| v.to_str().unwrap_or_default().to_lowercase().eq("https"))
.unwrap_or(false);
@ -33,10 +35,14 @@ pub async fn get_configuration(req: HttpRequest, app_conf: web::Data<AppConfig>)
Some(s) => s.to_str().unwrap_or_default(),
};
let curr_origin = format!("{}://{}", match is_secure_request {
true => "https",
false => "http"
}, host);
let curr_origin = format!(
"{}://{}",
match is_secure_request {
true => "https",
false => "http",
},
host
);
HttpResponse::Ok().json(OpenIDConfig {
issuer: app_conf.website_origin.clone(),
@ -80,35 +86,43 @@ pub struct AuthorizeQuery {
}
fn error_redirect(query: &AuthorizeQuery, error: &str, description: &str) -> HttpResponse {
log::warn!("Failed to process sign in request ({} => {}): {:?}", error, description, query);
log::warn!(
"Failed to process sign in request ({} => {}): {:?}",
error,
description,
query
);
HttpResponse::Found()
.append_header(
("Location", format!(
.append_header((
"Location",
format!(
"{}?error={}?error_description={}&state={}",
query.redirect_uri,
urlencoding::encode(error),
urlencoding::encode(description),
urlencoding::encode(&query.state)
))
)
),
))
.finish()
}
pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<AuthorizeQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>) -> impl Responder {
pub async fn authorize(
user: CurrentUser,
id: Identity,
query: web::Query<AuthorizeQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
) -> impl Responder {
let client = match clients.find_by_id(&query.client_id) {
None => {
return HttpResponse::BadRequest()
.body(build_fatal_error_page("Client is invalid!"));
return HttpResponse::BadRequest().body(build_fatal_error_page("Client is invalid!"));
}
Some(c) => c
Some(c) => c,
};
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 HttpResponse::BadRequest().body(build_fatal_error_page("Redirect URI is invalid!"));
}
if !query.scope.split(' ').any(|x| x == "openid") {
@ -116,7 +130,11 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
}
if !query.response_type.eq("code") {
return error_redirect(&query, "invalid_request", "Only code response type is supported!");
return error_redirect(
&query,
"invalid_request",
"Only code response type is supported!",
);
}
if query.state.is_empty() {
@ -127,18 +145,27 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
Some(chal) => {
let meth = query.0.code_challenge_method.as_deref().unwrap_or("plain");
if !meth.eq("S256") && !meth.eq("plain") {
return error_redirect(&query, "invalid_request",
"Only S256 and plain code challenge methods are supported!");
return error_redirect(
&query,
"invalid_request",
"Only S256 and plain code challenge methods are supported!",
);
}
Some(CodeChallenge { code_challenge: chal, code_challenge_method: meth.to_string() })
Some(CodeChallenge {
code_challenge: chal,
code_challenge_method: meth.to_string(),
})
}
_ => None
_ => None,
};
// Check if user is authorized to access the application
if !user.can_access_app(&client.id) {
return error_redirect(&query, "invalid_request",
"User is not authorized to access this application!");
return error_redirect(
&query,
"invalid_request",
"User is not authorized to access this application!",
);
}
// Save all authentication information in memory
@ -157,18 +184,25 @@ pub async fn authorize(user: CurrentUser, id: Identity, query: web::Query<Author
nonce: query.0.nonce,
code_challenge,
};
sessions.send(openid_sessions_actor::PushNewSession(session.clone())).await.unwrap();
sessions
.send(openid_sessions_actor::PushNewSession(session.clone()))
.await
.unwrap();
log::trace!("New OpenID session: {:#?}", session);
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()
.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()
}
#[derive(serde::Serialize)]
@ -178,12 +212,16 @@ struct ErrorResponse {
}
pub fn error_response<D: Debug>(query: &D, error: &str, description: &str) -> HttpResponse {
log::warn!("request failed: {} - {} => '{:#?}'", error, description, query);
HttpResponse::BadRequest()
.json(ErrorResponse {
error: error.to_string(),
error_description: description.to_string(),
})
log::warn!(
"request failed: {} - {} => '{:#?}'",
error,
description,
query
);
HttpResponse::BadRequest().json(ErrorResponse {
error: error.to_string(),
error_description: description.to_string(),
})
}
#[derive(Debug, serde::Deserialize)]
@ -198,7 +236,6 @@ pub struct TokenRefreshTokenQuery {
refresh_token: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct TokenQuery {
grant_type: String,
@ -222,115 +259,175 @@ pub struct TokenResponse {
id_token: Option<String>,
}
pub async fn token(req: HttpRequest,
query: web::Form<TokenQuery>,
clients: web::Data<ClientManager>,
app_config: web::Data<AppConfig>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
jwt_signer: web::Data<JWTSigner>) -> actix_web::Result<HttpResponse> {
pub async fn token(
req: HttpRequest,
query: web::Form<TokenQuery>,
clients: web::Data<ClientManager>,
app_config: web::Data<AppConfig>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
jwt_signer: web::Data<JWTSigner>,
) -> actix_web::Result<HttpResponse> {
// Extraction authentication information
let authorization_header = req.headers().get("authorization");
let (client_id, client_secret) = match (&query.client_id, &query.client_secret, authorization_header) {
// post authentication
(Some(client_id), Some(client_secret), None) => {
(client_id.clone(), client_secret.to_string())
}
// Basic authentication
(None, None, Some(v)) => {
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
None => {
return Ok(error_response(
&query,
"invalid_request",
&format!("Authorization header does not start with 'Basic ', got '{:#?}'", v),
));
}
Some(v) => v
};
let decode = String::from_utf8_lossy(&match base64::decode(token) {
Ok(d) => d,
Err(e) => {
log::error!("Failed to decode authorization header: {:?}", e);
return Ok(error_response(&query, "invalid_request", "Failed to decode authorization header!"));
}
}).to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string())
let (client_id, client_secret) =
match (&query.client_id, &query.client_secret, authorization_header) {
// post authentication
(Some(client_id), Some(client_secret), None) => {
(client_id.clone(), client_secret.to_string())
}
}
_ => {
return Ok(error_response(&query, "invalid_request", "Authentication method unknown!"));
}
};
// Basic authentication
(None, None, Some(v)) => {
let token = match v.to_str().unwrap_or_default().strip_prefix("Basic ") {
None => {
return Ok(error_response(
&query,
"invalid_request",
&format!(
"Authorization header does not start with 'Basic ', got '{:#?}'",
v
),
));
}
Some(v) => v,
};
let decode = String::from_utf8_lossy(&match base64::decode(token) {
Ok(d) => d,
Err(e) => {
log::error!("Failed to decode authorization header: {:?}", e);
return Ok(error_response(
&query,
"invalid_request",
"Failed to decode authorization header!",
));
}
})
.to_string();
match decode.split_once(':') {
None => (ClientID(decode), "".to_string()),
Some((id, secret)) => (ClientID(id.to_string()), secret.to_string()),
}
}
_ => {
return Ok(error_response(
&query,
"invalid_request",
"Authentication method unknown!",
));
}
};
let client = clients
.find_by_id(&client_id)
.ok_or_else(|| ErrorUnauthorized("Client not found"))?;
if !client.secret.eq(&client_secret) {
return Ok(error_response(&query, "invalid_request", "Client secret is invalid!"));
return Ok(error_response(
&query,
"invalid_request",
"Client secret is invalid!",
));
}
let token_response = match (query.grant_type.as_str(),
&query.authorization_code_query,
&query.refresh_token_query) {
let token_response = match (
query.grant_type.as_str(),
&query.authorization_code_query,
&query.refresh_token_query,
) {
("authorization_code", Some(q), _) => {
let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByAuthorizationCode(q.code.clone()))
.await.unwrap()
.send(openid_sessions_actor::FindSessionByAuthorizationCode(
q.code.clone(),
))
.await
.unwrap()
{
None => {
return Ok(error_response(&query, "invalid_request", "Session not found!"));
return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
}
Some(s) => s,
};
if session.client != client.id {
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
return Ok(error_response(
&query,
"invalid_request",
"Client mismatch!",
));
}
if session.redirect_uri != q.redirect_uri {
return Ok(error_response(&query, "invalid_request", "Invalid redirect URI!"));
return Ok(error_response(
&query,
"invalid_request",
"Invalid redirect URI!",
));
}
if session.authorization_code_expire_at < time() {
return Ok(error_response(&query, "invalid_request", "Authorization code expired!"));
return Ok(error_response(
&query,
"invalid_request",
"Authorization code expired!",
));
}
// Check code challenge, if needed
if let Some(chall) = &session.code_challenge {
let code_verifier = match &q.code_verifier {
None => {
return Ok(error_response(&query, "access_denied", "Code verifier missing"));
return Ok(error_response(
&query,
"access_denied",
"Code verifier missing",
));
}
Some(s) => s
Some(s) => s,
};
if !chall.verify_code(code_verifier) {
return Ok(error_response(&query, "invalid_grant", "Invalid code verifier"));
return Ok(error_response(
&query,
"invalid_grant",
"Invalid code verifier",
));
}
} else if q.code_verifier.is_some() {
return Ok(error_response(&query, "invalid_grant", "Unexpected `code_verifier` parameter!"));
return Ok(error_response(
&query,
"invalid_grant",
"Unexpected `code_verifier` parameter!",
));
}
if session.access_token.is_some() {
return Ok(error_response(&query, "invalid_request", "Authorization code already used!"));
return Ok(error_response(
&query,
"invalid_request",
"Authorization code already used!",
));
}
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await
.unwrap();
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user.clone()))
.await.unwrap().0;
let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user.clone()))
.await
.unwrap()
.0;
let user = match user {
None => return Ok(error_response(&query, "invalid_request", "User not found!")),
Some(u) => u,
@ -359,28 +456,44 @@ pub async fn token(req: HttpRequest,
("refresh_token", _, Some(q)) => {
let mut session: Session = match sessions
.send(openid_sessions_actor::FindSessionByRefreshToken(q.refresh_token.clone()))
.await.unwrap()
.send(openid_sessions_actor::FindSessionByRefreshToken(
q.refresh_token.clone(),
))
.await
.unwrap()
{
None => {
return Ok(error_response(&query, "invalid_request", "Session not found!"));
return Ok(error_response(
&query,
"invalid_request",
"Session not found!",
));
}
Some(s) => s,
};
if session.client != client.id {
return Ok(error_response(&query, "invalid_request", "Client mismatch!"));
return Ok(error_response(
&query,
"invalid_request",
"Client mismatch!",
));
}
if session.refresh_token_expire_at < time() {
return Ok(error_response(&query, "access_denied", "Refresh token has expired!"));
return Ok(error_response(
&query,
"access_denied",
"Refresh token has expired!",
));
}
session.regenerate_access_and_refresh_tokens(&app_config, &jwt_signer)?;
sessions
.send(openid_sessions_actor::UpdateSession(session.clone()))
.await.unwrap();
.await
.unwrap();
TokenResponse {
access_token: session.access_token.expect("Missing access token!"),
@ -392,7 +505,11 @@ pub async fn token(req: HttpRequest,
}
_ => {
return Ok(error_response(&query, "invalid_request", "Grant type unsupported!"));
return Ok(error_response(
&query,
"invalid_request",
"Grant type unsupported!",
));
}
};
@ -408,16 +525,20 @@ struct CertsResponse {
}
pub async fn cert_uri(jwt_signer: web::Data<JWTSigner>) -> impl Responder {
HttpResponse::Ok().json(CertsResponse { keys: vec![jwt_signer.get_json_web_key()] })
HttpResponse::Ok().json(CertsResponse {
keys: vec![jwt_signer.get_json_web_key()],
})
}
fn user_info_error(err: &str, description: &str) -> HttpResponse {
HttpResponse::Unauthorized()
.insert_header(("WWW-Authenticate", format!(
"Bearer error=\"{}\", error_description=\"{}\"",
err,
description
)))
.insert_header((
"WWW-Authenticate",
format!(
"Bearer error=\"{}\", error_description=\"{}\"",
err, description
),
))
.finish()
}
@ -426,37 +547,46 @@ pub struct UserInfoQuery {
access_token: Option<String>,
}
pub async fn user_info_post(req: HttpRequest,
form: Option<web::Form<UserInfoQuery>>,
query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
user_info(req,
form
.map(|f| f.0.access_token)
.unwrap_or_default()
.or(query.0.access_token),
sessions,
users,
).await
pub async fn user_info_post(
req: HttpRequest,
form: Option<web::Form<UserInfoQuery>>,
query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
user_info(
req,
form.map(|f| f.0.access_token)
.unwrap_or_default()
.or(query.0.access_token),
sessions,
users,
)
.await
}
pub async fn user_info_get(req: HttpRequest, query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
pub async fn user_info_get(
req: HttpRequest,
query: web::Query<UserInfoQuery>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
user_info(req, query.0.access_token, sessions, users).await
}
/// Authenticate request using RFC6750 <https://datatracker.ietf.org/doc/html/rfc6750>///
async fn user_info(req: HttpRequest, token: Option<String>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>) -> impl Responder {
async fn user_info(
req: HttpRequest,
token: Option<String>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
users: web::Data<Addr<UsersActor>>,
) -> impl Responder {
let token = match token {
Some(t) => t,
None => {
let token = match req.headers().get("Authorization") {
None => return user_info_error("invalid_request", "Missing access token!"),
Some(t) => t
Some(t) => t,
};
let token = match token.to_str() {
@ -465,7 +595,12 @@ async fn user_info(req: HttpRequest, token: Option<String>,
};
let token = match token.strip_prefix("Bearer ") {
None => return user_info_error("invalid_request", "Header token does not start with 'Bearer '!"),
None => {
return user_info_error(
"invalid_request",
"Header token does not start with 'Bearer '!",
)
}
Some(t) => t,
};
@ -474,7 +609,9 @@ async fn user_info(req: HttpRequest, token: Option<String>,
};
let session: Option<Session> = sessions
.send(openid_sessions_actor::FindSessionByAccessToken(token)).await.unwrap();
.send(openid_sessions_actor::FindSessionByAccessToken(token))
.await
.unwrap();
let session = match session {
None => {
return user_info_error("invalid_request", "Session not found!");
@ -486,7 +623,11 @@ async fn user_info(req: HttpRequest, token: Option<String>,
return user_info_error("invalid_request", "Access token has expired!");
}
let user: Option<User> = users.send(users_actor::GetUserRequest(session.user)).await.unwrap().0;
let user: Option<User> = users
.send(users_actor::GetUserRequest(session.user))
.await
.unwrap()
.0;
let user = match user {
None => {
return user_info_error("invalid_request", "Failed to extract user information!");
@ -494,14 +635,13 @@ async fn user_info(req: HttpRequest, token: Option<String>,
Some(u) => u,
};
HttpResponse::Ok()
.json(OpenIDUserInfo {
name: user.full_name(),
sub: user.uid.0,
given_name: user.first_name,
family_name: user.last_name,
preferred_username: user.username,
email: user.email,
email_verified: true,
})
}
HttpResponse::Ok().json(OpenIDUserInfo {
name: user.full_name(),
sub: user.uid.0,
given_name: user.first_name,
family_name: user.last_name,
preferred_username: user.username,
email: user.email,
email_verified: true,
})
}