fix(oidc): add CSRF state validation, PKCE S256, nonce, secure token delivery, registration guard

Security fixes for OIDC authentication flow:

1. CSRF state validation (High): State nonce is now stored server-side
   and validated on callback (single-use, 600s TTL)

2. PKCE S256 (Medium): code_challenge/code_verifier pair generated per
   RFC 9126, sent in authorize URL and token exchange

3. Nonce in ID token (Medium): Random nonce included in authorize URL,
   verified against ID token claims to prevent token replay

4. Secure token delivery (Medium): Tokens no longer in URL fragments.
   One-time exchange code redirected to frontend, tokens retrieved via
   POST /api/auth/oidc/exchange endpoint (60s TTL, single-use)

5. Registration guard (Low): POST /api/auth/register returns 403 when
   disable_password_login is active in OIDC-only mode
This commit is contained in:
Dionisio
2026-02-11 00:37:47 +01:00
parent f60c0df9f9
commit 1a1dee9179
5 changed files with 234 additions and 48 deletions
+8 -6
View File
@@ -124,14 +124,16 @@ pub struct OidcIdClaims {
/// Port for OIDC operations — implemented in infrastructure layer
#[async_trait]
pub trait OidcServicePort: Send + Sync + 'static {
/// Get the authorization URL for redirecting the user to the IdP
fn get_authorize_url(&self, state: &str) -> Result<String, DomainError>;
/// Get the authorization URL for redirecting the user to the IdP.
/// Includes PKCE code_challenge (S256) and nonce for ID token binding.
fn get_authorize_url(&self, state: &str, nonce: &str, pkce_challenge: &str) -> Result<String, DomainError>;
/// Exchange an authorization code for tokens
async fn exchange_code(&self, code: &str) -> Result<OidcTokenSet, DomainError>;
/// Exchange an authorization code for tokens, providing PKCE code_verifier.
async fn exchange_code(&self, code: &str, pkce_verifier: &str) -> Result<OidcTokenSet, DomainError>;
/// Validate an ID token and extract claims
async fn validate_id_token(&self, id_token: &str) -> Result<OidcIdClaims, DomainError>;
/// Validate an ID token and extract claims.
/// If `expected_nonce` is provided, verifies the `nonce` claim matches.
async fn validate_id_token(&self, id_token: &str, expected_nonce: Option<&str>) -> Result<OidcIdClaims, DomainError>;
/// Fetch user info from the UserInfo endpoint (fallback for missing ID token claims)
async fn fetch_user_info(&self, access_token: &str) -> Result<OidcIdClaims, DomainError>;