feat: Implement OIDC authentication with JWT token handling and dynamic auth configuration
This commit is contained in:
@@ -5,7 +5,7 @@ edition = "2024"
|
||||
default-run = "notes-api"
|
||||
|
||||
[features]
|
||||
default = ["sqlite", "smart-features", "auth-oidc", "auth-jwt"]
|
||||
default = ["sqlite", "smart-features"]
|
||||
sqlite = ["notes-infra/sqlite"]
|
||||
postgres = ["notes-infra/postgres"]
|
||||
smart-features = ["notes-infra/smart-features", "notes-infra/broker-nats"]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#[cfg(feature = "smart-features")]
|
||||
use notes_infra::factory::{EmbeddingProvider, VectorProvider};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
|
||||
/// Authentication mode - determines how the API authenticates requests
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthMode {
|
||||
/// Session-based authentication using cookies (default for backward compatibility)
|
||||
@@ -66,6 +66,9 @@ pub struct Config {
|
||||
|
||||
/// Whether the application is running in production mode
|
||||
pub is_production: bool,
|
||||
|
||||
/// Frontend URL for OIDC redirect (defaults to first CORS origin)
|
||||
pub frontend_url: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -100,6 +103,7 @@ impl Default for Config {
|
||||
jwt_audience: None,
|
||||
jwt_expiry_hours: 24,
|
||||
is_production: false,
|
||||
frontend_url: "http://localhost:5173".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,6 +223,8 @@ impl Config {
|
||||
jwt_audience,
|
||||
jwt_expiry_hours,
|
||||
is_production,
|
||||
frontend_url: env::var("FRONTEND_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:5173".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use validator::Validate;
|
||||
|
||||
use notes_domain::{Email, Note, Password, Tag};
|
||||
|
||||
use crate::config::AuthMode;
|
||||
|
||||
/// Request to create a new note
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct CreateNoteRequest {
|
||||
@@ -165,6 +167,9 @@ impl From<notes_domain::NoteVersion> for NoteVersionResponse {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigResponse {
|
||||
pub allow_registration: bool,
|
||||
pub auth_mode: AuthMode,
|
||||
pub oidc_enabled: bool,
|
||||
pub password_login_enabled: bool,
|
||||
}
|
||||
|
||||
/// Note Link response DTO
|
||||
|
||||
@@ -387,25 +387,19 @@ async fn oidc_callback(
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
|
||||
// In JWT mode, return token as JSON
|
||||
// In JWT mode, redirect to frontend with token in URL fragment
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
if matches!(auth_mode, AuthMode::Jwt | AuthMode::Both) {
|
||||
let token = create_jwt_for_user(&user, &state)?;
|
||||
return Ok(Json(TokenResponse {
|
||||
access_token: token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: state.config.jwt_expiry_hours * 3600,
|
||||
})
|
||||
.into_response());
|
||||
let redirect_url = format!(
|
||||
"{}/auth/callback#access_token={}",
|
||||
state.config.frontend_url, token
|
||||
);
|
||||
return Ok(axum::response::Redirect::to(&redirect_url).into_response());
|
||||
}
|
||||
|
||||
// Session mode: return user info
|
||||
Ok(Json(UserResponse {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: user.created_at,
|
||||
})
|
||||
.into_response())
|
||||
// Session mode: redirect to frontend (session cookie already set)
|
||||
Ok(axum::response::Redirect::to(&state.config.frontend_url).into_response())
|
||||
}
|
||||
|
||||
/// Fallback OIDC callback when auth-axum-login is not enabled
|
||||
@@ -470,15 +464,15 @@ async fn oidc_callback(
|
||||
.await
|
||||
.map_err(|_| ApiError::Internal("Session error".into()))?;
|
||||
|
||||
// Return token as JSON
|
||||
// Redirect to frontend with token in URL fragment
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
{
|
||||
let token = create_jwt_for_user(&user, &state)?;
|
||||
return Ok(Json(TokenResponse {
|
||||
access_token: token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: state.config.jwt_expiry_hours * 3600,
|
||||
}));
|
||||
let redirect_url = format!(
|
||||
"{}/auth/callback#access_token={}",
|
||||
state.config.frontend_url, token
|
||||
);
|
||||
return Ok(axum::response::Redirect::to(&redirect_url));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "auth-jwt"))]
|
||||
|
||||
@@ -10,5 +10,11 @@ use crate::state::AppState;
|
||||
pub async fn get_config(State(state): State<AppState>) -> ApiResult<Json<ConfigResponse>> {
|
||||
Ok(Json(ConfigResponse {
|
||||
allow_registration: state.config.allow_registration,
|
||||
auth_mode: state.config.auth_mode,
|
||||
#[cfg(feature = "auth-oidc")]
|
||||
oidc_enabled: state.oidc_service.is_some(),
|
||||
#[cfg(not(feature = "auth-oidc"))]
|
||||
oidc_enabled: false,
|
||||
password_login_enabled: cfg!(feature = "auth-axum-login"),
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user