9.7 KiB
30 - OIDC Integration
OpenID Connect (OIDC) is an identity layer on top of OAuth 2.0. It lets clients verify user identity based on authentication performed by an authorization server and obtain basic profile information. Adding OIDC enables SSO with providers like Authentik, Authelia, and KeyCloak.
What it gives us:
- Users authenticate with their existing IdP credentials
- No need for separate username/password management
- Modern auth best practices baked in
- Seamless experience for users already on SSO
OIDC Configuration
OIDC is configured separately from AuthConfig via OidcConfig in src/common/config.rs. This is a single-provider model -- one OIDC provider per instance:
/// OpenID Connect (OIDC) configuration
pub struct OidcConfig {
pub enabled: bool, // Whether OIDC is enabled
pub issuer_url: String, // OIDC Issuer URL
pub client_id: String, // OIDC Client ID
pub client_secret: String, // OIDC Client Secret
pub redirect_uri: String, // Redirect URI (default: http://localhost:8086/api/auth/oidc/callback)
pub scopes: String, // Scopes to request (default: "openid profile email")
pub frontend_url: String, // Frontend URL for post-login redirect
pub auto_provision: bool, // Auto-create users on first login (JIT provisioning)
pub admin_groups: String, // Comma-separated OIDC groups that map to admin role
pub disable_password_login: bool, // Disable password-based login entirely
pub provider_name: String, // Display name (default: "SSO")
}
Environment variables use the OXICLOUD_OIDC_* prefix:
OXICLOUD_OIDC_ENABLED=true
OXICLOUD_OIDC_ISSUER_URL="https://authentik.example.com/application/o/oxicloud/"
OXICLOUD_OIDC_CLIENT_ID="your-client-id"
OXICLOUD_OIDC_CLIENT_SECRET="your-client-secret"
OXICLOUD_OIDC_REDIRECT_URI="https://oxicloud.example.com/api/auth/oidc/callback"
OXICLOUD_OIDC_SCOPES="openid profile email"
OXICLOUD_OIDC_FRONTEND_URL="https://oxicloud.example.com"
OXICLOUD_OIDC_AUTO_PROVISION=true
OXICLOUD_OIDC_ADMIN_GROUPS="oxicloud-admins"
OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=false
OXICLOUD_OIDC_PROVIDER_NAME="Authentik"
OIDC Service Implementation
The OIDC service lives in the infrastructure layer at src/infrastructure/services/oidc_service.rs and implements the OidcServicePort trait defined in src/application/ports/auth_ports.rs:
// src/application/ports/auth_ports.rs — Port trait
#[async_trait]
pub trait OidcServicePort: Send + Sync + 'static {
fn enabled(&self) -> bool;
fn provider_name(&self) -> &str;
fn generate_auth_url(&self, state: &str) -> Result<String, DomainError>;
async fn exchange_code(&self, code: &str) -> Result<OidcTokenSet, DomainError>;
async fn get_user_info(&self, token_set: &OidcTokenSet) -> Result<OidcIdClaims, DomainError>;
}
// src/infrastructure/services/oidc_service.rs — Implementation
pub struct OidcService {
config: OidcConfig,
http_client: reqwest::Client,
// Discovery metadata cached after initialization
}
impl OidcService {
pub async fn new(config: OidcConfig) -> Result<Self, DomainError> {
// Discovers OIDC endpoints from issuer_url
// ...
}
}
Follows hexagonal architecture: the port (OidcServicePort) is in the application layer, and the implementation (OidcService) is in the infrastructure layer.
User Entity OIDC Support
The User entity in src/domain/entities/user.rs supports OIDC users via two fields:
#[derive(Debug, Clone)]
pub struct User {
// ... standard fields ...
oidc_provider: Option<String>, // OIDC provider name (e.g., "authentik")
oidc_subject: Option<String>, // OIDC subject identifier (unique ID from provider)
}
impl User {
pub fn oidc_provider(&self) -> Option<&str> {
self.oidc_provider.as_deref()
}
pub fn oidc_subject(&self) -> Option<&str> {
self.oidc_subject.as_deref()
}
// Constructor for OIDC users
pub fn new_oidc(username, email, role, quota, oidc_provider, oidc_subject) -> Self;
}
Database Schema
The auth.users table includes OIDC columns:
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oidc_provider VARCHAR(255);
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oidc_subject VARCHAR(255);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_oidc ON auth.users(oidc_provider, oidc_subject) WHERE oidc_provider IS NOT NULL;
Users are matched by oidc_provider + oidc_subject combination. The UserRepository trait includes get_user_by_oidc_subject() for lookups.
Auth Application Service
AuthApplicationService in src/application/services/auth_application_service.rs coordinates OIDC authentication:
impl AuthApplicationService {
// Initialize with OIDC support
pub fn with_oidc(self, oidc_service: Arc<dyn OidcServicePort>, oidc_config: OidcConfig) -> Self;
// Reload OIDC configuration (for admin settings changes)
pub async fn reload_oidc(&self, config: OidcConfig) -> Result<(), DomainError>;
// Disable OIDC
pub fn disable_oidc(&self);
// Check if OIDC is enabled
pub fn oidc_enabled(&self) -> bool;
// Get OIDC config
pub fn oidc_config(&self) -> Option<OidcConfig>;
// Get OIDC service reference
pub fn oidc_service(&self) -> Option<Arc<dyn OidcServicePort>>;
// Check if password login is disabled
pub fn password_login_disabled(&self) -> bool;
// Prepare OIDC authorization URL
pub fn prepare_oidc_authorize(&self) -> Result<OidcAuthorizeResponseDto, DomainError>;
// Handle OIDC callback (exchange code for tokens)
pub async fn oidc_callback(&self, code: &str, state: &str) -> Result<AuthResponseDto, DomainError>;
}
Auth Handler Routes
OIDC endpoints in src/interfaces/api/handlers/auth_handler.rs:
// Public OIDC routes (no auth required) — nested under /api/auth/
.route("/status", get(get_system_status))
.route("/oidc/providers", get(oidc_providers))
.route("/oidc/authorize", get(oidc_authorize))
.route("/oidc/callback", get(oidc_callback))
.route("/oidc/exchange", post(oidc_exchange))
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/oidc/providers |
Returns OIDC provider info (name, enabled state) |
| GET | /api/auth/oidc/authorize |
Returns authorization URL for redirect to IdP |
| GET | /api/auth/oidc/callback |
Receives callback redirect from IdP with auth code |
| POST | /api/auth/oidc/exchange |
Exchanges auth code for JWT tokens |
DTOs
DTOs in src/application/dtos/user_dto.rs:
// Response with authorization URL
#[derive(Debug, Clone, Serialize)]
pub struct OidcAuthorizeResponseDto {
pub authorize_url: String,
pub state: String,
}
// Query params received from IdP callback
#[derive(Debug, Clone, Deserialize)]
pub struct OidcCallbackQueryDto {
pub code: String,
pub state: String,
}
// Request to exchange code for tokens
#[derive(Debug, Clone, Deserialize)]
pub struct OidcExchangeDto {
pub code: String,
pub state: String,
}
// Provider info response
#[derive(Debug, Clone, Serialize)]
pub struct OidcProviderInfoDto {
pub enabled: bool,
pub provider_name: String,
pub disable_password_login: bool,
}
// User info from OIDC claims
#[derive(Debug, Clone, Serialize)]
pub struct OidcUserInfoDto {
pub subject: String,
pub email: Option<String>,
pub name: Option<String>,
pub preferred_username: Option<String>,
pub groups: Vec<String>,
}
Frontend Integration
OIDC login is built directly into static/login.html and handled by static/js/auth.js. There is no separate oidcAuth.js file. The login page checks the system status endpoint to see if OIDC is enabled, then shows an SSO button accordingly.
<!-- In login.html - SSO login button shown when OIDC is enabled -->
<button id="oidc-login-btn" class="btn btn-oidc" style="display:none">
Login with SSO
</button>
Configuration Example
KeyCloak setup via docker-compose:
# docker-compose.yml
version: '3'
services:
oxicloud:
image: oxicloud:latest
environment:
OXICLOUD_OIDC_ENABLED: "true"
OXICLOUD_OIDC_ISSUER_URL: "https://keycloak.example.com/realms/your-realm"
OXICLOUD_OIDC_CLIENT_ID: "oxicloud"
OXICLOUD_OIDC_CLIENT_SECRET: "your-client-secret"
OXICLOUD_OIDC_REDIRECT_URI: "https://oxicloud.example.com/api/auth/oidc/callback"
OXICLOUD_OIDC_SCOPES: "openid profile email"
OXICLOUD_OIDC_FRONTEND_URL: "https://oxicloud.example.com"
OXICLOUD_OIDC_AUTO_PROVISION: "true"
OXICLOUD_OIDC_ADMIN_GROUPS: "oxicloud-admins"
OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN: "false"
OXICLOUD_OIDC_PROVIDER_NAME: "KeyCloak"
ports:
- "8086:8086"
volumes:
- ./storage:/app/storage
See oidc-config-examples.md for more provider-specific configurations.
Additional Notes
- Security -- always use HTTPS for OIDC connections. Ensure proper TLS configuration.
- User mapping -- OIDC users are identified by oidc_provider + oidc_subject in the auth.users table. Groups from OIDC can map to admin role via OXICLOUD_OIDC_ADMIN_GROUPS.
- Single provider -- one OIDC provider per instance. Managed via admin settings UI or environment variables.
- Session management -- after OIDC authentication, the backend generates its own JWT access/refresh tokens. Sessions work identically to password-based login from that point.
- Access control -- OIDC users share the same permissions model as local users. Admin role can be auto-assigned based on OIDC group membership.
- Testing -- use the admin settings UI (
/admin.html) to configure and test OIDC connections.