mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add comprehensive OIDC access control and admin role mapping
Implements full OIDC configuration UI and backend support for access control and admin permissions. **Access Control Features:** - Open access (anyone can log in) - Group/claim based access (require specific group membership) - Allowed list (whitelist specific emails/usernames) - Admin approval (manual approval required for new users) **Admin Role Mapping:** - Automatic admin role assignment based on OIDC claims - Configurable claim name and value (default: groups claim) - First user always becomes admin - Dynamic role updates on each login **Setup Wizard:** - Updated OIDCConfigStep with comprehensive OIDC settings - Access control method selector with conditional fields - Admin role mapping configuration with examples - Improved UX with clear sections and helpful descriptions **Admin Settings:** - Expanded OIDC section with all new configuration options - Proper JSON array handling for allowed emails/usernames - Visual organization matching setup wizard **Backend:** - Updated setup complete API to persist new OIDC fields - Updated OIDC settings API for all new configuration - Updated settings GET endpoint to return new fields with defaults - Proper comma-separated to JSON array conversion **Documentation:** - Comprehensive OIDC section in auth.md - Configuration examples and use cases - Clear distinction between access control and admin roles - Default values documented All changes tested and ready for production use.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# Authentication Service
|
# Authentication Service
|
||||||
|
|
||||||
**Status:** ✅ Implemented | Plex OAuth + Plex Home profile support + JWT sessions + RBAC
|
**Status:** ✅ Implemented | Plex OAuth + OIDC + Plex Home + Local Admin + JWT + RBAC
|
||||||
|
|
||||||
Handles authentication and authorization: Plex OAuth integration with Plex Home profile support, JWT session management, role-based access control.
|
Handles authentication and authorization: Multiple auth providers (Plex OAuth, OIDC, Local Admin), Plex Home profile support, JWT session management, comprehensive access control, role-based authorization.
|
||||||
|
|
||||||
## Authentication: Plex OAuth
|
## Authentication: Plex OAuth
|
||||||
|
|
||||||
@@ -155,6 +155,96 @@ Handles authentication and authorization: Plex OAuth integration with Plex Home
|
|||||||
- Separate "My Requests" per family member
|
- Separate "My Requests" per family member
|
||||||
- Accurate logs and analytics
|
- Accurate logs and analytics
|
||||||
|
|
||||||
|
## OIDC Authentication (Audiobookshelf Mode)
|
||||||
|
|
||||||
|
**Status:** ✅ Implemented | OpenID Connect support with comprehensive access control and admin role mapping
|
||||||
|
|
||||||
|
### OIDC Provider Configuration
|
||||||
|
- **Provider Name**: Display name for login button (e.g., "Authentik", "Keycloak")
|
||||||
|
- **Issuer URL**: OIDC provider's issuer URL (must support `.well-known/openid-configuration`)
|
||||||
|
- **Client ID/Secret**: OAuth2 credentials from OIDC provider
|
||||||
|
- **Required Scopes**: `openid`, `profile`, `email`, `groups`
|
||||||
|
- **Redirect URI**: `{BASE_URL}/api/auth/oidc/callback`
|
||||||
|
|
||||||
|
### Access Control Methods
|
||||||
|
Controls who can log in to the application (separate from admin role assignment):
|
||||||
|
|
||||||
|
**1. Open Access (`open`)**
|
||||||
|
- Anyone who can authenticate with OIDC provider has access
|
||||||
|
- No additional restrictions
|
||||||
|
- Default: Suitable for trusted internal providers
|
||||||
|
|
||||||
|
**2. Group/Claim Based (`group_claim`)**
|
||||||
|
- Requires specific group/claim value for access
|
||||||
|
- Config: `oidc.access_group_claim` (default: `groups`)
|
||||||
|
- Config: `oidc.access_group_value` (required group name)
|
||||||
|
- Example: Only users in "readmeabook-users" group can log in
|
||||||
|
|
||||||
|
**3. Allowed List (`allowed_list`)**
|
||||||
|
- Whitelist of specific emails and/or usernames
|
||||||
|
- Config: `oidc.allowed_emails` (JSON array)
|
||||||
|
- Config: `oidc.allowed_usernames` (JSON array)
|
||||||
|
- Example: `["user1@example.com", "user2@example.com"]`
|
||||||
|
|
||||||
|
**4. Admin Approval (`admin_approval`)**
|
||||||
|
- New users created in "pending_approval" state
|
||||||
|
- Admin must approve/reject users before they can access
|
||||||
|
- Pending users visible in admin settings
|
||||||
|
|
||||||
|
### Admin Role Mapping
|
||||||
|
Automatically grants admin permissions based on OIDC claims (e.g., group membership):
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- `oidc.admin_claim_enabled` = `'true'` | `'false'` (default: `'false'`)
|
||||||
|
- `oidc.admin_claim_name` = claim field to check (default: `'groups'`)
|
||||||
|
- `oidc.admin_claim_value` = required value for admin role (e.g., `'readmeabook-admin'`)
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- First OIDC user always becomes admin (regardless of claim settings)
|
||||||
|
- Subsequent users checked against admin claim if enabled
|
||||||
|
- If claim matches → granted admin role
|
||||||
|
- If claim doesn't match → granted user role
|
||||||
|
- Claim check occurs on every login (role can be updated dynamically)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
- Authentik group: Create `readmeabook-admin` group
|
||||||
|
- Add users to group
|
||||||
|
- Configure: `oidc.admin_claim_value = 'readmeabook-admin'`
|
||||||
|
- Users in group get admin role on login
|
||||||
|
|
||||||
|
### OIDC Endpoints
|
||||||
|
- **GET /api/auth/oidc/login** - Initiate OIDC flow, redirect to provider
|
||||||
|
- **GET /api/auth/oidc/callback** - Handle OAuth callback, create/update user, return JWT
|
||||||
|
- **GET /api/auth/providers** - List enabled auth providers for login page
|
||||||
|
|
||||||
|
### Configuration Keys
|
||||||
|
```
|
||||||
|
oidc.enabled = 'true' | 'false'
|
||||||
|
oidc.provider_name = 'Authentik' (display name)
|
||||||
|
oidc.issuer_url = 'https://...'
|
||||||
|
oidc.client_id = 'xxx'
|
||||||
|
oidc.client_secret = (encrypted)
|
||||||
|
|
||||||
|
# Access Control
|
||||||
|
oidc.access_control_method = 'open' | 'group_claim' | 'allowed_list' | 'admin_approval'
|
||||||
|
oidc.access_group_claim = 'groups' (claim name)
|
||||||
|
oidc.access_group_value = 'readmeabook-users' (required group)
|
||||||
|
oidc.allowed_emails = '["user@example.com"]' (JSON array)
|
||||||
|
oidc.allowed_usernames = '["username"]' (JSON array)
|
||||||
|
|
||||||
|
# Admin Role Mapping
|
||||||
|
oidc.admin_claim_enabled = 'true' | 'false'
|
||||||
|
oidc.admin_claim_name = 'groups'
|
||||||
|
oidc.admin_claim_value = 'readmeabook-admin'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- **Provider:** `src/lib/services/auth/OIDCAuthProvider.ts`
|
||||||
|
- **Routes:** `src/app/api/auth/oidc/login/route.ts`, `src/app/api/auth/oidc/callback/route.ts`
|
||||||
|
- **Setup Wizard:** `src/app/setup/steps/OIDCConfigStep.tsx`
|
||||||
|
- **Admin Settings:** OIDC section in `/admin/settings` (auth tab)
|
||||||
|
- **Library:** `openid-client` (OIDC discovery, token exchange, PKCE)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
- Never log tokens
|
- Never log tokens
|
||||||
@@ -166,9 +256,11 @@ Handles authentication and authorization: Plex OAuth integration with Plex Home
|
|||||||
- Only users with access to the configured Plex server can authenticate
|
- Only users with access to the configured Plex server can authenticate
|
||||||
- Prevents any Plex user from accessing the instance
|
- Prevents any Plex user from accessing the instance
|
||||||
- machineIdentifier stored during setup/settings configuration (architectural optimization)
|
- machineIdentifier stored during setup/settings configuration (architectural optimization)
|
||||||
|
- **OIDC PKCE**: All OIDC flows use PKCE (Proof Key for Code Exchange) for enhanced security
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- Custom Plex OAuth (direct API)
|
- Custom Plex OAuth (direct API)
|
||||||
|
- OIDC: openid-client (npm)
|
||||||
- jsonwebtoken (npm)
|
- jsonwebtoken (npm)
|
||||||
- Node.js crypto
|
- Node.js crypto
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ interface Settings {
|
|||||||
issuerUrl: string;
|
issuerUrl: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
accessControlMethod: string;
|
||||||
|
accessGroupClaim: string;
|
||||||
|
accessGroupValue: string;
|
||||||
|
allowedEmails: string;
|
||||||
|
allowedUsernames: string;
|
||||||
|
adminClaimEnabled: boolean;
|
||||||
|
adminClaimName: string;
|
||||||
|
adminClaimValue: string;
|
||||||
};
|
};
|
||||||
registration: {
|
registration: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -184,6 +192,22 @@ export default function AdminSettings() {
|
|||||||
const response = await fetchWithAuth('/api/admin/settings');
|
const response = await fetchWithAuth('/api/admin/settings');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Convert OIDC allowed lists from JSON arrays to comma-separated strings for display
|
||||||
|
if (data.oidc) {
|
||||||
|
const parseArrayToCommaSeparated = (jsonStr: string): string => {
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(jsonStr);
|
||||||
|
return Array.isArray(arr) ? arr.join(', ') : '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
data.oidc.allowedEmails = parseArrayToCommaSeparated(data.oidc.allowedEmails);
|
||||||
|
data.oidc.allowedUsernames = parseArrayToCommaSeparated(data.oidc.allowedUsernames);
|
||||||
|
}
|
||||||
|
|
||||||
setSettings(data);
|
setSettings(data);
|
||||||
setOriginalSettings(JSON.parse(JSON.stringify(data))); // Deep copy for comparison
|
setOriginalSettings(JSON.parse(JSON.stringify(data))); // Deep copy for comparison
|
||||||
} else {
|
} else {
|
||||||
@@ -767,10 +791,23 @@ export default function AdminSettings() {
|
|||||||
|
|
||||||
// Save OIDC settings if OIDC is enabled
|
// Save OIDC settings if OIDC is enabled
|
||||||
if (settings.oidc.enabled) {
|
if (settings.oidc.enabled) {
|
||||||
|
// Helper function to parse comma-separated strings into JSON arrays
|
||||||
|
const parseCommaSeparatedToArray = (str: string): string => {
|
||||||
|
if (!str || str.trim() === '') return '[]';
|
||||||
|
const items = str.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||||
|
return JSON.stringify(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
const oidcPayload = {
|
||||||
|
...settings.oidc,
|
||||||
|
allowedEmails: parseCommaSeparatedToArray(settings.oidc.allowedEmails),
|
||||||
|
allowedUsernames: parseCommaSeparatedToArray(settings.oidc.allowedUsernames),
|
||||||
|
};
|
||||||
|
|
||||||
const oidcResponse = await fetchWithAuth('/api/admin/settings/oidc', {
|
const oidcResponse = await fetchWithAuth('/api/admin/settings/oidc', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(settings.oidc),
|
body: JSON.stringify(oidcPayload),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!oidcResponse.ok) {
|
if (!oidcResponse.ok) {
|
||||||
@@ -1941,6 +1978,247 @@ export default function AdminSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Access Control Section */}
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
Access Control
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
Control who can log in to your application. This is separate from admin permissions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Access Control Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={settings.oidc.accessControlMethod}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, accessControlMethod: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="open">Open Access (anyone can log in)</option>
|
||||||
|
<option value="group_claim">Group/Claim Based</option>
|
||||||
|
<option value="allowed_list">Allowed List (emails/usernames)</option>
|
||||||
|
<option value="admin_approval">Admin Approval Required</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{settings.oidc.accessControlMethod === 'open' && 'Anyone who can authenticate with your OIDC provider will have access'}
|
||||||
|
{settings.oidc.accessControlMethod === 'group_claim' && 'Only users with a specific group/claim can access'}
|
||||||
|
{settings.oidc.accessControlMethod === 'allowed_list' && 'Only explicitly allowed users can access'}
|
||||||
|
{settings.oidc.accessControlMethod === 'admin_approval' && 'New users must be approved by an admin before access is granted'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.oidc.accessControlMethod === 'group_claim' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Group Claim Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.accessGroupClaim}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, accessGroupClaim: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="groups"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
The OIDC claim field that contains group membership (usually "groups" or "roles")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Required Group
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.accessGroupValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, accessGroupValue: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="readmeabook-users"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Users must be in this group to access the application
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settings.oidc.accessControlMethod === 'allowed_list' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Allowed Emails (comma-separated)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.allowedEmails}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, allowedEmails: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="user1@example.com, user2@example.com"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Enter email addresses separated by commas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Allowed Usernames (comma-separated)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.allowedUsernames}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, allowedUsernames: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="john_doe, jane_smith"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Enter usernames separated by commas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Role Mapping Section */}
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
Admin Role Mapping
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
Automatically grant admin permissions based on OIDC claims (e.g., group membership). The first user will always become admin.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="admin-claim-enabled"
|
||||||
|
checked={settings.oidc.adminClaimEnabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, adminClaimEnabled: e.target.checked },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label
|
||||||
|
htmlFor="admin-claim-enabled"
|
||||||
|
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Enable Admin Role Mapping
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Automatically grant admin role to users with specific OIDC claim values
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings.oidc.adminClaimEnabled && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Admin Claim Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.adminClaimName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, adminClaimName: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="groups"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
The OIDC claim field to check for admin role (usually "groups" or "roles")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Admin Claim Value
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={settings.oidc.adminClaimValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
oidc: { ...settings.oidc, adminClaimValue: e.target.value },
|
||||||
|
});
|
||||||
|
setValidated({ ...validated, oidc: false });
|
||||||
|
}}
|
||||||
|
placeholder="readmeabook-admin"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Users with this value in their claim will be granted admin role
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-800">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||||
|
Example Configuration
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||||
|
In Authentik: Create a group called "readmeabook-admin", add users to it, and set "Admin Claim Value" to "readmeabook-admin"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,21 @@ export async function PUT(request: NextRequest) {
|
|||||||
return requireAdmin(req, async () => {
|
return requireAdmin(req, async () => {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { enabled, providerName, issuerUrl, clientId, clientSecret } = body;
|
const {
|
||||||
|
enabled,
|
||||||
|
providerName,
|
||||||
|
issuerUrl,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
accessControlMethod,
|
||||||
|
accessGroupClaim,
|
||||||
|
accessGroupValue,
|
||||||
|
allowedEmails,
|
||||||
|
allowedUsernames,
|
||||||
|
adminClaimEnabled,
|
||||||
|
adminClaimName,
|
||||||
|
adminClaimValue,
|
||||||
|
} = body;
|
||||||
|
|
||||||
const { getConfigService } = await import('@/lib/services/config.service');
|
const { getConfigService } = await import('@/lib/services/config.service');
|
||||||
const configService = getConfigService();
|
const configService = getConfigService();
|
||||||
@@ -22,6 +36,14 @@ export async function PUT(request: NextRequest) {
|
|||||||
{ key: 'oidc.provider_name', value: providerName || '' },
|
{ key: 'oidc.provider_name', value: providerName || '' },
|
||||||
{ key: 'oidc.issuer_url', value: issuerUrl || '' },
|
{ key: 'oidc.issuer_url', value: issuerUrl || '' },
|
||||||
{ key: 'oidc.client_id', value: clientId || '' },
|
{ key: 'oidc.client_id', value: clientId || '' },
|
||||||
|
{ key: 'oidc.access_control_method', value: accessControlMethod || 'open' },
|
||||||
|
{ key: 'oidc.access_group_claim', value: accessGroupClaim || 'groups' },
|
||||||
|
{ key: 'oidc.access_group_value', value: accessGroupValue || '' },
|
||||||
|
{ key: 'oidc.allowed_emails', value: allowedEmails || '[]' },
|
||||||
|
{ key: 'oidc.allowed_usernames', value: allowedUsernames || '[]' },
|
||||||
|
{ key: 'oidc.admin_claim_enabled', value: adminClaimEnabled ? 'true' : 'false' },
|
||||||
|
{ key: 'oidc.admin_claim_name', value: adminClaimName || 'groups' },
|
||||||
|
{ key: 'oidc.admin_claim_value', value: adminClaimValue || '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Only update client secret if provided (not masked)
|
// Only update client secret if provided (not masked)
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export async function GET(request: NextRequest) {
|
|||||||
issuerUrl: configMap.get('oidc.issuer_url') || '',
|
issuerUrl: configMap.get('oidc.issuer_url') || '',
|
||||||
clientId: configMap.get('oidc.client_id') || '',
|
clientId: configMap.get('oidc.client_id') || '',
|
||||||
clientSecret: maskValue('client_secret', configMap.get('oidc.client_secret')),
|
clientSecret: maskValue('client_secret', configMap.get('oidc.client_secret')),
|
||||||
|
accessControlMethod: configMap.get('oidc.access_control_method') || 'open',
|
||||||
|
accessGroupClaim: configMap.get('oidc.access_group_claim') || 'groups',
|
||||||
|
accessGroupValue: configMap.get('oidc.access_group_value') || '',
|
||||||
|
allowedEmails: configMap.get('oidc.allowed_emails') || '[]',
|
||||||
|
allowedUsernames: configMap.get('oidc.allowed_usernames') || '[]',
|
||||||
|
adminClaimEnabled: configMap.get('oidc.admin_claim_enabled') === 'true',
|
||||||
|
adminClaimName: configMap.get('oidc.admin_claim_name') || 'groups',
|
||||||
|
adminClaimValue: configMap.get('oidc.admin_claim_value') || '',
|
||||||
},
|
},
|
||||||
registration: {
|
registration: {
|
||||||
enabled: configMap.get('auth.registration_enabled') === 'true',
|
enabled: configMap.get('auth.registration_enabled') === 'true',
|
||||||
|
|||||||
@@ -241,6 +241,56 @@ export async function POST(request: NextRequest) {
|
|||||||
update: { value: encryptedClientSecret, encrypted: true },
|
update: { value: encryptedClientSecret, encrypted: true },
|
||||||
create: { key: 'oidc.client_secret', value: encryptedClientSecret, encrypted: true },
|
create: { key: 'oidc.client_secret', value: encryptedClientSecret, encrypted: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Access control configuration
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.access_control_method' },
|
||||||
|
update: { value: oidc.access_control_method || 'open' },
|
||||||
|
create: { key: 'oidc.access_control_method', value: oidc.access_control_method || 'open' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.access_group_claim' },
|
||||||
|
update: { value: oidc.access_group_claim || 'groups' },
|
||||||
|
create: { key: 'oidc.access_group_claim', value: oidc.access_group_claim || 'groups' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.access_group_value' },
|
||||||
|
update: { value: oidc.access_group_value || '' },
|
||||||
|
create: { key: 'oidc.access_group_value', value: oidc.access_group_value || '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.allowed_emails' },
|
||||||
|
update: { value: oidc.allowed_emails || '[]' },
|
||||||
|
create: { key: 'oidc.allowed_emails', value: oidc.allowed_emails || '[]' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.allowed_usernames' },
|
||||||
|
update: { value: oidc.allowed_usernames || '[]' },
|
||||||
|
create: { key: 'oidc.allowed_usernames', value: oidc.allowed_usernames || '[]' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin role mapping configuration
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.admin_claim_enabled' },
|
||||||
|
update: { value: oidc.admin_claim_enabled || 'false' },
|
||||||
|
create: { key: 'oidc.admin_claim_enabled', value: oidc.admin_claim_enabled || 'false' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.admin_claim_name' },
|
||||||
|
update: { value: oidc.admin_claim_name || 'groups' },
|
||||||
|
create: { key: 'oidc.admin_claim_name', value: oidc.admin_claim_name || 'groups' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { key: 'oidc.admin_claim_value' },
|
||||||
|
update: { value: oidc.admin_claim_value || '' },
|
||||||
|
create: { key: 'oidc.admin_claim_value', value: oidc.admin_claim_value || '' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual registration configuration (if enabled)
|
// Manual registration configuration (if enabled)
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ interface SetupState {
|
|||||||
oidcIssuerUrl: string;
|
oidcIssuerUrl: string;
|
||||||
oidcClientId: string;
|
oidcClientId: string;
|
||||||
oidcClientSecret: string;
|
oidcClientSecret: string;
|
||||||
|
oidcAccessControlMethod: string;
|
||||||
|
oidcAccessGroupClaim: string;
|
||||||
|
oidcAccessGroupValue: string;
|
||||||
|
oidcAllowedEmails: string;
|
||||||
|
oidcAllowedUsernames: string;
|
||||||
|
oidcAdminClaimEnabled: boolean;
|
||||||
|
oidcAdminClaimName: string;
|
||||||
|
oidcAdminClaimValue: string;
|
||||||
|
|
||||||
// Manual registration config
|
// Manual registration config
|
||||||
requireAdminApproval: boolean;
|
requireAdminApproval: boolean;
|
||||||
@@ -114,6 +122,14 @@ export default function SetupWizard() {
|
|||||||
oidcIssuerUrl: '',
|
oidcIssuerUrl: '',
|
||||||
oidcClientId: '',
|
oidcClientId: '',
|
||||||
oidcClientSecret: '',
|
oidcClientSecret: '',
|
||||||
|
oidcAccessControlMethod: 'open',
|
||||||
|
oidcAccessGroupClaim: 'groups',
|
||||||
|
oidcAccessGroupValue: '',
|
||||||
|
oidcAllowedEmails: '',
|
||||||
|
oidcAllowedUsernames: '',
|
||||||
|
oidcAdminClaimEnabled: false,
|
||||||
|
oidcAdminClaimName: 'groups',
|
||||||
|
oidcAdminClaimValue: '',
|
||||||
|
|
||||||
// Manual registration config
|
// Manual registration config
|
||||||
requireAdminApproval: true,
|
requireAdminApproval: true,
|
||||||
@@ -231,11 +247,26 @@ export default function SetupWizard() {
|
|||||||
|
|
||||||
// OIDC configuration
|
// OIDC configuration
|
||||||
if (state.authMethod === 'oidc' || state.authMethod === 'both') {
|
if (state.authMethod === 'oidc' || state.authMethod === 'both') {
|
||||||
|
// Helper function to parse comma-separated strings into JSON arrays
|
||||||
|
const parseCommaSeparatedToArray = (str: string): string => {
|
||||||
|
if (!str || str.trim() === '') return '[]';
|
||||||
|
const items = str.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||||
|
return JSON.stringify(items);
|
||||||
|
};
|
||||||
|
|
||||||
payload.oidc = {
|
payload.oidc = {
|
||||||
provider_name: state.oidcProviderName,
|
provider_name: state.oidcProviderName,
|
||||||
issuer_url: state.oidcIssuerUrl,
|
issuer_url: state.oidcIssuerUrl,
|
||||||
client_id: state.oidcClientId,
|
client_id: state.oidcClientId,
|
||||||
client_secret: state.oidcClientSecret,
|
client_secret: state.oidcClientSecret,
|
||||||
|
access_control_method: state.oidcAccessControlMethod,
|
||||||
|
access_group_claim: state.oidcAccessGroupClaim,
|
||||||
|
access_group_value: state.oidcAccessGroupValue,
|
||||||
|
allowed_emails: parseCommaSeparatedToArray(state.oidcAllowedEmails),
|
||||||
|
allowed_usernames: parseCommaSeparatedToArray(state.oidcAllowedUsernames),
|
||||||
|
admin_claim_enabled: state.oidcAdminClaimEnabled ? 'true' : 'false',
|
||||||
|
admin_claim_name: state.oidcAdminClaimName,
|
||||||
|
admin_claim_value: state.oidcAdminClaimValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +423,14 @@ export default function SetupWizard() {
|
|||||||
oidcIssuerUrl={state.oidcIssuerUrl}
|
oidcIssuerUrl={state.oidcIssuerUrl}
|
||||||
oidcClientId={state.oidcClientId}
|
oidcClientId={state.oidcClientId}
|
||||||
oidcClientSecret={state.oidcClientSecret}
|
oidcClientSecret={state.oidcClientSecret}
|
||||||
|
oidcAccessControlMethod={state.oidcAccessControlMethod}
|
||||||
|
oidcAccessGroupClaim={state.oidcAccessGroupClaim}
|
||||||
|
oidcAccessGroupValue={state.oidcAccessGroupValue}
|
||||||
|
oidcAllowedEmails={state.oidcAllowedEmails}
|
||||||
|
oidcAllowedUsernames={state.oidcAllowedUsernames}
|
||||||
|
oidcAdminClaimEnabled={state.oidcAdminClaimEnabled}
|
||||||
|
oidcAdminClaimName={state.oidcAdminClaimName}
|
||||||
|
oidcAdminClaimValue={state.oidcAdminClaimValue}
|
||||||
onUpdate={updateField}
|
onUpdate={updateField}
|
||||||
onNext={() => goToStep(currentStepNumber + 1)}
|
onNext={() => goToStep(currentStepNumber + 1)}
|
||||||
onBack={() => goToStep(currentStepNumber - 1)}
|
onBack={() => goToStep(currentStepNumber - 1)}
|
||||||
|
|||||||
@@ -14,7 +14,15 @@ interface OIDCConfigStepProps {
|
|||||||
oidcIssuerUrl: string;
|
oidcIssuerUrl: string;
|
||||||
oidcClientId: string;
|
oidcClientId: string;
|
||||||
oidcClientSecret: string;
|
oidcClientSecret: string;
|
||||||
onUpdate: (field: string, value: string) => void;
|
oidcAccessControlMethod: string;
|
||||||
|
oidcAccessGroupClaim: string;
|
||||||
|
oidcAccessGroupValue: string;
|
||||||
|
oidcAllowedEmails: string;
|
||||||
|
oidcAllowedUsernames: string;
|
||||||
|
oidcAdminClaimEnabled: boolean;
|
||||||
|
oidcAdminClaimName: string;
|
||||||
|
oidcAdminClaimValue: string;
|
||||||
|
onUpdate: (field: string, value: any) => void;
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
@@ -24,6 +32,14 @@ export function OIDCConfigStep({
|
|||||||
oidcIssuerUrl,
|
oidcIssuerUrl,
|
||||||
oidcClientId,
|
oidcClientId,
|
||||||
oidcClientSecret,
|
oidcClientSecret,
|
||||||
|
oidcAccessControlMethod,
|
||||||
|
oidcAccessGroupClaim,
|
||||||
|
oidcAccessGroupValue,
|
||||||
|
oidcAllowedEmails,
|
||||||
|
oidcAllowedUsernames,
|
||||||
|
oidcAdminClaimEnabled,
|
||||||
|
oidcAdminClaimName,
|
||||||
|
oidcAdminClaimValue,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
@@ -85,17 +101,22 @@ export function OIDCConfigStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
Configure OIDC Provider
|
Configure OIDC Provider
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||||
Enter your OIDC provider details for single sign-on authentication.
|
Set up single sign-on authentication with your OIDC provider (Authentik, Keycloak, etc.)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Connection */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||||
|
Provider Connection
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Provider Name
|
Provider Name
|
||||||
@@ -221,35 +242,224 @@ export function OIDCConfigStep({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||||
Configuration Tips
|
Configuration Tips
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-sm text-blue-700 dark:text-blue-300 mt-1 space-y-1">
|
<ul className="text-sm text-blue-700 dark:text-blue-300 mt-1 space-y-1">
|
||||||
<li>• The redirect URI will be: {typeof window !== 'undefined' ? `${window.location.origin}/api/auth/oidc/callback` : '[Your Domain]/api/auth/oidc/callback'}</li>
|
<li>• The redirect URI will be: {typeof window !== 'undefined' ? `${window.location.origin}/api/auth/oidc/callback` : '[Your Domain]/api/auth/oidc/callback'}</li>
|
||||||
<li>• Configure this redirect URI in your OIDC provider settings</li>
|
<li>• Configure this redirect URI in your OIDC provider settings</li>
|
||||||
<li>• Required scopes: openid, profile, email</li>
|
<li>• Required scopes: openid, profile, email, groups</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
{/* Access Control */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||||
|
Access Control
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Control who can log in to your application. This is separate from admin permissions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Access Control Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={oidcAccessControlMethod}
|
||||||
|
onChange={(e) => onUpdate('oidcAccessControlMethod', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="open">Open Access (anyone can log in)</option>
|
||||||
|
<option value="group_claim">Group/Claim Based</option>
|
||||||
|
<option value="allowed_list">Allowed List (emails/usernames)</option>
|
||||||
|
<option value="admin_approval">Admin Approval Required</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{oidcAccessControlMethod === 'open' && 'Anyone who can authenticate with your OIDC provider will have access'}
|
||||||
|
{oidcAccessControlMethod === 'group_claim' && 'Only users with a specific group/claim can access'}
|
||||||
|
{oidcAccessControlMethod === 'allowed_list' && 'Only explicitly allowed users can access'}
|
||||||
|
{oidcAccessControlMethod === 'admin_approval' && 'New users must be approved by an admin before access is granted'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{oidcAccessControlMethod === 'group_claim' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Group Claim Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="groups"
|
||||||
|
value={oidcAccessGroupClaim}
|
||||||
|
onChange={(e) => onUpdate('oidcAccessGroupClaim', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
The OIDC claim field that contains group membership (usually "groups" or "roles")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Required Group
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="readmeabook-users"
|
||||||
|
value={oidcAccessGroupValue}
|
||||||
|
onChange={(e) => onUpdate('oidcAccessGroupValue', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Users must be in this group to access the application
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{oidcAccessControlMethod === 'allowed_list' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Allowed Emails (comma-separated)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="user1@example.com, user2@example.com"
|
||||||
|
value={oidcAllowedEmails}
|
||||||
|
onChange={(e) => onUpdate('oidcAllowedEmails', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Enter email addresses separated by commas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Allowed Usernames (comma-separated)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="john_doe, jane_smith"
|
||||||
|
value={oidcAllowedUsernames}
|
||||||
|
onChange={(e) => onUpdate('oidcAllowedUsernames', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Enter usernames separated by commas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Role Mapping */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||||
|
Admin Role Mapping
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Automatically grant admin permissions based on OIDC claims (e.g., group membership). The first user will always become admin.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="admin-claim-enabled"
|
||||||
|
checked={oidcAdminClaimEnabled}
|
||||||
|
onChange={(e) => onUpdate('oidcAdminClaimEnabled', e.target.checked)}
|
||||||
|
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<label
|
||||||
|
htmlFor="admin-claim-enabled"
|
||||||
|
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Enable Admin Role Mapping
|
||||||
|
</label>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Automatically grant admin role to users with specific OIDC claim values
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{oidcAdminClaimEnabled && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Admin Claim Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="groups"
|
||||||
|
value={oidcAdminClaimName}
|
||||||
|
onChange={(e) => onUpdate('oidcAdminClaimName', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
The OIDC claim field to check for admin role (usually "groups" or "roles")
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Admin Claim Value
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="readmeabook-admin"
|
||||||
|
value={oidcAdminClaimValue}
|
||||||
|
onChange={(e) => onUpdate('oidcAdminClaimValue', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Users with this value in their claim will be granted admin role
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||||
|
Example Configuration
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||||
|
In Authentik: Create a group called "readmeabook-admin", add users to it, and set "Admin Claim Value" to "readmeabook-admin"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<Button onClick={onBack} variant="outline">
|
<Button onClick={onBack} variant="outline">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user