Can finish open id login

This commit is contained in:
Pierre HUBERT 2023-06-02 15:04:49 +02:00
parent 29c0247b4b
commit d54f9e4503
6 changed files with 150 additions and 4 deletions

View File

@ -796,6 +796,7 @@ dependencies = [
"redis", "redis",
"serde", "serde",
"serde_json", "serde_json",
"thiserror",
] ]
[[package]] [[package]]
@ -1740,6 +1741,26 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "thiserror"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.21" version = "0.3.21"

View File

@ -23,3 +23,4 @@ lettre = "0.10.4"
rand = "0.8.5" rand = "0.8.5"
bcrypt = "0.14.0" bcrypt = "0.14.0"
light-openid = "1.0.1" light-openid = "1.0.1"
thiserror = "1.0.40"

View File

@ -49,3 +49,10 @@ where
Ok(()) Ok(())
} }
/// Remove a value from Redis
pub async fn remove_value(key: &str) -> anyhow::Result<()> {
execute_request(|conn| Ok(conn.del(key)?))?;
Ok(())
}

View File

@ -254,3 +254,53 @@ pub async fn start_openid_login(
Ok(HttpResponse::Ok().json(StartOpenIDLoginResponse { url })) Ok(HttpResponse::Ok().json(StartOpenIDLoginResponse { url }))
} }
#[derive(serde::Deserialize)]
pub struct FinishOpenIDLoginQuery {
code: String,
state: String,
}
/// Finish OpenID login
pub async fn finish_openid_login(
remote_ip: RemoteIP,
req: web::Json<FinishOpenIDLoginQuery>,
) -> HttpResult {
let user_info = openid_service::finish_login(remote_ip.0, &req.code, &req.state).await?;
if user_info.email_verified != Some(true) {
log::error!("Email is not verified!");
return Ok(
HttpResponse::Unauthorized().json("Email non vérifié par le fournisseur d'identité !")
);
}
let mail = match user_info.email {
Some(m) => m,
None => {
return Ok(HttpResponse::Unauthorized()
.json("Email non spécifié par le fournisseur d'identité !"));
}
};
// Create the account, if required
if !users_service::exists_email(&mail).await? {
let name = match (user_info.name, user_info.given_name, user_info.family_name) {
(Some(name), _, _) => name,
(None, Some(g), Some(f)) => format!("{g} {f}"),
(_, _, _) => {
return Ok(HttpResponse::Unauthorized()
.json("Nom non spécifié par le fournisseur d'identité !"));
}
};
users_service::create_account(&name, &mail).await?;
}
let user = users_service::get_by_mail(&mail).await?;
// OpenID auth is enough to validate accounts
users_service::validate_account(&user).await?;
finish_login(&user).await
}

View File

@ -47,6 +47,10 @@ async fn main() -> std::io::Result<()> {
"/auth/start_openid_login", "/auth/start_openid_login",
web::post().to(auth_controller::start_openid_login), web::post().to(auth_controller::start_openid_login),
) )
.route(
"/auth/finish_openid_login",
web::post().to(auth_controller::finish_openid_login),
)
// User controller // User controller
.route("/user/info", web::get().to(user_controller::auth_info)) .route("/user/info", web::get().to(user_controller::auth_info))
}) })

View File

@ -8,12 +8,28 @@ use crate::utils::time_utils::time;
use light_openid::primitives::OpenIDConfig; use light_openid::primitives::OpenIDConfig;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::ErrorKind;
use std::net::IpAddr; use std::net::IpAddr;
thread_local! { thread_local! {
static CONFIG_CACHES: RefCell<HashMap<String, OpenIDConfig>> = RefCell::new(Default::default()); static CONFIG_CACHES: RefCell<HashMap<String, OpenIDConfig>> = RefCell::new(Default::default());
}
#[derive(thiserror::Error, Debug)]
enum OpenIDServiceError {
#[error("Given provider not found!")]
FindProvider,
#[error("Failed to get provider configuration: {0}")]
GetProviderConfiguration(String),
#[error("Provided state does not exists!")]
NonExistingState,
#[error("The state has expired!")]
ExpiredState,
#[error("Invalid IP address")]
InvalidIP,
#[error("Failed to query token endpoint: {0}")]
QueryTokenEndpoint(String),
#[error("Failed to query user info endpoint: {0}")]
QueryUserInfoEndpoint(String),
} }
struct OpenIDClient<'a> { struct OpenIDClient<'a> {
@ -53,7 +69,7 @@ async fn load_provider_info(prov_id: &str) -> anyhow::Result<OpenIDClient> {
.openid_providers() .openid_providers()
.into_iter() .into_iter()
.find(|p| p.id.eq(prov_id)) .find(|p| p.id.eq(prov_id))
.ok_or_else(|| std::io::Error::new(ErrorKind::Other, "Provider not found!"))?; .ok_or(OpenIDServiceError::FindProvider)?;
if let Some(conf) = CONFIG_CACHES.with(|i| i.borrow().get(prov_id).cloned()) { if let Some(conf) = CONFIG_CACHES.with(|i| i.borrow().get(prov_id).cloned()) {
return Ok(OpenIDClient { prov, conf }); return Ok(OpenIDClient { prov, conf });
@ -61,7 +77,7 @@ async fn load_provider_info(prov_id: &str) -> anyhow::Result<OpenIDClient> {
let conf = OpenIDConfig::load_from_url(prov.configuration_url) let conf = OpenIDConfig::load_from_url(prov.configuration_url)
.await .await
.map_err(|e| std::io::Error::new(ErrorKind::Other, e.to_string()))?; .map_err(|e| OpenIDServiceError::GetProviderConfiguration(e.to_string()))?;
CONFIG_CACHES.with(|i| { CONFIG_CACHES.with(|i| {
i.borrow_mut() i.borrow_mut()
@ -83,3 +99,50 @@ pub async fn start_login(prov_id: &str, ip: IpAddr) -> anyhow::Result<String> {
&AppConfig::get().oidc_redirect_url, &AppConfig::get().oidc_redirect_url,
)) ))
} }
/// Finish OpenID login
pub async fn finish_login(
ip: IpAddr,
code: &str,
state_key: &str,
) -> anyhow::Result<light_openid::primitives::OpenIDUserInfo> {
// Consume state
let state = redis_connection::get_value::<OpenIDState>(&redis_key(state_key))
.await?
.ok_or(OpenIDServiceError::NonExistingState)?;
redis_connection::remove_value(&redis_key(state_key)).await?;
if state.expire < time() {
return Err(OpenIDServiceError::ExpiredState.into());
}
if state.ip != ip {
log::error!(
"Mismatching IP addresses (expected {} / got {}",
state.ip,
ip
);
return Err(OpenIDServiceError::InvalidIP.into());
}
// Query provider
let prov = load_provider_info(&state.prov_id).await?;
let (token, _) = prov
.conf
.request_token(
prov.prov.client_id,
prov.prov.client_secret,
code,
&AppConfig::get().oidc_redirect_url,
)
.await
.map_err(|e| OpenIDServiceError::QueryTokenEndpoint(e.to_string()))?;
let (user_info, _) = prov
.conf
.request_user_info(&token)
.await
.map_err(|e| OpenIDServiceError::QueryUserInfoEndpoint(e.to_string()))?;
Ok(user_info)
}