8.7 KiB
Authentication Architecture
This document describes the authentication stack for SimbaRAG: LLDAP → Authelia → OAuth2/OIDC.
Overview
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
│ LLDAP │────▶│ Authelia │────▶│ OAuth2/OIDC │────▶│ SimbaRAG │
│ (Users) │ │ (IdP) │ │ (Flow) │ │ (App) │
└─────────┘ └──────────┘ └──────────────┘ └──────────┘
| Component | Role |
|---|---|
| LLDAP | Lightweight LDAP server storing users and groups |
| Authelia | Identity provider that authenticates against LLDAP and issues OIDC tokens |
| SimbaRAG | Relying party that consumes OIDC tokens and manages sessions |
OIDC Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
OIDC_ISSUER |
Authelia server URL | Required |
OIDC_CLIENT_ID |
Client ID registered in Authelia | Required |
OIDC_CLIENT_SECRET |
Client secret for token exchange | Required |
OIDC_REDIRECT_URI |
Callback URL after authentication | Required |
OIDC_USE_DISCOVERY |
Enable automatic discovery | true |
JWT_SECRET_KEY |
Secret for signing backend JWTs | Required |
Discovery
When OIDC_USE_DISCOVERY=true, the application fetches endpoints from:
{OIDC_ISSUER}/.well-known/openid-configuration
This provides:
- Authorization endpoint
- Token endpoint
- JWKS URI for signature verification
- Supported scopes and claims
Authentication Flow
1. Login Initiation
GET /api/user/oidc/login
- Generate PKCE code verifier and challenge (S256)
- Generate CSRF state token
- Store state in session storage
- Return authorization URL for frontend redirect
2. Authorization
User is redirected to Authelia where they:
- Enter LDAP credentials
- Complete MFA if configured
- Consent to requested scopes
3. Callback
GET /api/user/oidc/callback?code=...&state=...
- Validate state matches stored value (CSRF protection)
- Exchange authorization code for tokens using PKCE verifier
- Verify ID token signature using JWKS
- Validate claims (issuer, audience, expiration)
- Create or update user in database
- Issue backend JWT tokens (access + refresh)
4. Token Refresh
POST /api/user/refresh
Authorization: Bearer <refresh_token>
Issues a new access token without re-authentication.
User Model
class User(Model):
id = UUIDField(primary_key=True)
username = CharField(max_length=255)
password = BinaryField(null=True) # Nullable for OIDC-only users
email = CharField(max_length=100, unique=True)
# OIDC fields
oidc_subject = CharField(max_length=255, unique=True, null=True)
auth_provider = CharField(max_length=50, default="local") # "local" or "oidc"
ldap_groups = JSONField(default=[]) # LDAP groups from OIDC claims
created_at = DatetimeField(auto_now_add=True)
updated_at = DatetimeField(auto_now=True)
def has_group(self, group: str) -> bool:
"""Check if user belongs to a specific LDAP group."""
return group in (self.ldap_groups or [])
def is_admin(self) -> bool:
"""Check if user is an admin (member of lldap_admin group)."""
return self.has_group("lldap_admin")
User Provisioning
The OIDCUserService handles automatic user creation:
- Extract claims from ID token (
sub,email,preferred_username) - Check if user exists by
oidc_subject - If not, check by email for migration from local auth
- Create new user or update existing
JWT Tokens
Backend issues its own JWTs after OIDC authentication:
| Token Type | Purpose | Typical Lifetime |
|---|---|---|
| Access Token | API authorization | 15 minutes |
| Refresh Token | Obtain new access tokens | 7 days |
Claims
{
"identity": "<user-uuid>",
"type": "access|refresh",
"exp": 1234567890,
"iat": 1234567890
}
Protected Endpoints
All API endpoints use the @jwt_refresh_token_required decorator for basic authentication:
@blueprint.route("/example")
@jwt_refresh_token_required
async def protected_endpoint():
user_id = get_jwt_identity()
# ...
Role-Based Access Control (RBAC)
RBAC is implemented using LDAP groups passed through Authelia as OIDC claims. Users in the lldap_admin group have admin privileges.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ LLDAP │
│ Groups: lldap_admin, lldap_user │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Authelia │
│ Scope: groups → Claim: groups = ["lldap_admin"] │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SimbaRAG │
│ 1. Extract groups from ID token │
│ 2. Store in User.ldap_groups │
│ 3. Check membership with @admin_required decorator │
└─────────────────────────────────────────────────────────────┘
Authelia Configuration
Ensure Authelia is configured to pass the groups claim:
identity_providers:
oidc:
clients:
- client_id: simbarag
scopes:
- openid
- profile
- email
- groups # Required for RBAC
Admin-Only Endpoints
The @admin_required decorator protects privileged endpoints:
from blueprints.users.decorators import admin_required
@blueprint.post("/admin-action")
@admin_required
async def admin_only_endpoint():
# Only users in lldap_admin group can access
...
Protected endpoints:
| Endpoint | Access | Description |
|---|---|---|
POST /api/rag/index |
Admin | Trigger document indexing |
POST /api/rag/reindex |
Admin | Clear and reindex all documents |
GET /api/rag/stats |
All users | View vector store statistics |
User Response
The OIDC callback returns group information:
{
"access_token": "...",
"refresh_token": "...",
"user": {
"id": "uuid",
"username": "john",
"email": "john@example.com",
"groups": ["lldap_admin", "lldap_user"],
"is_admin": true
}
}
Security Considerations
Current Gaps
| Issue | Risk | Mitigation |
|---|---|---|
| In-memory session storage | State lost on restart, not scalable | Use Redis for production |
| No token revocation | Tokens valid until expiry | Implement blacklist or short expiry |
| No audit logging | Cannot track auth events | Add event logging |
| Single JWT secret | Compromise affects all tokens | Rotate secrets, use asymmetric keys |
Recommendations
- Use Redis for OIDC state storage in production
- Implement logout with token blacklisting
- Add audit logging for authentication events
- Rotate JWT secrets regularly
- Use short-lived access tokens (15 min) with refresh
File Reference
| File | Purpose |
|---|---|
services/raggr/oidc_config.py |
OIDC client configuration and discovery |
services/raggr/blueprints/users/models.py |
User model definition with group helpers |
services/raggr/blueprints/users/oidc_service.py |
User provisioning from OIDC claims |
services/raggr/blueprints/users/__init__.py |
Auth endpoints and flow |
services/raggr/blueprints/users/decorators.py |
Auth decorators (@admin_required) |