Start to build init openid login

This commit is contained in:
2023-06-02 11:49:18 +02:00
parent bca1768fc4
commit 83d731c546
9 changed files with 385 additions and 21 deletions

View File

@ -112,9 +112,9 @@ pub struct AppConfig {
#[arg(long, env, default_value = "bar")]
pub oidc_client_secret: String,
/// OpenID login callback URL
/// OpenID login redirect URL
#[arg(long, env, default_value = "http://localhost:3000/oidc_cb")]
pub oidc_callback_url: String,
pub oidc_redirect_url: String,
}
lazy_static::lazy_static! {
@ -155,29 +155,29 @@ impl AppConfig {
}
/// Get OpenID providers configuration
pub fn openid_providers(&self) -> Vec<OIDCProvider> {
pub fn openid_providers(&self) -> Vec<OIDCProvider<'_>> {
if self.disable_oidc {
return vec![];
}
return vec![OIDCProvider {
id: "first_prov".to_string(),
client_id: self.oidc_client_id.to_string(),
client_secret: self.oidc_client_secret.to_string(),
configuration_url: self.oidc_configuration_url.to_string(),
name: self.oidc_provider_name.to_string(),
}];
vec![OIDCProvider {
id: "first_prov",
client_id: self.oidc_client_id.as_str(),
client_secret: self.oidc_client_secret.as_str(),
configuration_url: self.oidc_configuration_url.as_str(),
name: self.oidc_provider_name.as_str(),
}]
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct OIDCProvider {
pub id: String,
pub struct OIDCProvider<'a> {
pub id: &'a str,
#[serde(skip_serializing)]
pub client_id: String,
pub client_id: &'a str,
#[serde(skip_serializing)]
pub client_secret: String,
pub client_secret: &'a str,
#[serde(skip_serializing)]
pub configuration_url: String,
pub name: String,
pub configuration_url: &'a str,
pub name: &'a str,
}

View File

@ -36,3 +36,6 @@ impl Default for StaticConstraints {
/// Password reset token duration
pub const PASSWORD_RESET_TOKEN_DURATION: Duration = Duration::from_secs(3600 * 24);
/// OpenID state duration
pub const OPEN_ID_STATE_DURATION: Duration = Duration::from_secs(3600);

View File

@ -2,7 +2,7 @@ use crate::constants::StaticConstraints;
use crate::controllers::HttpResult;
use crate::models::{User, UserID};
use crate::services::rate_limiter_service::RatedAction;
use crate::services::{login_token_service, rate_limiter_service, users_service};
use crate::services::{login_token_service, openid_service, rate_limiter_service, users_service};
use actix_remote_ip::RemoteIP;
use actix_web::{web, HttpResponse};
@ -227,3 +227,20 @@ async fn finish_login(user: &User) -> HttpResult {
token: login_token_service::gen_new_token(user).await?,
}))
}
#[derive(serde::Deserialize)]
pub struct StartOpenIDLoginQuery {
provider: String,
}
#[derive(serde::Serialize)]
pub struct StartOpenIDLoginResponse {
url: String,
}
/// Start OpenID login
pub async fn start_openid_login(ip: RemoteIP, req: web::Json<StartOpenIDLoginQuery>) -> HttpResult {
let url = openid_service::start_login(&req.provider, ip.0).await?;
Ok(HttpResponse::Ok().json(StartOpenIDLoginResponse { url }))
}

View File

@ -8,13 +8,13 @@ pub async fn home() -> impl Responder {
}
#[derive(Debug, Clone, serde::Serialize)]
struct ServerConfig {
struct ServerConfig<'a> {
constraints: StaticConstraints,
mail: &'static str,
oidc_providers: Vec<OIDCProvider>,
oidc_providers: Vec<OIDCProvider<'a>>,
}
impl Default for ServerConfig {
impl Default for ServerConfig<'_> {
fn default() -> Self {
Self {
mail: AppConfig::get().mail_sender.as_str(),

View File

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

View File

@ -2,5 +2,6 @@
pub mod login_token_service;
pub mod mail_service;
pub mod openid_service;
pub mod rate_limiter_service;
pub mod users_service;

View File

@ -0,0 +1,85 @@
//! # OpenID service
use crate::app_config::{AppConfig, OIDCProvider};
use crate::connections::redis_connection;
use crate::constants::OPEN_ID_STATE_DURATION;
use crate::utils::string_utils;
use crate::utils::time_utils::time;
use light_openid::primitives::OpenIDConfig;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::net::IpAddr;
thread_local! {
static CONFIG_CACHES: RefCell<HashMap<String, OpenIDConfig>> = RefCell::new(Default::default());
}
struct OpenIDClient<'a> {
prov: OIDCProvider<'a>,
conf: OpenIDConfig,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct OpenIDState {
#[serde(rename = "i")]
ip: IpAddr,
#[serde(rename = "e")]
expire: u64,
#[serde(rename = "p")]
prov_id: String,
}
impl OpenIDState {
pub fn new(ip: IpAddr, client: &OpenIDClient) -> (String, Self) {
(
string_utils::rand_str(30),
Self {
ip,
expire: time() + OPEN_ID_STATE_DURATION.as_secs(),
prov_id: client.prov.id.to_string(),
},
)
}
}
fn redis_key(state: &str) -> String {
format!("oidc-state-{state}")
}
async fn load_provider_info(prov_id: &str) -> anyhow::Result<OpenIDClient> {
let prov = AppConfig::get()
.openid_providers()
.into_iter()
.find(|p| p.id.eq(prov_id))
.ok_or_else(|| std::io::Error::new(ErrorKind::Other, "Provider not found!"))?;
if let Some(conf) = CONFIG_CACHES.with(|i| i.borrow().get(prov_id).cloned()) {
return Ok(OpenIDClient { prov, conf });
}
let conf = OpenIDConfig::load_from_url(prov.configuration_url)
.await
.map_err(|e| std::io::Error::new(ErrorKind::Other, e.to_string()))?;
CONFIG_CACHES.with(|i| {
i.borrow_mut()
.insert(prov.configuration_url.to_string(), conf.clone())
});
Ok(OpenIDClient { prov, conf })
}
/// Get the URL where a user should be redirected for login
pub async fn start_login(prov_id: &str, ip: IpAddr) -> anyhow::Result<String> {
let prov = load_provider_info(prov_id).await?;
let (state_key, state) = OpenIDState::new(ip, &prov);
redis_connection::set_value(&redis_key(&state_key), &state, OPEN_ID_STATE_DURATION).await?;
Ok(prov.conf.gen_authorization_url(
prov.prov.client_id,
&state_key,
&AppConfig::get().oidc_redirect_url,
))
}