feat(auth): add OpenID Connect (OIDC) authentication support
Implements OIDC Authorization Code Flow for external identity providers (Authentik, Keycloak, etc.) with JIT user provisioning. New features: - OidcService with OpenID Discovery, JWKS caching, RS256 ID token validation - Authorization Code Flow: /api/auth/oidc/authorize -> IdP -> /api/auth/oidc/callback - JIT user provisioning from OIDC claims (sub, email, name, groups) - OIDC group-to-role mapping (admin_groups config) - Provider info endpoint: GET /api/auth/oidc/providers - Option to disable password login entirely (OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN) - Auto-provision toggle (OXICLOUD_OIDC_AUTO_PROVISION) - Email collision detection (security: prevents account takeover) Configuration (env vars): - OXICLOUD_OIDC_ENABLED, OXICLOUD_OIDC_ISSUER_URL - OXICLOUD_OIDC_CLIENT_ID, OXICLOUD_OIDC_CLIENT_SECRET - OXICLOUD_OIDC_REDIRECT_URI, OXICLOUD_OIDC_SCOPES - OXICLOUD_OIDC_FRONTEND_URL, OXICLOUD_OIDC_PROVIDER_NAME - OXICLOUD_OIDC_AUTO_PROVISION, OXICLOUD_OIDC_ADMIN_GROUPS - OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN DB migration: - ALTER TABLE auth.users ADD oidc_provider, oidc_subject columns - UNIQUE index on (oidc_provider, oidc_subject) Files changed: 14 files, ~1400 lines added Dependencies: reqwest 0.12 (rustls-tls-webpki-roots), base64 0.22
This commit is contained in:
@@ -33,6 +33,8 @@ pub struct User {
|
||||
updated_at: DateTime<Utc>,
|
||||
last_login_at: Option<DateTime<Utc>>,
|
||||
active: bool,
|
||||
oidc_provider: Option<String>,
|
||||
oidc_subject: Option<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
@@ -88,6 +90,45 @@ impl User {
|
||||
updated_at: now,
|
||||
last_login_at: None,
|
||||
active: true,
|
||||
oidc_provider: None,
|
||||
oidc_subject: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new OIDC-authenticated user (no password required).
|
||||
pub fn new_oidc(
|
||||
username: String,
|
||||
email: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
oidc_provider: String,
|
||||
oidc_subject: String,
|
||||
) -> UserResult<Self> {
|
||||
if username.is_empty() || username.len() < 3 || username.len() > 32 {
|
||||
return Err(UserError::InvalidUsername(
|
||||
"Username debe tener entre 3 y 32 caracteres".to_string(),
|
||||
));
|
||||
}
|
||||
if !email.contains('@') || email.len() < 5 {
|
||||
return Err(UserError::ValidationError(
|
||||
"Email inválido".to_string(),
|
||||
));
|
||||
}
|
||||
let now = Utc::now();
|
||||
Ok(Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
username,
|
||||
email,
|
||||
password_hash: "__OIDC_NO_PASSWORD__".to_string(),
|
||||
role,
|
||||
storage_quota_bytes,
|
||||
storage_used_bytes: 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_login_at: None,
|
||||
active: true,
|
||||
oidc_provider: Some(oidc_provider),
|
||||
oidc_subject: Some(oidc_subject),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -117,6 +158,41 @@ impl User {
|
||||
updated_at,
|
||||
last_login_at,
|
||||
active,
|
||||
oidc_provider: None,
|
||||
oidc_subject: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct from DB with OIDC fields
|
||||
pub fn from_data_full(
|
||||
id: String,
|
||||
username: String,
|
||||
email: String,
|
||||
password_hash: String,
|
||||
role: UserRole,
|
||||
storage_quota_bytes: i64,
|
||||
storage_used_bytes: i64,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
last_login_at: Option<DateTime<Utc>>,
|
||||
active: bool,
|
||||
oidc_provider: Option<String>,
|
||||
oidc_subject: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
password_hash,
|
||||
role,
|
||||
storage_quota_bytes,
|
||||
storage_used_bytes,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_login_at,
|
||||
active,
|
||||
oidc_provider,
|
||||
oidc_subject,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +240,19 @@ impl User {
|
||||
pub fn password_hash(&self) -> &str {
|
||||
&self.password_hash
|
||||
}
|
||||
|
||||
pub fn oidc_provider(&self) -> Option<&str> {
|
||||
self.oidc_provider.as_deref()
|
||||
}
|
||||
|
||||
pub fn oidc_subject(&self) -> Option<&str> {
|
||||
self.oidc_subject.as_deref()
|
||||
}
|
||||
|
||||
/// Returns true if this is an OIDC-only user (no password)
|
||||
pub fn is_oidc_user(&self) -> bool {
|
||||
self.oidc_provider.is_some()
|
||||
}
|
||||
|
||||
/// Update the password hash.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user