Merge pull request #267 from readur/oidc-improvements
WIP: OIDC improvements
This commit is contained in:
commit
e81c60192b
|
|
@ -214,7 +214,7 @@ OIDC_ENABLED: true
|
||||||
OIDC_ISSUER_URL: https://keycloak.example.com/auth/realms/readur
|
OIDC_ISSUER_URL: https://keycloak.example.com/auth/realms/readur
|
||||||
OIDC_CLIENT_ID: readur-client
|
OIDC_CLIENT_ID: readur-client
|
||||||
OIDC_CLIENT_SECRET: your-client-secret
|
OIDC_CLIENT_SECRET: your-client-secret
|
||||||
OIDC_REDIRECT_URI: https://readur.example.com/auth/oidc/callback
|
OIDC_REDIRECT_URI: https://readur.example.com/api/auth/oidc/callback
|
||||||
OIDC_SCOPES: openid profile email
|
OIDC_SCOPES: openid profile email
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ Keycloak client configuration:
|
||||||
"frontchannelLogout": true,
|
"frontchannelLogout": true,
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"redirectUris": [
|
"redirectUris": [
|
||||||
"https://readur.example.com/auth/oidc/callback"
|
"https://readur.example.com/api/auth/oidc/callback"
|
||||||
],
|
],
|
||||||
"webOrigins": [
|
"webOrigins": [
|
||||||
"https://readur.example.com"
|
"https://readur.example.com"
|
||||||
|
|
@ -246,7 +246,7 @@ OIDC_ENABLED: true
|
||||||
OIDC_ISSUER_URL: https://your-tenant.auth0.com/
|
OIDC_ISSUER_URL: https://your-tenant.auth0.com/
|
||||||
OIDC_CLIENT_ID: your-client-id
|
OIDC_CLIENT_ID: your-client-id
|
||||||
OIDC_CLIENT_SECRET: your-client-secret
|
OIDC_CLIENT_SECRET: your-client-secret
|
||||||
OIDC_REDIRECT_URI: https://readur.example.com/auth/oidc/callback
|
OIDC_REDIRECT_URI: https://readur.example.com/api/auth/oidc/callback
|
||||||
OIDC_SCOPES: openid profile email
|
OIDC_SCOPES: openid profile email
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -258,7 +258,7 @@ OIDC_ENABLED: true
|
||||||
OIDC_ISSUER_URL: https://your-org.okta.com/oauth2/default
|
OIDC_ISSUER_URL: https://your-org.okta.com/oauth2/default
|
||||||
OIDC_CLIENT_ID: your-client-id
|
OIDC_CLIENT_ID: your-client-id
|
||||||
OIDC_CLIENT_SECRET: your-client-secret
|
OIDC_CLIENT_SECRET: your-client-secret
|
||||||
OIDC_REDIRECT_URI: https://readur.example.com/auth/oidc/callback
|
OIDC_REDIRECT_URI: https://readur.example.com/api/auth/oidc/callback
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Azure AD
|
#### Azure AD
|
||||||
|
|
@ -269,7 +269,7 @@ OIDC_ENABLED: true
|
||||||
OIDC_ISSUER_URL: https://login.microsoftonline.com/{tenant-id}/v2.0
|
OIDC_ISSUER_URL: https://login.microsoftonline.com/{tenant-id}/v2.0
|
||||||
OIDC_CLIENT_ID: your-application-id
|
OIDC_CLIENT_ID: your-application-id
|
||||||
OIDC_CLIENT_SECRET: your-client-secret
|
OIDC_CLIENT_SECRET: your-client-secret
|
||||||
OIDC_REDIRECT_URI: https://readur.example.com/auth/oidc/callback
|
OIDC_REDIRECT_URI: https://readur.example.com/api/auth/oidc/callback
|
||||||
OIDC_SCOPES: openid profile email User.Read
|
OIDC_SCOPES: openid profile email User.Read
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -281,7 +281,7 @@ OIDC_ENABLED: true
|
||||||
OIDC_ISSUER_URL: https://accounts.google.com
|
OIDC_ISSUER_URL: https://accounts.google.com
|
||||||
OIDC_CLIENT_ID: your-client-id.apps.googleusercontent.com
|
OIDC_CLIENT_ID: your-client-id.apps.googleusercontent.com
|
||||||
OIDC_CLIENT_SECRET: your-client-secret
|
OIDC_CLIENT_SECRET: your-client-secret
|
||||||
OIDC_REDIRECT_URI: https://readur.example.com/auth/oidc/callback
|
OIDC_REDIRECT_URI: https://readur.example.com/api/auth/oidc/callback
|
||||||
OIDC_SCOPES: openid profile email
|
OIDC_SCOPES: openid profile email
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ This guide explains how to configure OpenID Connect (OIDC) authentication for Re
|
||||||
- [Microsoft Azure AD](#microsoft-azure-ad)
|
- [Microsoft Azure AD](#microsoft-azure-ad)
|
||||||
- [Keycloak](#keycloak)
|
- [Keycloak](#keycloak)
|
||||||
- [Auth0](#auth0)
|
- [Auth0](#auth0)
|
||||||
|
- [Authentik](#authentik)
|
||||||
- [Generic OIDC Provider](#generic-oidc-provider)
|
- [Generic OIDC Provider](#generic-oidc-provider)
|
||||||
- [Testing the Setup](#testing-the-setup)
|
- [Testing the Setup](#testing-the-setup)
|
||||||
- [User Experience](#user-experience)
|
- [User Experience](#user-experience)
|
||||||
|
|
@ -25,6 +26,8 @@ This guide explains how to configure OpenID Connect (OIDC) authentication for Re
|
||||||
OIDC authentication in Readur provides:
|
OIDC authentication in Readur provides:
|
||||||
|
|
||||||
- **Single Sign-On (SSO)**: Users can sign in with existing corporate accounts
|
- **Single Sign-On (SSO)**: Users can sign in with existing corporate accounts
|
||||||
|
- **Email-Based User Syncing**: Automatically link existing local users to OIDC by email
|
||||||
|
- **Flexible Auto-Registration**: Control whether new users can self-register via OIDC
|
||||||
- **Centralized User Management**: User provisioning handled by your identity provider
|
- **Centralized User Management**: User provisioning handled by your identity provider
|
||||||
- **Enhanced Security**: No need to manage passwords in Readur
|
- **Enhanced Security**: No need to manage passwords in Readur
|
||||||
- **Seamless Integration**: Works alongside existing local authentication
|
- **Seamless Integration**: Works alongside existing local authentication
|
||||||
|
|
@ -52,7 +55,9 @@ Configure OIDC by setting these environment variables:
|
||||||
| `OIDC_CLIENT_ID` | ✅ | OAuth2 client ID from your provider | `readur-app-client-id` |
|
| `OIDC_CLIENT_ID` | ✅ | OAuth2 client ID from your provider | `readur-app-client-id` |
|
||||||
| `OIDC_CLIENT_SECRET` | ✅ | OAuth2 client secret from your provider | `very-secret-key` |
|
| `OIDC_CLIENT_SECRET` | ✅ | OAuth2 client secret from your provider | `very-secret-key` |
|
||||||
| `OIDC_ISSUER_URL` | ✅ | OIDC provider's issuer URL | `https://accounts.google.com` |
|
| `OIDC_ISSUER_URL` | ✅ | OIDC provider's issuer URL | `https://accounts.google.com` |
|
||||||
| `OIDC_REDIRECT_URI` | ✅ | Callback URL for your Readur instance | `https://readur.company.com/auth/oidc/callback` |
|
| `OIDC_REDIRECT_URI` | ✅ | Callback URL for your Readur instance | `https://readur.company.com/api/auth/oidc/callback` |
|
||||||
|
| `OIDC_AUTO_REGISTER` | ❌ | Allow new users to self-register (default: `true`) | `true` or `false` |
|
||||||
|
| `ALLOW_LOCAL_AUTH` | ❌ | Allow username/password authentication (default: `true`) | `true` or `false` |
|
||||||
|
|
||||||
### Example Configurations
|
### Example Configurations
|
||||||
|
|
||||||
|
|
@ -66,7 +71,9 @@ OIDC_ENABLED=true
|
||||||
OIDC_CLIENT_ID=123456789-abcdefgh.apps.googleusercontent.com
|
OIDC_CLIENT_ID=123456789-abcdefgh.apps.googleusercontent.com
|
||||||
OIDC_CLIENT_SECRET=GOCSPX-your-secret-key
|
OIDC_CLIENT_SECRET=GOCSPX-your-secret-key
|
||||||
OIDC_ISSUER_URL=https://accounts.google.com
|
OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
OIDC_REDIRECT_URI=https://readur.company.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://readur.company.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true # Allow new users to register via OIDC
|
||||||
|
ALLOW_LOCAL_AUTH=true # Set to false to disable username/password login
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development Setup
|
#### Development Setup
|
||||||
|
|
@ -79,7 +86,9 @@ OIDC_ENABLED=true
|
||||||
OIDC_CLIENT_ID=dev-client-id
|
OIDC_CLIENT_ID=dev-client-id
|
||||||
OIDC_CLIENT_SECRET=dev-client-secret
|
OIDC_CLIENT_SECRET=dev-client-secret
|
||||||
OIDC_ISSUER_URL=https://your-keycloak.company.com/auth/realms/readur
|
OIDC_ISSUER_URL=https://your-keycloak.company.com/auth/realms/readur
|
||||||
OIDC_REDIRECT_URI=http://localhost:8000/auth/oidc/callback
|
OIDC_REDIRECT_URI=http://localhost:8000/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=false # Only allow existing users to login
|
||||||
|
ALLOW_LOCAL_AUTH=true # Keep local auth for development
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Compose Setup
|
#### Docker Compose Setup
|
||||||
|
|
@ -98,7 +107,8 @@ services:
|
||||||
OIDC_CLIENT_ID: "${OIDC_CLIENT_ID}"
|
OIDC_CLIENT_ID: "${OIDC_CLIENT_ID}"
|
||||||
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}"
|
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET}"
|
||||||
OIDC_ISSUER_URL: "${OIDC_ISSUER_URL}"
|
OIDC_ISSUER_URL: "${OIDC_ISSUER_URL}"
|
||||||
OIDC_REDIRECT_URI: "https://readur.company.com/auth/oidc/callback"
|
OIDC_REDIRECT_URI: "https://readur.company.com/api/auth/oidc/callback"
|
||||||
|
OIDC_AUTO_REGISTER: "true"
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
```
|
```
|
||||||
|
|
@ -122,8 +132,8 @@ services:
|
||||||
4. **Configure Redirect URIs**:
|
4. **Configure Redirect URIs**:
|
||||||
```
|
```
|
||||||
Authorized redirect URIs:
|
Authorized redirect URIs:
|
||||||
https://your-readur-domain.com/auth/oidc/callback
|
https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
http://localhost:8000/auth/oidc/callback (for development)
|
http://localhost:8000/api/auth/oidc/callback (for development)
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Environment Variables**:
|
5. **Environment Variables**:
|
||||||
|
|
@ -132,7 +142,8 @@ services:
|
||||||
OIDC_CLIENT_ID=123456789-abcdefgh.apps.googleusercontent.com
|
OIDC_CLIENT_ID=123456789-abcdefgh.apps.googleusercontent.com
|
||||||
OIDC_CLIENT_SECRET=GOCSPX-your-secret-key
|
OIDC_CLIENT_SECRET=GOCSPX-your-secret-key
|
||||||
OIDC_ISSUER_URL=https://accounts.google.com
|
OIDC_ISSUER_URL=https://accounts.google.com
|
||||||
OIDC_REDIRECT_URI=https://your-readur-domain.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Microsoft Azure AD
|
### Microsoft Azure AD
|
||||||
|
|
@ -142,7 +153,7 @@ services:
|
||||||
- Click "New registration"
|
- Click "New registration"
|
||||||
- Name: "Readur Document Management"
|
- Name: "Readur Document Management"
|
||||||
- Supported account types: Choose based on your needs
|
- Supported account types: Choose based on your needs
|
||||||
- Redirect URI: `https://your-readur-domain.com/auth/oidc/callback`
|
- Redirect URI: `https://your-readur-domain.com/api/auth/oidc/callback`
|
||||||
|
|
||||||
2. **Configure Authentication**:
|
2. **Configure Authentication**:
|
||||||
- In your app registration, go to "Authentication"
|
- In your app registration, go to "Authentication"
|
||||||
|
|
@ -166,7 +177,8 @@ services:
|
||||||
OIDC_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
OIDC_CLIENT_ID=12345678-1234-1234-1234-123456789012
|
||||||
OIDC_CLIENT_SECRET=your-client-secret
|
OIDC_CLIENT_SECRET=your-client-secret
|
||||||
OIDC_ISSUER_URL=https://login.microsoftonline.com/your-tenant-id/v2.0
|
OIDC_ISSUER_URL=https://login.microsoftonline.com/your-tenant-id/v2.0
|
||||||
OIDC_REDIRECT_URI=https://your-readur-domain.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Keycloak
|
### Keycloak
|
||||||
|
|
@ -184,7 +196,7 @@ services:
|
||||||
3. **Configure Client Settings**:
|
3. **Configure Client Settings**:
|
||||||
- Access Type: `confidential`
|
- Access Type: `confidential`
|
||||||
- Standard Flow Enabled: `ON`
|
- Standard Flow Enabled: `ON`
|
||||||
- Valid Redirect URIs: `https://your-readur-domain.com/auth/oidc/callback*`
|
- Valid Redirect URIs: `https://your-readur-domain.com/api/auth/oidc/callback*`
|
||||||
- Web Origins: `https://your-readur-domain.com`
|
- Web Origins: `https://your-readur-domain.com`
|
||||||
|
|
||||||
4. **Get Client Secret**:
|
4. **Get Client Secret**:
|
||||||
|
|
@ -197,7 +209,8 @@ services:
|
||||||
OIDC_CLIENT_ID=readur
|
OIDC_CLIENT_ID=readur
|
||||||
OIDC_CLIENT_SECRET=your-keycloak-client-secret
|
OIDC_CLIENT_SECRET=your-keycloak-client-secret
|
||||||
OIDC_ISSUER_URL=https://keycloak.company.com/auth/realms/your-realm
|
OIDC_ISSUER_URL=https://keycloak.company.com/auth/realms/your-realm
|
||||||
OIDC_REDIRECT_URI=https://your-readur-domain.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auth0
|
### Auth0
|
||||||
|
|
@ -208,7 +221,7 @@ services:
|
||||||
- Application Type: "Regular Web Applications"
|
- Application Type: "Regular Web Applications"
|
||||||
|
|
||||||
2. **Configure Settings**:
|
2. **Configure Settings**:
|
||||||
- Allowed Callback URLs: `https://your-readur-domain.com/auth/oidc/callback`
|
- Allowed Callback URLs: `https://your-readur-domain.com/api/auth/oidc/callback`
|
||||||
- Allowed Web Origins: `https://your-readur-domain.com`
|
- Allowed Web Origins: `https://your-readur-domain.com`
|
||||||
- Allowed Logout URLs: `https://your-readur-domain.com/login`
|
- Allowed Logout URLs: `https://your-readur-domain.com/login`
|
||||||
|
|
||||||
|
|
@ -222,15 +235,154 @@ services:
|
||||||
OIDC_CLIENT_ID=your-auth0-client-id
|
OIDC_CLIENT_ID=your-auth0-client-id
|
||||||
OIDC_CLIENT_SECRET=your-auth0-client-secret
|
OIDC_CLIENT_SECRET=your-auth0-client-secret
|
||||||
OIDC_ISSUER_URL=https://your-app.auth0.com
|
OIDC_ISSUER_URL=https://your-app.auth0.com
|
||||||
OIDC_REDIRECT_URI=https://your-readur-domain.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Authentik
|
||||||
|
|
||||||
|
[Authentik](https://goauthentik.io/) is a self-hosted, open-source identity provider that's perfect for organizations wanting full control over their authentication.
|
||||||
|
|
||||||
|
1. **Create an Application** in Authentik Admin Interface:
|
||||||
|
- Navigate to "Applications" → "Applications"
|
||||||
|
- Click "Create" and choose "Create with Wizard"
|
||||||
|
- Name: "Readur Document Management"
|
||||||
|
- Slug: `readur` (or your preferred slug)
|
||||||
|
|
||||||
|
2. **Configure Provider**:
|
||||||
|
- Provider type: Choose "OAuth2/OpenID Provider"
|
||||||
|
- Authorization flow: "default-provider-authorization-implicit-consent"
|
||||||
|
- Client type: "Confidential"
|
||||||
|
- Redirect URIs: Add `https://your-readur-domain.com/api/auth/oidc/callback`
|
||||||
|
- Scopes: Ensure `openid`, `email`, and `profile` are included
|
||||||
|
|
||||||
|
3. **Get Application Credentials**:
|
||||||
|
- After creation, go to your application's "Provider" settings
|
||||||
|
- Copy the Client ID (shown in the overview)
|
||||||
|
- Copy the Client Secret (click "Copy" button)
|
||||||
|
|
||||||
|
4. **Configure Scopes and Claims** (Optional but recommended):
|
||||||
|
- Go to "Customization" → "Property Mappings"
|
||||||
|
- Ensure the following scope mappings exist and are enabled:
|
||||||
|
- `openid` → `sub` claim
|
||||||
|
- `email` → `email` claim
|
||||||
|
- `profile` → `preferred_username` and `name` claims
|
||||||
|
|
||||||
|
5. **Get Issuer URL**:
|
||||||
|
- The issuer URL format is: `https://your-authentik-domain.com/application/o/readur/`
|
||||||
|
- Replace `readur` with your application's slug
|
||||||
|
- Alternatively, use: `https://your-authentik-domain.com/application/o/<application-slug>/`
|
||||||
|
|
||||||
|
6. **Environment Variables**:
|
||||||
|
```env
|
||||||
|
OIDC_ENABLED=true
|
||||||
|
OIDC_CLIENT_ID=<your-authentik-client-id>
|
||||||
|
OIDC_CLIENT_SECRET=<your-authentik-client-secret>
|
||||||
|
OIDC_ISSUER_URL=https://your-authentik-domain.com/application/o/readur/
|
||||||
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Testing Authentik Integration**:
|
||||||
|
- Navigate to your Readur instance
|
||||||
|
- Click "Sign in with OIDC"
|
||||||
|
- You should be redirected to Authentik's login page
|
||||||
|
- After authentication, you'll be redirected back to Readur
|
||||||
|
|
||||||
|
**Authentik-Specific Tips**:
|
||||||
|
|
||||||
|
- **User Attributes**: Authentik automatically provides `email`, `preferred_username`, and `name` in the OIDC claims
|
||||||
|
- **Group Mapping**: You can map Authentik groups to user attributes (future Readur feature will support role mapping)
|
||||||
|
- **Self-Service Portal**: Users can manage their Authentik profile at `https://your-authentik-domain.com/if/user/`
|
||||||
|
- **Email Verification**: If email verification is required in Authentik, ensure users verify their email before using Readur
|
||||||
|
- **Custom Branding**: Authentik allows you to customize the login page to match your organization's branding
|
||||||
|
|
||||||
|
**Docker Compose Example with Authentik**:
|
||||||
|
|
||||||
|
If you're running both Readur and Authentik with Docker Compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
authentik-server:
|
||||||
|
image: ghcr.io/goauthentik/server:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server
|
||||||
|
environment:
|
||||||
|
AUTHENTIK_SECRET_KEY: your-secret-key-here
|
||||||
|
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||||
|
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||||
|
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||||
|
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
|
- ./custom-templates:/templates
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9443:9443"
|
||||||
|
depends_on:
|
||||||
|
- postgresql
|
||||||
|
- redis
|
||||||
|
|
||||||
|
readur:
|
||||||
|
image: readur:latest
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://readur:readur@postgres:5432/readur
|
||||||
|
|
||||||
|
# Authentik OIDC Configuration
|
||||||
|
OIDC_ENABLED: "true"
|
||||||
|
OIDC_CLIENT_ID: "<from-authentik-application>"
|
||||||
|
OIDC_CLIENT_SECRET: "<from-authentik-application>"
|
||||||
|
OIDC_ISSUER_URL: "https://authentik.company.com/application/o/readur/"
|
||||||
|
OIDC_REDIRECT_URI: "https://readur.company.com/api/auth/oidc/callback"
|
||||||
|
OIDC_AUTO_REGISTER: "true"
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
volumes:
|
||||||
|
- database:/var/lib/postgresql/data
|
||||||
|
# Create both databases
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
docker-entrypoint.sh postgres &
|
||||||
|
sleep 5
|
||||||
|
psql -U postgres -c 'CREATE DATABASE authentik;'
|
||||||
|
psql -U postgres -c 'CREATE DATABASE readur;'
|
||||||
|
wait
|
||||||
|
"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
database:
|
||||||
|
media:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Troubleshooting Authentik**:
|
||||||
|
|
||||||
|
- **"Discovery failed"**: Verify the issuer URL includes the full path with application slug
|
||||||
|
- **"Invalid client"**: Double-check the Client ID matches exactly (no extra spaces)
|
||||||
|
- **"Redirect URI mismatch"**: Ensure the redirect URI in Authentik matches `OIDC_REDIRECT_URI` exactly
|
||||||
|
- **Email not syncing**: Check that the `email` scope is enabled in your Authentik application
|
||||||
|
- **Users not linking**: Verify emails match exactly between local users and Authentik user emails
|
||||||
|
|
||||||
### Generic OIDC Provider
|
### Generic OIDC Provider
|
||||||
|
|
||||||
For any OIDC-compliant provider:
|
For any OIDC-compliant provider:
|
||||||
|
|
||||||
1. **Register Your Application** with the provider
|
1. **Register Your Application** with the provider
|
||||||
2. **Configure Redirect URI**: `https://your-readur-domain.com/auth/oidc/callback`
|
2. **Configure Redirect URI**: `https://your-readur-domain.com/api/auth/oidc/callback`
|
||||||
3. **Get Credentials**: Client ID, Client Secret, and Issuer URL
|
3. **Get Credentials**: Client ID, Client Secret, and Issuer URL
|
||||||
4. **Set Environment Variables**:
|
4. **Set Environment Variables**:
|
||||||
```env
|
```env
|
||||||
|
|
@ -238,7 +390,8 @@ For any OIDC-compliant provider:
|
||||||
OIDC_CLIENT_ID=your-client-id
|
OIDC_CLIENT_ID=your-client-id
|
||||||
OIDC_CLIENT_SECRET=your-client-secret
|
OIDC_CLIENT_SECRET=your-client-secret
|
||||||
OIDC_ISSUER_URL=https://your-provider.com
|
OIDC_ISSUER_URL=https://your-provider.com
|
||||||
OIDC_REDIRECT_URI=https://your-readur-domain.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://your-readur-domain.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing the Setup
|
## Testing the Setup
|
||||||
|
|
@ -252,7 +405,7 @@ When starting Readur, check the logs for OIDC configuration:
|
||||||
✅ OIDC_CLIENT_ID: your-client-id (loaded from env)
|
✅ OIDC_CLIENT_ID: your-client-id (loaded from env)
|
||||||
✅ OIDC_CLIENT_SECRET: ***hidden*** (loaded from env, 32 chars)
|
✅ OIDC_CLIENT_SECRET: ***hidden*** (loaded from env, 32 chars)
|
||||||
✅ OIDC_ISSUER_URL: https://accounts.google.com (loaded from env)
|
✅ OIDC_ISSUER_URL: https://accounts.google.com (loaded from env)
|
||||||
✅ OIDC_REDIRECT_URI: https://your-domain.com/auth/oidc/callback (loaded from env)
|
✅ OIDC_REDIRECT_URI: https://your-domain.com/api/auth/oidc/callback (loaded from env)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Test Discovery Endpoint
|
### 2. Test Discovery Endpoint
|
||||||
|
|
@ -306,12 +459,82 @@ For returning users:
|
||||||
2. Readur matches the user by `oidc_subject` and `oidc_issuer`
|
2. Readur matches the user by `oidc_subject` and `oidc_issuer`
|
||||||
3. User is automatically signed in without creating a duplicate account
|
3. User is automatically signed in without creating a duplicate account
|
||||||
|
|
||||||
|
### Email-Based User Syncing
|
||||||
|
|
||||||
|
Readur intelligently handles existing local users when they first log in via OIDC:
|
||||||
|
|
||||||
|
**Existing Local User with Matching Email**:
|
||||||
|
- When an OIDC user logs in with an email that matches an existing local user
|
||||||
|
- The OIDC identity is automatically linked to that existing account
|
||||||
|
- User retains all their documents, settings, and permissions
|
||||||
|
- The `auth_provider` field is updated to `oidc`
|
||||||
|
- Future logins can use OIDC (password still works if set)
|
||||||
|
|
||||||
|
**Example**: If you have a local user `john.doe@company.com`, and they log in via OIDC with the same email, their account is seamlessly upgraded to support OIDC authentication without creating a duplicate account.
|
||||||
|
|
||||||
|
### Auto-Registration Control
|
||||||
|
|
||||||
|
The `OIDC_AUTO_REGISTER` setting controls whether new users can self-register:
|
||||||
|
|
||||||
|
**When `OIDC_AUTO_REGISTER=true` (default)**:
|
||||||
|
- New OIDC users are automatically created when they first log in
|
||||||
|
- Perfect for open environments where any company employee should get access
|
||||||
|
- Username is derived from OIDC claims (preferred_username or email)
|
||||||
|
- Users get the default "user" role
|
||||||
|
|
||||||
|
**When `OIDC_AUTO_REGISTER=false`**:
|
||||||
|
- Only existing users (pre-created by admin or linked by email) can log in
|
||||||
|
- OIDC login attempts by unregistered users are rejected with HTTP 403
|
||||||
|
- Ideal for production environments requiring controlled access
|
||||||
|
- Admin must pre-create users before they can use OIDC
|
||||||
|
|
||||||
|
**Migration Strategy**: Set to `false` initially, have existing users log in to link accounts, then enable for new users.
|
||||||
|
|
||||||
|
### Disabling Local Authentication
|
||||||
|
|
||||||
|
For OIDC-only deployments, you can disable local username/password authentication:
|
||||||
|
|
||||||
|
**When `ALLOW_LOCAL_AUTH=false`**:
|
||||||
|
- Local registration endpoint returns HTTP 403 Forbidden
|
||||||
|
- Local login endpoint returns HTTP 403 Forbidden
|
||||||
|
- Only OIDC authentication is available
|
||||||
|
- Perfect for enforcing SSO-only access
|
||||||
|
- Existing local users can still be linked via email when they use OIDC
|
||||||
|
|
||||||
|
**Security Benefits**:
|
||||||
|
- Single authentication method reduces attack surface
|
||||||
|
- Centralized password management through identity provider
|
||||||
|
- No need to manage password resets or policies in Readur
|
||||||
|
- Corporate password policies automatically apply
|
||||||
|
|
||||||
|
**Important Notes**:
|
||||||
|
- Ensure OIDC is working before disabling local auth
|
||||||
|
- At least one admin should test OIDC login first
|
||||||
|
- Cannot disable both OIDC and local auth (server will refuse to start)
|
||||||
|
- Recommended configuration for production SSO environments
|
||||||
|
|
||||||
|
**Example OIDC-Only Configuration**:
|
||||||
|
```env
|
||||||
|
# Enable OIDC
|
||||||
|
OIDC_ENABLED=true
|
||||||
|
OIDC_CLIENT_ID=your-client-id
|
||||||
|
OIDC_CLIENT_SECRET=your-client-secret
|
||||||
|
OIDC_ISSUER_URL=https://your-provider.com
|
||||||
|
OIDC_REDIRECT_URI=https://readur.company.com/api/auth/oidc/callback
|
||||||
|
OIDC_AUTO_REGISTER=true
|
||||||
|
|
||||||
|
# Disable local authentication
|
||||||
|
ALLOW_LOCAL_AUTH=false
|
||||||
|
```
|
||||||
|
|
||||||
### Mixed Authentication
|
### Mixed Authentication
|
||||||
|
|
||||||
|
When both authentication methods are enabled (`ALLOW_LOCAL_AUTH=true`):
|
||||||
- Local users can continue using username/password
|
- Local users can continue using username/password
|
||||||
- OIDC users are created as separate accounts
|
- OIDC users can have both OIDC and password authentication
|
||||||
- Administrators can manage both types of users
|
- Administrators can manage both types of users
|
||||||
- No automatic account linking between local and OIDC accounts
|
- Email-based automatic account linking prevents duplicate accounts
|
||||||
|
- Users can choose their preferred login method
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|
@ -398,7 +621,7 @@ curl -X GET "https://your-readur-domain.com/api/auth/oidc/callback?code=AUTH_COD
|
||||||
|
|
||||||
1. **Use HTTPS**: Always use HTTPS in production
|
1. **Use HTTPS**: Always use HTTPS in production
|
||||||
```env
|
```env
|
||||||
OIDC_REDIRECT_URI=https://readur.company.com/auth/oidc/callback
|
OIDC_REDIRECT_URI=https://readur.company.com/api/auth/oidc/callback
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Secure Client Secret**: Store client secrets securely
|
2. **Secure Client Secret**: Store client secrets securely
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ OIDC_ENABLED: "true"
|
||||||
OIDC_CLIENT_ID: "readur-client"
|
OIDC_CLIENT_ID: "readur-client"
|
||||||
OIDC_CLIENT_SECRET: "your-client-secret"
|
OIDC_CLIENT_SECRET: "your-client-secret"
|
||||||
OIDC_ISSUER_URL: "https://auth.example.com/realms/readur"
|
OIDC_ISSUER_URL: "https://auth.example.com/realms/readur"
|
||||||
OIDC_REDIRECT_URI: "https://readur.example.com/auth/oidc/callback"
|
OIDC_REDIRECT_URI: "https://readur.example.com/api/auth/oidc/callback"
|
||||||
OIDC_SCOPES: "openid profile email"
|
OIDC_SCOPES: "openid profile email"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ pub struct Config {
|
||||||
pub oidc_client_secret: Option<String>,
|
pub oidc_client_secret: Option<String>,
|
||||||
pub oidc_issuer_url: Option<String>,
|
pub oidc_issuer_url: Option<String>,
|
||||||
pub oidc_redirect_uri: Option<String>,
|
pub oidc_redirect_uri: Option<String>,
|
||||||
|
pub oidc_auto_register: Option<bool>,
|
||||||
|
|
||||||
|
// Authentication Configuration
|
||||||
|
pub allow_local_auth: Option<bool>,
|
||||||
|
|
||||||
// S3 Configuration
|
// S3 Configuration
|
||||||
pub s3_enabled: bool,
|
pub s3_enabled: bool,
|
||||||
|
|
@ -461,7 +465,49 @@ impl Config {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
oidc_auto_register: match env::var("OIDC_AUTO_REGISTER") {
|
||||||
|
Ok(val) => match val.to_lowercase().as_str() {
|
||||||
|
"true" | "1" | "yes" | "on" => {
|
||||||
|
println!("✅ OIDC_AUTO_REGISTER: true (loaded from env)");
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
"false" | "0" | "no" | "off" => {
|
||||||
|
println!("✅ OIDC_AUTO_REGISTER: false (loaded from env)");
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("⚠️ OIDC_AUTO_REGISTER: Invalid value '{}', using default (false)", val);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
println!("⚠️ OIDC_AUTO_REGISTER: Not set, will use default (false)");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Authentication Configuration
|
||||||
|
allow_local_auth: match env::var("ALLOW_LOCAL_AUTH") {
|
||||||
|
Ok(val) => match val.to_lowercase().as_str() {
|
||||||
|
"true" | "1" | "yes" | "on" => {
|
||||||
|
println!("✅ ALLOW_LOCAL_AUTH: true (loaded from env)");
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
"false" | "0" | "no" | "off" => {
|
||||||
|
println!("✅ ALLOW_LOCAL_AUTH: false (loaded from env)");
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
println!("⚠️ ALLOW_LOCAL_AUTH: Invalid value '{}', using default (true)", val);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
println!("⚠️ ALLOW_LOCAL_AUTH: Not set, will use default (true)");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// S3 Configuration
|
// S3 Configuration
|
||||||
s3_enabled: match env::var("S3_ENABLED") {
|
s3_enabled: match env::var("S3_ENABLED") {
|
||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
|
|
@ -575,6 +621,7 @@ impl Config {
|
||||||
// OIDC validation
|
// OIDC validation
|
||||||
if config.oidc_enabled {
|
if config.oidc_enabled {
|
||||||
println!("🔐 OIDC is enabled");
|
println!("🔐 OIDC is enabled");
|
||||||
|
println!("🔓 OIDC auto-registration: {}", config.oidc_auto_register.unwrap_or(false));
|
||||||
if config.oidc_client_id.is_none() {
|
if config.oidc_client_id.is_none() {
|
||||||
println!("❌ OIDC_CLIENT_ID is required when OIDC is enabled");
|
println!("❌ OIDC_CLIENT_ID is required when OIDC is enabled");
|
||||||
}
|
}
|
||||||
|
|
@ -590,6 +637,20 @@ impl Config {
|
||||||
} else {
|
} else {
|
||||||
println!("🔐 OIDC is disabled");
|
println!("🔐 OIDC is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authentication method validation
|
||||||
|
let allow_local_auth = config.allow_local_auth.unwrap_or(true);
|
||||||
|
println!("🔑 Local authentication (username/password): {}",
|
||||||
|
if allow_local_auth { "enabled" } else { "disabled" });
|
||||||
|
|
||||||
|
if !config.oidc_enabled && !allow_local_auth {
|
||||||
|
println!("❌ WARNING: Both OIDC and local authentication are disabled!");
|
||||||
|
println!(" You will not be able to log in. Enable at least one authentication method.");
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Invalid authentication configuration: Both OIDC and local auth are disabled. \
|
||||||
|
Enable at least one authentication method (OIDC_ENABLED=true or ALLOW_LOCAL_AUTH=true)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
println!("✅ Configuration validation completed successfully!\n");
|
println!("✅ Configuration validation completed successfully!\n");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ impl Database {
|
||||||
|
|
||||||
let row = sqlx::query(
|
let row = sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO users (username, email, role, created_at, updated_at,
|
INSERT INTO users (username, email, role, created_at, updated_at,
|
||||||
oidc_subject, oidc_issuer, oidc_email, auth_provider)
|
oidc_subject, oidc_issuer, oidc_email, auth_provider)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
RETURNING id, username, email, password_hash, role, created_at, updated_at,
|
RETURNING id, username, email, password_hash, role, created_at, updated_at,
|
||||||
|
|
@ -249,4 +249,74 @@ impl Database {
|
||||||
auth_provider: row.get::<String, _>("auth_provider").try_into().unwrap_or(AuthProvider::Oidc),
|
auth_provider: row.get::<String, _>("auth_provider").try_into().unwrap_or(AuthProvider::Oidc),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT id, username, email, password_hash, role, created_at, updated_at,
|
||||||
|
oidc_subject, oidc_issuer, oidc_email, auth_provider FROM users WHERE email = $1"
|
||||||
|
)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(row) => Ok(Some(User {
|
||||||
|
id: row.get("id"),
|
||||||
|
username: row.get("username"),
|
||||||
|
email: row.get("email"),
|
||||||
|
password_hash: row.get("password_hash"),
|
||||||
|
role: row.get::<String, _>("role").try_into().unwrap_or(crate::models::UserRole::User),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
oidc_subject: row.get("oidc_subject"),
|
||||||
|
oidc_issuer: row.get("oidc_issuer"),
|
||||||
|
oidc_email: row.get("oidc_email"),
|
||||||
|
auth_provider: row.get::<String, _>("auth_provider").try_into().unwrap_or(AuthProvider::Local),
|
||||||
|
})),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn link_user_to_oidc(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
oidc_subject: &str,
|
||||||
|
oidc_issuer: &str,
|
||||||
|
oidc_email: &str,
|
||||||
|
) -> Result<User> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE users
|
||||||
|
SET oidc_subject = $2,
|
||||||
|
oidc_issuer = $3,
|
||||||
|
oidc_email = $4,
|
||||||
|
auth_provider = $5,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, username, email, password_hash, role, created_at, updated_at,
|
||||||
|
oidc_subject, oidc_issuer, oidc_email, auth_provider
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(oidc_subject)
|
||||||
|
.bind(oidc_issuer)
|
||||||
|
.bind(oidc_email)
|
||||||
|
.bind(AuthProvider::Oidc.to_string())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(User {
|
||||||
|
id: row.get("id"),
|
||||||
|
username: row.get("username"),
|
||||||
|
email: row.get("email"),
|
||||||
|
password_hash: row.get("password_hash"),
|
||||||
|
role: row.get::<String, _>("role").try_into().unwrap_or(crate::models::UserRole::User),
|
||||||
|
created_at: row.get("created_at"),
|
||||||
|
updated_at: row.get("updated_at"),
|
||||||
|
oidc_subject: row.get("oidc_subject"),
|
||||||
|
oidc_issuer: row.get("oidc_issuer"),
|
||||||
|
oidc_email: row.get("oidc_email"),
|
||||||
|
auth_provider: row.get::<String, _>("auth_provider").try_into().unwrap_or(AuthProvider::Oidc),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,8 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{create_jwt, AuthUser},
|
auth::{create_jwt, AuthUser},
|
||||||
models::{CreateUser, LoginRequest, LoginResponse, UserResponse, UserRole},
|
models::{CreateUser, LoginRequest, LoginResponse, User, UserResponse, UserRole},
|
||||||
|
oidc::OidcUserInfo,
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -39,6 +40,18 @@ async fn register(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(user_data): Json<CreateUser>,
|
Json(user_data): Json<CreateUser>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
// Check if local authentication is enabled
|
||||||
|
if !state.config.allow_local_auth.unwrap_or(true) {
|
||||||
|
tracing::warn!("Local registration attempt rejected - local auth is disabled");
|
||||||
|
return (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Local registration is disabled",
|
||||||
|
"details": "This instance only allows OIDC authentication. Please contact your administrator."
|
||||||
|
}))
|
||||||
|
).into_response();
|
||||||
|
}
|
||||||
|
|
||||||
match state.db.create_user(user_data).await {
|
match state.db.create_user(user_data).await {
|
||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let user_response: UserResponse = user.into();
|
let user_response: UserResponse = user.into();
|
||||||
|
|
@ -84,6 +97,12 @@ async fn login(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(login_data): Json<LoginRequest>,
|
Json(login_data): Json<LoginRequest>,
|
||||||
) -> Result<Json<LoginResponse>, StatusCode> {
|
) -> Result<Json<LoginResponse>, StatusCode> {
|
||||||
|
// Check if local authentication is enabled
|
||||||
|
if !state.config.allow_local_auth.unwrap_or(true) {
|
||||||
|
tracing::warn!("Local authentication attempt rejected - local auth is disabled");
|
||||||
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
let user = state
|
let user = state
|
||||||
.db
|
.db
|
||||||
.get_user_by_username(&login_data.username)
|
.get_user_by_username(&login_data.username)
|
||||||
|
|
@ -204,47 +223,91 @@ async fn oidc_callback(
|
||||||
StatusCode::UNAUTHORIZED
|
StatusCode::UNAUTHORIZED
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Find or create user in database
|
// Find or create user in database with email-based syncing
|
||||||
let issuer_url = state.config.oidc_issuer_url.as_ref().unwrap();
|
let issuer_url = state.config.oidc_issuer_url.as_ref().unwrap();
|
||||||
tracing::debug!("Looking up user by OIDC subject: {} and issuer: {}", user_info.sub, issuer_url);
|
tracing::debug!("Looking up user by OIDC subject: {} and issuer: {}", user_info.sub, issuer_url);
|
||||||
|
|
||||||
let user = match state.db.get_user_by_oidc_subject(&user_info.sub, issuer_url).await {
|
let user = match state.db.get_user_by_oidc_subject(&user_info.sub, issuer_url).await {
|
||||||
Ok(Some(existing_user)) => {
|
Ok(Some(existing_user)) => {
|
||||||
tracing::debug!("Found existing OIDC user: {}", existing_user.username);
|
tracing::debug!("Found existing OIDC user: {}", existing_user.username);
|
||||||
existing_user
|
existing_user
|
||||||
},
|
},
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::debug!("Creating new OIDC user");
|
// No OIDC user found, check if there's an existing local user with this email
|
||||||
// Create new user
|
let email = user_info.email.clone();
|
||||||
let username = user_info.preferred_username
|
|
||||||
.or_else(|| user_info.email.clone())
|
if let Some(email_addr) = &email {
|
||||||
.unwrap_or_else(|| format!("oidc_user_{}", &user_info.sub[..8]));
|
tracing::debug!("Checking for existing local user with email: {}", email_addr);
|
||||||
|
match state.db.get_user_by_email(email_addr).await {
|
||||||
let email = user_info.email.unwrap_or_else(|| format!("{}@oidc.local", username));
|
Ok(Some(existing_local_user)) => {
|
||||||
|
// Found existing local user with matching email - link to OIDC
|
||||||
tracing::debug!("New user details - username: {}, email: {}", username, email);
|
tracing::info!(
|
||||||
|
"Found existing local user '{}' with email '{}', linking to OIDC identity",
|
||||||
let create_user = CreateUser {
|
existing_local_user.username,
|
||||||
username,
|
email_addr
|
||||||
email: email.clone(),
|
);
|
||||||
password: "".to_string(), // Not used for OIDC users
|
|
||||||
role: Some(UserRole::User),
|
match state.db.link_user_to_oidc(
|
||||||
};
|
existing_local_user.id,
|
||||||
|
&user_info.sub,
|
||||||
let result = state.db.create_oidc_user(
|
issuer_url,
|
||||||
create_user,
|
email_addr,
|
||||||
&user_info.sub,
|
).await {
|
||||||
issuer_url,
|
Ok(linked_user) => {
|
||||||
&email,
|
tracing::info!(
|
||||||
).await;
|
"Successfully linked user '{}' to OIDC identity",
|
||||||
|
linked_user.username
|
||||||
match result {
|
);
|
||||||
Ok(user) => {
|
linked_user
|
||||||
tracing::info!("Successfully created OIDC user: {}", user.username);
|
},
|
||||||
user
|
Err(e) => {
|
||||||
},
|
tracing::error!("Failed to link existing user to OIDC: {}", e);
|
||||||
Err(e) => {
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
tracing::error!("Failed to create OIDC user: {} (full error: {:#})", e, e);
|
}
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
}
|
||||||
|
},
|
||||||
|
Ok(None) => {
|
||||||
|
// No existing user with this email
|
||||||
|
if state.config.oidc_auto_register.unwrap_or(false) {
|
||||||
|
// Auto-registration is enabled, create new OIDC user
|
||||||
|
tracing::debug!("No existing user with this email, creating new OIDC user (auto-registration enabled)");
|
||||||
|
create_new_oidc_user(
|
||||||
|
&state,
|
||||||
|
&user_info,
|
||||||
|
issuer_url,
|
||||||
|
email.as_deref(),
|
||||||
|
).await?
|
||||||
|
} else {
|
||||||
|
// Auto-registration is disabled, reject login
|
||||||
|
tracing::warn!(
|
||||||
|
"OIDC login attempted for unregistered email '{}', but auto-registration is disabled",
|
||||||
|
email_addr
|
||||||
|
);
|
||||||
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Database error during email lookup: {}", e);
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No email provided by OIDC provider
|
||||||
|
if state.config.oidc_auto_register.unwrap_or(false) {
|
||||||
|
// Auto-registration is enabled, create new user without email sync
|
||||||
|
tracing::debug!("No email provided by OIDC, creating new user (auto-registration enabled)");
|
||||||
|
create_new_oidc_user(
|
||||||
|
&state,
|
||||||
|
&user_info,
|
||||||
|
issuer_url,
|
||||||
|
None,
|
||||||
|
).await?
|
||||||
|
} else {
|
||||||
|
// Auto-registration is disabled and no email to sync
|
||||||
|
tracing::warn!(
|
||||||
|
"OIDC login attempted without email claim, but auto-registration is disabled"
|
||||||
|
);
|
||||||
|
return Err(StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,4 +328,50 @@ async fn oidc_callback(
|
||||||
token,
|
token,
|
||||||
user: user.into(),
|
user: user.into(),
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a new OIDC user
|
||||||
|
async fn create_new_oidc_user(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
user_info: &OidcUserInfo,
|
||||||
|
issuer_url: &str,
|
||||||
|
email: Option<&str>,
|
||||||
|
) -> Result<User, StatusCode> {
|
||||||
|
tracing::debug!("Creating new OIDC user");
|
||||||
|
|
||||||
|
let username = user_info.preferred_username
|
||||||
|
.clone()
|
||||||
|
.or_else(|| email.map(|e| e.to_string()))
|
||||||
|
.unwrap_or_else(|| format!("oidc_user_{}", &user_info.sub[..8]));
|
||||||
|
|
||||||
|
let user_email = email
|
||||||
|
.map(|e| e.to_string())
|
||||||
|
.unwrap_or_else(|| format!("{}@oidc.local", username));
|
||||||
|
|
||||||
|
tracing::debug!("New user details - username: {}, email: {}", username, user_email);
|
||||||
|
|
||||||
|
let create_user = CreateUser {
|
||||||
|
username,
|
||||||
|
email: user_email.clone(),
|
||||||
|
password: "".to_string(), // Not used for OIDC users
|
||||||
|
role: Some(UserRole::User),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = state.db.create_oidc_user(
|
||||||
|
create_user,
|
||||||
|
&user_info.sub,
|
||||||
|
issuer_url,
|
||||||
|
&user_email,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(user) => {
|
||||||
|
tracing::info!("Successfully created OIDC user: {}", user.username);
|
||||||
|
Ok(user)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create OIDC user: {} (full error: {:#})", e, e);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +204,9 @@ pub fn create_test_config() -> Config {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
|
|
||||||
// S3 Configuration (disabled for tests by default)
|
// S3 Configuration (disabled for tests by default)
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
|
|
|
||||||
|
|
@ -835,7 +835,9 @@ impl TestConfigBuilder {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
|
|
||||||
// S3 Configuration
|
// S3 Configuration
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ mod tests {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ mod tests {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
@ -130,6 +132,8 @@ mod tests {
|
||||||
oidc_client_secret: Some("test-client-secret".to_string()),
|
oidc_client_secret: Some("test-client-secret".to_string()),
|
||||||
oidc_issuer_url: Some(mock_server.uri()),
|
oidc_issuer_url: Some(mock_server.uri()),
|
||||||
oidc_redirect_uri: Some("http://localhost:8000/auth/oidc/callback".to_string()),
|
oidc_redirect_uri: Some("http://localhost:8000/auth/oidc/callback".to_string()),
|
||||||
|
oidc_auto_register: Some(true),
|
||||||
|
allow_local_auth: Some(true),
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
@ -447,4 +451,4 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,8 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,8 @@ async fn create_test_app_state() -> Arc<AppState> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,8 @@ fn test_webdav_scheduler_creation() {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,8 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ async fn setup_test_app() -> (Router, Arc<AppState>) {
|
||||||
oidc_client_secret: None,
|
oidc_client_secret: None,
|
||||||
oidc_issuer_url: None,
|
oidc_issuer_url: None,
|
||||||
oidc_redirect_uri: None,
|
oidc_redirect_uri: None,
|
||||||
|
oidc_auto_register: None,
|
||||||
|
allow_local_auth: None,
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ fn create_test_config_with_oidc(issuer_url: &str) -> Config {
|
||||||
oidc_client_secret: Some("test-client-secret".to_string()),
|
oidc_client_secret: Some("test-client-secret".to_string()),
|
||||||
oidc_issuer_url: Some(issuer_url.to_string()),
|
oidc_issuer_url: Some(issuer_url.to_string()),
|
||||||
oidc_redirect_uri: Some("http://localhost:8000/auth/oidc/callback".to_string()),
|
oidc_redirect_uri: Some("http://localhost:8000/auth/oidc/callback".to_string()),
|
||||||
|
oidc_auto_register: Some(true),
|
||||||
|
allow_local_auth: Some(true),
|
||||||
s3_enabled: false,
|
s3_enabled: false,
|
||||||
s3_config: None,
|
s3_config: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue