Return only scopes requested ( OpenID conformance test. Update README

This commit is contained in:
Dan Milne
2026-01-02 14:05:54 +11:00
parent 07cddf5823
commit b2030df8c2
3 changed files with 48 additions and 23 deletions

View File

@@ -347,27 +347,39 @@ services:
Create a `.env` file in the same directory: Create a `.env` file in the same directory:
```bash **Generate required secrets first:**
# Generate with: openssl rand -hex 64
SECRET_KEY_BASE=your-secret-key-here
# Application URLs ```bash
# Generate SECRET_KEY_BASE (required)
openssl rand -hex 64
# Generate OIDC private key (optional - auto-generated if not provided)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
cat private_key.pem # Copy the output into OIDC_PRIVATE_KEY below
```
**Then create `.env`:**
```bash
# Rails Secret (REQUIRED)
SECRET_KEY_BASE=paste-output-from-openssl-rand-hex-64-here
# Application URLs (REQUIRED)
CLINCH_HOST=https://auth.yourdomain.com CLINCH_HOST=https://auth.yourdomain.com
CLINCH_FROM_EMAIL=noreply@yourdomain.com CLINCH_FROM_EMAIL=noreply@yourdomain.com
# SMTP Settings # SMTP Settings (REQUIRED for invitations and password resets)
SMTP_ADDRESS=smtp.example.com SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_DOMAIN=yourdomain.com SMTP_DOMAIN=yourdomain.com
SMTP_USERNAME=your-smtp-username SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password SMTP_PASSWORD=your-smtp-password
# OIDC (optional - generates temporary key if not set) # OIDC Private Key (OPTIONAL - generates temporary key if not provided)
# Generate with: openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 # For production, generate a persistent key and paste the ENTIRE contents here
# Then: OIDC_PRIVATE_KEY=$(cat private_key.pem)
OIDC_PRIVATE_KEY= OIDC_PRIVATE_KEY=
# Optional: Force SSL redirects (if not behind a reverse proxy handling SSL) # Optional: Force SSL redirects (only if NOT behind a reverse proxy handling SSL)
FORCE_SSL=false FORCE_SSL=false
``` ```

View File

@@ -3,7 +3,7 @@ class OidcJwtService
class << self class << self
# Generate an ID token (JWT) for the user # Generate an ID token (JWT) for the user
def generate_id_token(user, application, consent: nil, nonce: nil, access_token: nil, auth_time: nil, acr: nil) def generate_id_token(user, application, consent: nil, nonce: nil, access_token: nil, auth_time: nil, acr: nil, scopes: "openid")
now = Time.current.to_i now = Time.current.to_i
# Use application's configured ID token TTL (defaults to 1 hour) # Use application's configured ID token TTL (defaults to 1 hour)
ttl = application.id_token_expiry_seconds ttl = application.id_token_expiry_seconds
@@ -11,18 +11,30 @@ class OidcJwtService
# Use pairwise SID from consent if available, fallback to user ID # Use pairwise SID from consent if available, fallback to user ID
subject = consent&.sid || user.id.to_s subject = consent&.sid || user.id.to_s
# Parse scopes (space-separated string)
requested_scopes = scopes.to_s.split
# Required claims (always included per OIDC Core spec)
payload = { payload = {
iss: issuer_url, iss: issuer_url,
sub: subject, sub: subject,
aud: application.client_id, aud: application.client_id,
exp: now + ttl, exp: now + ttl,
iat: now, iat: now
email: user.email_address,
email_verified: true,
preferred_username: user.username.presence || user.email_address,
name: user.name.presence || user.email_address
} }
# Email claims (only if 'email' scope requested)
if requested_scopes.include?("email")
payload[:email] = user.email_address
payload[:email_verified] = true
end
# Profile claims (only if 'profile' scope requested)
if requested_scopes.include?("profile")
payload[:preferred_username] = user.username.presence || user.email_address
payload[:name] = user.name.presence || user.email_address
end
# Add nonce if provided (OIDC requires this for implicit flow) # Add nonce if provided (OIDC requires this for implicit flow)
payload[:nonce] = nonce if nonce.present? payload[:nonce] = nonce if nonce.present?
@@ -44,12 +56,13 @@ class OidcJwtService
payload[:at_hash] = at_hash payload[:at_hash] = at_hash
end end
# Add groups if user has any # Groups claims (only if 'groups' scope requested)
if user.groups.any? if requested_scopes.include?("groups") && user.groups.any?
payload[:groups] = user.groups.pluck(:name) payload[:groups] = user.groups.pluck(:name)
end end
# Merge custom claims from groups (arrays are combined, not overwritten) # Merge custom claims from groups (arrays are combined, not overwritten)
# Note: Custom claims from groups are always merged (not scope-dependent)
user.groups.each do |group| user.groups.each do |group|
payload = deep_merge_claims(payload, group.parsed_custom_claims) payload = deep_merge_claims(payload, group.parsed_custom_claims)
end end

View File

@@ -204,13 +204,13 @@ This checklist ensures Clinch meets security, quality, and documentation standar
- [ ] Document backup code security (single-use, store securely) - [ ] Document backup code security (single-use, store securely)
- [ ] Document admin password security requirements - [ ] Document admin password security requirements
### Future Security Enhancements ### Future Security Enhancements (Post-Beta)
- [ ] Rate limiting on authentication endpoints - [x] Rate limiting on authentication endpoints (comprehensive coverage implemented)
- [ ] Account lockout after N failed attempts - [ ] Account lockout after N failed attempts (rate limiting provides similar protection)
- [ ] Admin audit logging - [ ] Admin audit logging
- [ ] Security event notifications - [ ] Security event notifications (email/webhook alerts for suspicious activity)
- [ ] Brute force detection - [ ] Advanced brute force detection (pattern analysis beyond rate limiting)
- [ ] Suspicious login detection - [ ] Suspicious login detection (geolocation, device fingerprinting)
- [ ] IP allowlist/blocklist - [ ] IP allowlist/blocklist
## External Security Review ## External Security Review