Can finish open id login
This commit is contained in:
parent
29c0247b4b
commit
d54f9e4503
21
geneit_backend/Cargo.lock
generated
21
geneit_backend/Cargo.lock
generated
@ -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"
|
||||||
|
@ -22,4 +22,5 @@ redis = "0.23.0"
|
|||||||
lettre = "0.10.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"
|
@ -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(())
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user