feat: Add OpenID Connect (OIDC) authentication support with new OIDC service, routes, and configuration.
This commit is contained in:
@@ -5,10 +5,11 @@ edition = "2024"
|
||||
default-run = "api"
|
||||
|
||||
[features]
|
||||
default = ["sqlite", "auth-axum-login"]
|
||||
default = ["sqlite", "auth-axum-login", "auth-oidc"]
|
||||
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
|
||||
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
|
||||
auth-axum-login = ["infra/auth-axum-login"]
|
||||
auth-oidc = ["infra/auth-oidc"]
|
||||
|
||||
[dependencies]
|
||||
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
|
||||
@@ -58,3 +59,4 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||
|
||||
dotenvy = "0.15.7"
|
||||
config = "0.15.19"
|
||||
tower-sessions = "0.14.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::env;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
//todo: replace with newtypes
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
pub database_url: String,
|
||||
@@ -26,6 +27,11 @@ pub struct Config {
|
||||
|
||||
#[serde(default = "default_db_min_connections")]
|
||||
pub db_min_connections: u32,
|
||||
|
||||
pub oidc_issuer: Option<String>,
|
||||
pub oidc_client_id: Option<String>,
|
||||
pub oidc_client_secret: Option<String>,
|
||||
pub oidc_redirect_url: Option<String>,
|
||||
}
|
||||
|
||||
fn default_secure_cookie() -> bool {
|
||||
@@ -98,6 +104,11 @@ impl Config {
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
let oidc_issuer = env::var("OIDC_ISSUER").ok();
|
||||
let oidc_client_id = env::var("OIDC_CLIENT_ID").ok();
|
||||
let oidc_client_secret = env::var("OIDC_CLIENT_SECRET").ok();
|
||||
let oidc_redirect_url = env::var("OIDC_REDIRECT_URL").ok();
|
||||
|
||||
Self {
|
||||
host,
|
||||
port,
|
||||
@@ -107,6 +118,10 @@ impl Config {
|
||||
secure_cookie,
|
||||
db_max_connections,
|
||||
db_min_connections,
|
||||
oidc_issuer,
|
||||
oidc_client_id,
|
||||
oidc_client_secret,
|
||||
oidc_redirect_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let user_repo = build_user_repository(&db_pool).await?;
|
||||
let user_service = UserService::new(user_repo.clone());
|
||||
|
||||
let state = AppState::new(user_service, config.clone());
|
||||
let state = AppState::new(user_service, config.clone()).await?;
|
||||
|
||||
let session_store = build_session_store(&db_pool)
|
||||
.await
|
||||
|
||||
@@ -12,13 +12,21 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
use domain::{DomainError, Email};
|
||||
use tower_sessions::Session;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
let r = Router::new()
|
||||
.route("/login", post(login))
|
||||
.route("/register", post(register))
|
||||
.route("/logout", post(logout))
|
||||
.route("/me", post(me))
|
||||
.route("/me", post(me));
|
||||
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
let r = r
|
||||
.route("/login/oidc", axum::routing::get(oidc_login))
|
||||
.route("/auth/callback", axum::routing::get(oidc_callback));
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
async fn login(
|
||||
@@ -115,3 +123,104 @@ async fn me(auth_session: crate::auth::AuthSession) -> Result<impl IntoResponse,
|
||||
created_at: user.0.created_at,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
async fn oidc_login(
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let service = state
|
||||
.oidc_service
|
||||
.as_ref()
|
||||
.ok_or(ApiError::Internal("OIDC not configured".into()))?;
|
||||
|
||||
let (url, csrf, nonce, pkce) = service.get_authorization_url();
|
||||
|
||||
session
|
||||
.insert("oidc_csrf", csrf)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
session
|
||||
.insert("oidc_nonce", nonce)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
session
|
||||
.insert("oidc_pkce", pkce)
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
|
||||
Ok(axum::response::Redirect::to(&url))
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CallbackParams {
|
||||
code: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
async fn oidc_callback(
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
mut auth_session: crate::auth::AuthSession,
|
||||
axum::extract::Query(params): axum::extract::Query<CallbackParams>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let service = state
|
||||
.oidc_service
|
||||
.as_ref()
|
||||
.ok_or(ApiError::Internal("OIDC not configured".into()))?;
|
||||
|
||||
let stored_csrf: String = session
|
||||
.get("oidc_csrf")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?
|
||||
.ok_or(ApiError::Validation("Missing CSRF token".into()))?;
|
||||
|
||||
if params.state != stored_csrf {
|
||||
return Err(ApiError::Validation("Invalid CSRF token".into()));
|
||||
}
|
||||
|
||||
// 2. Retrieve secrets
|
||||
let stored_pkce: String = session
|
||||
.get("oidc_pkce")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?
|
||||
.ok_or(ApiError::Validation("Missing PKCE".into()))?;
|
||||
let stored_nonce: String = session
|
||||
.get("oidc_nonce")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?
|
||||
.ok_or(ApiError::Validation("Missing Nonce".into()))?;
|
||||
|
||||
let oidc_user = service
|
||||
.resolve_callback(params.code, stored_nonce, stored_pkce)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
|
||||
let user = state
|
||||
.user_service
|
||||
.find_or_create(&oidc_user.subject, &oidc_user.email)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
|
||||
auth_session
|
||||
.login(&crate::auth::AuthUser(user))
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Login failed".into()))?;
|
||||
|
||||
let _: Option<String> = session
|
||||
.remove("oidc_csrf")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
let _: Option<String> = session
|
||||
.remove("oidc_pkce")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
let _: Option<String> = session
|
||||
.remove("oidc_nonce")
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
|
||||
Ok(axum::response::Redirect::to("/"))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
//! Holds shared state for the application.
|
||||
|
||||
use axum::extract::FromRef;
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
use infra::auth::oidc::OidcService;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -11,15 +13,36 @@ use domain::UserService;
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub user_service: Arc<UserService>,
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
pub oidc_service: Option<Arc<OidcService>>,
|
||||
|
||||
pub config: Arc<Config>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(user_service: UserService, config: Config) -> Self {
|
||||
Self {
|
||||
pub async fn new(user_service: UserService, config: Config) -> anyhow::Result<Self> {
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
let oidc_service = if let (Some(issuer), Some(id), Some(secret), Some(redirect)) = (
|
||||
&config.oidc_issuer,
|
||||
&config.oidc_client_id,
|
||||
&config.oidc_client_secret,
|
||||
&config.oidc_redirect_url,
|
||||
) {
|
||||
tracing::info!("Initializing OIDC service with issuer: {}", issuer);
|
||||
Some(Arc::new(
|
||||
OidcService::new(issuer.clone(), id.clone(), secret.clone(), redirect.clone())
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
user_service: Arc::new(user_service),
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
oidc_service,
|
||||
config: Arc::new(config),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user