mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
279 lines
11 KiB
Markdown
279 lines
11 KiB
Markdown
# Authentication Service
|
|
|
|
**Status:** ✅ Implemented | Plex OAuth + OIDC + Plex Home + Local Admin + JWT + RBAC
|
|
|
|
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
|
|
|
|
- No password management needed
|
|
- Users already have Plex accounts
|
|
- Seamless integration
|
|
- Automatic profile pictures/metadata
|
|
- **Plex Home support:** Each profile = separate user
|
|
|
|
## Session Management: JWT Tokens
|
|
|
|
- Stateless authentication
|
|
- No server-side session storage
|
|
- Easy horizontal scaling
|
|
- Includes user claims (role, permissions)
|
|
|
|
## Access Control: RBAC
|
|
|
|
**Roles:**
|
|
1. **user** - Request audiobooks, view own requests, search
|
|
2. **admin** - Full system access (settings, users, all requests)
|
|
|
|
## OAuth Flow (with Plex Home Support)
|
|
|
|
1. User clicks "Login with Plex"
|
|
2. Redirect to Plex OAuth
|
|
3. User authorizes app
|
|
4. Redirect back with PIN code
|
|
5. Exchange code for main account token
|
|
6. Get main account user info
|
|
7. **Verify user has access to configured Plex server** (uses stored machineIdentifier from config)
|
|
8. **Check for Plex Home profiles:**
|
|
- If profiles exist → Redirect to profile selection page
|
|
- If no profiles → Continue with main account
|
|
9. **Profile Selection (if applicable):**
|
|
- User selects profile from grid
|
|
- Enter PIN if profile is protected
|
|
- Switch to profile, get profile's auth token
|
|
10. Create/update user in DB (with profile details)
|
|
11. Generate JWT
|
|
12. Return JWT to client
|
|
13. Client includes JWT in subsequent requests
|
|
|
|
## OAuth Endpoints
|
|
|
|
**GET /api/auth/plex/login** - Redirect to Plex OAuth
|
|
**GET /api/auth/plex/callback?pinId=...** - Exchange PIN, check for profiles, return JWT or redirect to profile selection
|
|
**GET /api/auth/plex/home-users** - Get list of Plex Home profiles (requires X-Plex-Token header)
|
|
**POST /api/auth/plex/switch-profile** - Switch to selected profile and complete authentication
|
|
**POST /api/auth/refresh** - Get new access token (refresh token in header)
|
|
**POST /api/auth/logout** - Clear client-side token
|
|
**GET /api/auth/me** - Get current user (JWT in header), includes `authProvider` field ('plex' | 'oidc' | 'local')
|
|
|
|
## JWT Structure
|
|
|
|
**Access Token (1hr):**
|
|
```json
|
|
{
|
|
"sub": "user-uuid",
|
|
"plexId": "plex-user-id",
|
|
"username": "john_doe",
|
|
"role": "admin",
|
|
"iat": 1234567890,
|
|
"exp": 1234571490
|
|
}
|
|
```
|
|
|
|
**Refresh Token (7d):**
|
|
```json
|
|
{
|
|
"sub": "user-uuid",
|
|
"type": "refresh",
|
|
"iat": 1234567890,
|
|
"exp": 1234971490
|
|
}
|
|
```
|
|
|
|
**Storage:**
|
|
- Access: HTTP-only cookie + localStorage
|
|
- Refresh: HTTP-only secure cookie only
|
|
- SameSite=Strict (CSRF protection)
|
|
|
|
## Middleware
|
|
|
|
**requireAuth()** - Verifies JWT exists/valid, adds user to request, returns 401 if invalid
|
|
**requireAdmin()** - Checks `user.role === 'admin'`, returns 403 if not, chains after requireAuth
|
|
|
|
## First User Setup
|
|
|
|
- First user created during setup automatically promoted to admin
|
|
- Marked as "setup admin" with `isSetupAdmin=true` flag
|
|
- Setup admin role is **protected** - cannot be changed to prevent lockout
|
|
- Ensures someone always has admin access after fresh install
|
|
- Subsequent users default to 'user' role
|
|
- Local admin uses username/password authentication (stored in `authToken` field as bcrypt hash)
|
|
- `plexId` format: `local-{username}` for local admin accounts
|
|
|
|
## Local Admin Authentication
|
|
|
|
**Local Admin (Setup Admin):**
|
|
- Created during setup wizard (step 2)
|
|
- Username/password authentication (separate from Plex OAuth)
|
|
- Password hashed with bcrypt (10 rounds) and stored in `authToken` field
|
|
- Login: POST `/api/auth/admin/login` with username/password
|
|
- Identified by: `isSetupAdmin=true` AND `plexId` starts with `local-`
|
|
|
|
**Password Management:**
|
|
- POST `/api/auth/change-password` - Change password for any local user
|
|
- Requires: current password, new password (min 8 chars), confirmation
|
|
- Security: Only accessible to users with `authProvider='local'` (Plex/OIDC users cannot change passwords)
|
|
- Validates current password before allowing change
|
|
- Properly decrypts stored password hash before bcrypt comparison
|
|
- Available to all local users (setup admin, locally registered users)
|
|
|
|
## Plex Home Profile Support
|
|
|
|
**Overview:**
|
|
- Plex Home accounts can have multiple profiles (managed users, family members)
|
|
- Each profile has its own library ratings, watch history, restrictions
|
|
- **Architecture:** Each profile = separate user in ReadMeABook system
|
|
|
|
**Profile Selection Flow:**
|
|
1. User authenticates with main Plex account
|
|
2. System fetches list of profiles via `GET https://plex.tv/api/home/users`
|
|
3. If profiles exist → Show profile selection page (`/auth/select-profile`)
|
|
4. User selects their profile (enters PIN if protected)
|
|
5. System switches to profile via `POST https://plex.tv/api/home/users/{id}/switch`
|
|
6. Profile's auth token is stored (encrypted)
|
|
7. User record created with profile's details
|
|
|
|
**Profile Data Storage:**
|
|
- `plexId`: Profile's unique ID (not main account ID)
|
|
- `plexUsername`: Profile's friendlyName
|
|
- `authToken`: Profile's auth token (encrypted)
|
|
- `avatarUrl`: Profile's avatar
|
|
- `plexHomeUserId`: Profile ID for reference (null = main account, set = home profile)
|
|
|
|
**User Isolation:**
|
|
- Each profile is a completely separate user
|
|
- Separate requests, separate BookDate recommendations, separate ratings
|
|
- Admin sees all profiles as independent users (no grouping)
|
|
- Profile switching = logout and login again
|
|
|
|
**Profile Protection:**
|
|
- Protected profiles require PIN on login
|
|
- PIN validated by Plex API during switch
|
|
- PIN not stored (only needed at login)
|
|
|
|
**Benefits:**
|
|
- Accurate request attribution ("Requested by Dad" vs "Requested by Kids")
|
|
- Personalized BookDate recommendations based on each profile's ratings
|
|
- Separate "My Requests" per family member
|
|
- 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
|
|
- Returns: `backendMode`, `providers[]`, `registrationEnabled`, `hasLocalUsers`, `oidcProviderName`, `localLoginDisabled`, `automationEnabled`
|
|
- `automationEnabled`: true if Prowlarr/indexer configured (used for dynamic login page description)
|
|
|
|
### 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)
|
|
|
|
## Admin-Generated Login Token
|
|
|
|
- Login token stored as SHA-256 hash in `User.loginTokenHash`
|
|
- Admin generates/revokes via user permissions modal
|
|
- User navigates to `/auth/token/login?token=rmab_...` → page POSTs token to API in request body
|
|
- API: `POST /api/auth/token/login` with `{ token }` in JSON body
|
|
- Invalid token redirects to `/login`
|
|
|
|
## Security
|
|
|
|
- Never log tokens
|
|
- HTTPS only in production
|
|
- Short access token expiry (1hr)
|
|
- Optional refresh token rotation
|
|
- Track valid tokens for revocation
|
|
- **Server access verification**: Uses stored `machineIdentifier` from config (no API call needed)
|
|
- Only users with access to the configured Plex server can authenticate
|
|
- Prevents any Plex user from accessing the instance
|
|
- 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
|
|
|
|
- Custom Plex OAuth (direct API)
|
|
- OIDC: openid-client (npm)
|
|
- jsonwebtoken (npm)
|
|
- Node.js crypto
|