Overview
CCDigital implements role-based access control with three distinct authentication flows:- Admin (ROLE_GOBIERNO): Form-based authentication with credentials
- Issuer (ROLE_ISSUER): Form-based authentication with credentials
- User (ROLE_USER): Indy proof verification + multi-factor authentication
All authentication is configured in
SecurityConfig.java with separate security filter chains for each module.Roles
ROLE_GOBIERNO
Administrative role for government users. Capabilities:- Manage persons and documents
- Review and approve documents
- Execute synchronization with blockchain
- Generate analytics and reports
- Manage user access states
users table (AppUser entity)
Login Path: /login/admin
ROLE_ISSUER
Role for document-issuing entities (hospitals, notaries, government offices). Capabilities:- Search for persons
- Upload PDF documents
- Create access requests
- View approved documents
entity_users table (EntityUser entity)
Login Path: /login/issuer
Principal: IssuerPrincipal (exposes issuerId as entity_id)
ROLE_USER
Role for end users (citizens). Capabilities:- View own documents from Fabric ledger
- Approve/reject access requests
- Configure TOTP MFA
- Manage profile
/login/user
Principal: IndyUserPrincipal (contains verified attributes from proof)
Admin Authentication
Login Flow
- User navigates to
/login/admin - Submits form with username (email or full name) and password
adminUserDetailsServicequeriesuserstable- Validates user is active and credentials match
- Creates Spring Security authentication with role from
rolefield - Marks session with current app instance ID
- Redirects to
/admin/dashboard
Endpoint
URL:/login/admin
Method: POST
Form Parameters:
Email or full name (case-insensitive)
User password (BCrypt hashed)
/admin/dashboard
Failure Response: 302 redirect to /login/admin?error=true
Reference: SecurityConfig.java:233
Logout
URL:/admin/logout
Method: POST
Effect:
- Invalidates HTTP session
- Deletes JSESSIONID cookie
- Redirects to
/login/admin?logout=true
Issuer Authentication
Login Flow
- User navigates to
/login/issuer - Submits form with email and password
issuerUserDetailsServicequeriesentity_userstable- Validates user is active, has password hash, and has entity_id
- Creates
IssuerPrincipalwith entity information - Marks session with app instance ID
- Redirects to
/issuer
Endpoint
URL:/login/issuer
Method: POST
Form Parameters:
Email (case-insensitive)
User password (BCrypt hashed)
/issuer
Failure Response: 302 redirect to /login/issuer?error=true
Reference: SecurityConfig.java:289
Logout
URL:/issuer/logout
Method: POST
Effect:
- Invalidates session
- Deletes cookies
- Redirects to
/login/issuer?logout=true
User Authentication (Indy + MFA)
Flow Overview
Step 1: Start Authentication
URL:/user/auth/start
Method: POST
Content-Type: application/json
User’s email address
User’s password
Presentation exchange ID from ACA-Py
Base64-encoded QR code image (data URL)
Deep link for mobile wallet apps
- Validates email exists in system
- Creates Indy proof request via ACA-Py
- Stores email/password in session (temporary)
- Returns QR code for mobile wallet
Step 2: Poll Proof State
URL:/user/auth/poll
Method: GET
Presentation exchange ID from start response
- Queries ACA-Py for presentation exchange state
- If verified, extracts attributes (id_type, id_number, first_name, last_name, email)
- Validates password from session
- Determines second factor method (TOTP if enabled, else email OTP)
- Sends email OTP if needed
acapy.proof.poll-timeout-ms (default: 120000ms)
Reference: UserAuthController.java:111
Step 3: Verify OTP
URL:/user/auth/otp/verify
Method: POST
Content-Type: application/json
Presentation exchange ID
6-digit OTP code (from email or TOTP app)
- Retrieves stored proof data from session
- Validates OTP code (TOTP via time-based algorithm, or email OTP from cache)
- Creates
IndyUserPrincipalwith verified attributes - Establishes Spring Security authentication
- Records login audit event
- Syncs user state to Indy if configured
Step 4: Resend OTP
URL:/user/auth/otp/resend
Method: POST
Content-Type: application/json
Presentation exchange ID
app.security.login-otp.resend-cooldown-seconds (default: 60s)
Reference: UserAuthController.java:134
Indy Proof Configuration
Required Attributes
The proof request requires the following attributes from the verifiable credential:Credential Definition
Environment Variable:ACAPY_CRED_DEF_ID or INDY_CRED_DEF_ID
Format: {issuer_did}:3:CL:{schema_seq_no}:tag
Example: Th7MpTaRZVRYnPiabds81Y:3:CL:12:default
Multi-Factor Authentication
TOTP (Time-based One-Time Password)
Algorithm: HOTP (RFC 4226) with time steps (RFC 6238) Configuration:- Period: 30 seconds (configurable via
app.security.totp.period-seconds) - Digits: 6
- Window: ±1 time step for clock skew tolerance
- Secret Length: 32 bytes (Base32 encoded)
- Google Authenticator
- Aegis Authenticator
- Microsoft Authenticator
- Any RFC 6238 compatible app
Email OTP
Fallback: Used when TOTP is not enabled Configuration:- Code Length: 6 digits (configurable via
app.security.login-otp.code-length) - TTL: 10 minutes (configurable via
app.security.login-otp.code-ttl-minutes) - Max Attempts: 3 (configurable via
app.security.login-otp.max-attempts)
Session Management
Session Timeout
Configuration:server.servlet.session.timeout (env: SERVER_SESSION_TIMEOUT)
Default: 30 minutes
Invalid Session URL:
- Admin:
/login/admin?expired=true - Issuer:
/login/issuer?expired=true - User:
/login/user?expired=true
App Instance Validation
Admin and Issuer sessions are validated against the current app instance ID. Purpose: Invalidate sessions after server restart Implementation:AppInstanceSessionValidationFilter checks session attribute against current UUID
Reference: SecurityConfig.java:498
Security Headers
All responses include the following security headers:HSTS
Header:
Strict-Transport-SecurityValue: max-age=31536000; includeSubDomainsContent Security Policy
Header:
Content-Security-PolicyPolicy:default-src 'self'script-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.comstyle-src 'self' 'unsafe-inline' cdn.jsdelivr.net fonts.googleapis.comfont-src 'self' data: cdn.jsdelivr.net fonts.gstatic.comimg-src 'self' data: blob:frame-ancestors 'self'object-src 'none'
Referrer Policy
Header:
Referrer-PolicyValue: same-originX-Content-Type-Options
Header:
X-Content-Type-OptionsValue: nosniffPermissions Policy
Header:
Permissions-PolicyValue: geolocation=(), camera=(), microphone=(), payment=()Rate Limiting
Configuration
Enable:app.security.rate-limit.enabled (env: APP_SECURITY_RATE_LIMIT_ENABLED)
Window: app.security.rate-limit.window-seconds (env: APP_SECURITY_RATE_LIMIT_WINDOW_SECONDS)
Max Requests: app.security.rate-limit.max-requests-per-window (env: APP_SECURITY_RATE_LIMIT_MAX_REQUESTS_PER_WINDOW)
Default: 100 requests per 60 seconds
Protected Endpoints
/user/auth/start/user/auth/otp/verify/user/auth/otp/resend/register/user/**/login/user/forgot/**
SensitiveEndpointRateLimitFilter
CSRF Protection
Cross-Site Request Forgery protection is enabled for all POST/PUT/DELETE requests. Exception:/user/auth/** endpoints (AJAX-based, use custom validation)
Token Location:
- Form field:
_csrf - HTTP header:
X-CSRF-TOKEN
?expired=true
Reference: SecurityConfig.java:253
