Finish OIDC login
This commit is contained in:
@ -2,4 +2,8 @@
|
||||
pub mod sessions {
|
||||
/// OpenID auth session state key
|
||||
pub const OIDC_STATE_KEY: &str = "oidc-state";
|
||||
/// OpenID auth remote IP address
|
||||
pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip";
|
||||
/// Authenticated ID
|
||||
pub const USER_ID: &str = "uid";
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::controllers::HttpResult;
|
||||
use crate::controllers::{HttpFailure, HttpResult};
|
||||
use crate::extractors::money_session::MoneySession;
|
||||
use actix_web::HttpResponse;
|
||||
use crate::services::users_service;
|
||||
use actix_remote_ip::RemoteIP;
|
||||
use actix_web::{HttpResponse, web};
|
||||
use light_openid::primitives::OpenIDConfig;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@ -10,7 +12,7 @@ struct StartOIDCResponse {
|
||||
}
|
||||
|
||||
/// Start OIDC authentication
|
||||
pub async fn start_oidc(session: MoneySession) -> HttpResult {
|
||||
pub async fn start_oidc(session: MoneySession, remote_ip: RemoteIP) -> HttpResult {
|
||||
let prov = AppConfig::get().openid_provider();
|
||||
|
||||
let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await {
|
||||
@ -22,7 +24,7 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult {
|
||||
}
|
||||
};
|
||||
|
||||
let state = match session.gen_oidc_state() {
|
||||
let state = match session.gen_oidc_state(remote_ip.0) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to generate auth state! {e}");
|
||||
@ -39,4 +41,66 @@ pub async fn start_oidc(session: MoneySession) -> HttpResult {
|
||||
}))
|
||||
}
|
||||
|
||||
// TODO : take from previous projects
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct FinishOpenIDLoginQuery {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
/// Finish OIDC authentication
|
||||
pub async fn finish_oidc(
|
||||
session: MoneySession,
|
||||
remote_ip: RemoteIP,
|
||||
req: web::Json<FinishOpenIDLoginQuery>,
|
||||
) -> HttpResult {
|
||||
if let Err(e) = session.validate_state(&req.state, remote_ip.0) {
|
||||
log::error!("Failed to validate OIDC CB state! {e}");
|
||||
return Ok(HttpResponse::BadRequest().json("Invalid state!"));
|
||||
}
|
||||
|
||||
let prov = AppConfig::get().openid_provider();
|
||||
|
||||
let conf = OpenIDConfig::load_from_url(prov.configuration_url)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
|
||||
let (token, _) = conf
|
||||
.request_token(
|
||||
prov.client_id,
|
||||
prov.client_secret,
|
||||
&req.code,
|
||||
&AppConfig::get().oidc_redirect_url(),
|
||||
)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
let (user_info, _) = conf
|
||||
.request_user_info(&token)
|
||||
.await
|
||||
.map_err(HttpFailure::OpenID)?;
|
||||
|
||||
if user_info.email_verified != Some(true) {
|
||||
log::error!("Email is not verified!");
|
||||
return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
|
||||
}
|
||||
|
||||
let mail = match user_info.email {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
|
||||
}
|
||||
};
|
||||
|
||||
let user_name = user_info.name.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{} {}",
|
||||
user_info.given_name.as_deref().unwrap_or(""),
|
||||
user_info.family_name.as_deref().unwrap_or("")
|
||||
)
|
||||
});
|
||||
|
||||
let user = users_service::create_or_update_user(&mail, &user_name).await?;
|
||||
|
||||
session.set_user(&user)?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
@ -1,9 +1,24 @@
|
||||
use crate::constants;
|
||||
use crate::models::users::User;
|
||||
use crate::utils::rand_utils::rand_string;
|
||||
use actix_session::Session;
|
||||
use actix_web::dev::Payload;
|
||||
use actix_web::{Error, FromRequest, HttpRequest};
|
||||
use futures_util::future::{Ready, ready};
|
||||
use std::net::IpAddr;
|
||||
|
||||
/// Money session errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum MoneySessionError {
|
||||
#[error("Missing state!")]
|
||||
OIDCMissingState,
|
||||
#[error("Missing IP address!")]
|
||||
OIDCMissingIP,
|
||||
#[error("Invalid state!")]
|
||||
OIDCInvalidState,
|
||||
#[error("Invalid IP address!")]
|
||||
OIDCInvalidIP,
|
||||
}
|
||||
|
||||
/// Money session
|
||||
///
|
||||
@ -12,12 +27,42 @@ pub struct MoneySession(Session);
|
||||
|
||||
impl MoneySession {
|
||||
/// Generate OpenID state for this session
|
||||
pub fn gen_oidc_state(&self) -> anyhow::Result<String> {
|
||||
pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result<String> {
|
||||
let random_string = rand_string(50);
|
||||
self.0
|
||||
.insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?;
|
||||
self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?;
|
||||
Ok(random_string)
|
||||
}
|
||||
|
||||
/// Validate OpenID state
|
||||
pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> {
|
||||
let session_state: String = self
|
||||
.0
|
||||
.get(constants::sessions::OIDC_STATE_KEY)?
|
||||
.ok_or(MoneySessionError::OIDCMissingState)?;
|
||||
|
||||
let session_ip: IpAddr = self
|
||||
.0
|
||||
.get(constants::sessions::OIDC_REMOTE_IP)?
|
||||
.ok_or(MoneySessionError::OIDCMissingIP)?;
|
||||
|
||||
if session_state != state {
|
||||
return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidState));
|
||||
}
|
||||
|
||||
if session_ip != ip {
|
||||
return Err(anyhow::anyhow!(MoneySessionError::OIDCInvalidIP));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set current user
|
||||
pub fn set_user(&self, user: &User) -> anyhow::Result<()> {
|
||||
self.0.insert(constants::sessions::USER_ID, user.id())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for MoneySession {
|
||||
|
@ -81,6 +81,10 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/auth/start_oidc",
|
||||
web::get().to(auth_controller::start_oidc),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/finish_oidc",
|
||||
web::post().to(auth_controller::finish_oidc),
|
||||
)
|
||||
})
|
||||
.bind(AppConfig::get().listen_address.as_str())?
|
||||
.run()
|
||||
|
Reference in New Issue
Block a user