Skip to main content

Overview

CCDigital implements role-based access control with three distinct authentication flows:
  1. Admin (ROLE_GOBIERNO): Form-based authentication with credentials
  2. Issuer (ROLE_ISSUER): Form-based authentication with credentials
  3. 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
Data Source: 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
Data Source: 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
Data Source: Hyperledger Indy verifiable credentials Login Path: /login/user Principal: IndyUserPrincipal (contains verified attributes from proof)

Admin Authentication

Login Flow

  1. User navigates to /login/admin
  2. Submits form with username (email or full name) and password
  3. adminUserDetailsService queries users table
  4. Validates user is active and credentials match
  5. Creates Spring Security authentication with role from role field
  6. Marks session with current app instance ID
  7. Redirects to /admin/dashboard

Endpoint

URL: /login/admin Method: POST Form Parameters:
username
string
required
Email or full name (case-insensitive)
password
string
required
User password (BCrypt hashed)
Success Response: 302 redirect to /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
Reference: SecurityConfig.java:240

Issuer Authentication

Login Flow

  1. User navigates to /login/issuer
  2. Submits form with email and password
  3. issuerUserDetailsService queries entity_users table
  4. Validates user is active, has password hash, and has entity_id
  5. Creates IssuerPrincipal with entity information
  6. Marks session with app instance ID
  7. Redirects to /issuer

Endpoint

URL: /login/issuer Method: POST Form Parameters:
username
string
required
Email (case-insensitive)
password
string
required
User password (BCrypt hashed)
Success Response: 302 redirect to /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)

User authentication is a multi-step process involving:
  1. Hyperledger Indy proof verification
  2. Password validation
  3. Second-factor authentication (TOTP or email OTP)

Flow Overview


Step 1: Start Authentication

URL: /user/auth/start Method: POST Content-Type: application/json
email
string
required
User’s email address
password
string
required
User’s password
Response: JSON
presExId
string
Presentation exchange ID from ACA-Py
qrDataUrl
string
Base64-encoded QR code image (data URL)
Deep link for mobile wallet apps
Process:
  1. Validates email exists in system
  2. Creates Indy proof request via ACA-Py
  3. Stores email/password in session (temporary)
  4. Returns QR code for mobile wallet
Reference: UserAuthController.java:98 → UserAuthFlowService Example Request:
curl -X POST https://api.example.com/user/auth/start \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "SecurePassword123"
  }'
Example Response:
{
  "presExId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "qrDataUrl": "data:image/png;base64,iVBORw0KGgoAAAANS...",
  "deepLink": "didcomm://aries_connection_invitation?..."
}

Step 2: Poll Proof State

URL: /user/auth/poll Method: GET
presExId
string
required
Presentation exchange ID from start response
Response States: Pending:
{
  "status": "pending",
  "message": "Esperando presentación de credenciales..."
}
Proof Verified:
{
  "status": "otp_required",
  "needsOtp": true,
  "otpMethod": "email",
  "message": "Se envió un código a tu correo"
}
Error:
{
  "status": "error",
  "message": "Proof verification failed"
}
Process:
  1. Queries ACA-Py for presentation exchange state
  2. If verified, extracts attributes (id_type, id_number, first_name, last_name, email)
  3. Validates password from session
  4. Determines second factor method (TOTP if enabled, else email OTP)
  5. Sends email OTP if needed
Polling: Frontend should poll every 2 seconds Timeout: Configured via 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
presExId
string
required
Presentation exchange ID
code
string
required
6-digit OTP code (from email or TOTP app)
Success Response:
{
  "status": "authenticated",
  "authenticated": true,
  "redirectUrl": "/user/dashboard"
}
Error Response:
{
  "status": "error",
  "message": "Código inválido o expirado"
}
Process:
  1. Retrieves stored proof data from session
  2. Validates OTP code (TOTP via time-based algorithm, or email OTP from cache)
  3. Creates IndyUserPrincipal with verified attributes
  4. Establishes Spring Security authentication
  5. Records login audit event
  6. Syncs user state to Indy if configured
Rate Limiting: Max 3 attempts per OTP session Reference: UserAuthController.java:121

Step 4: Resend OTP

URL: /user/auth/otp/resend Method: POST Content-Type: application/json
presExId
string
required
Presentation exchange ID
Response:
{
  "status": "sent",
  "message": "Código reenviado a tu correo"
}
Cooldown: Configured via 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)
Apps Supported:
  • Google Authenticator
  • Aegis Authenticator
  • Microsoft Authenticator
  • Any RFC 6238 compatible app
Reference: UserTotpService.java

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)
Email Template:
Tu código de verificación para CCDigital es: 123456

Este código expira en 10 minutos.
Reference: LoginOtpService.java

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; includeSubDomains

Content Security Policy

Header: Content-Security-PolicyPolicy:
  • default-src 'self'
  • script-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com
  • style-src 'self' 'unsafe-inline' cdn.jsdelivr.net fonts.googleapis.com
  • font-src 'self' data: cdn.jsdelivr.net fonts.gstatic.com
  • img-src 'self' data: blob:
  • frame-ancestors 'self'
  • object-src 'none'

Referrer Policy

Header: Referrer-PolicyValue: same-origin

X-Content-Type-Options

Header: X-Content-Type-OptionsValue: nosniff

Permissions Policy

Header: Permissions-PolicyValue: geolocation=(), camera=(), microphone=(), payment=()
Reference: SecurityConfig.java:450

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/**
Implementation: 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
Error Handling: CSRF failures redirect to login with ?expired=true Reference: SecurityConfig.java:253

Environment Variables

ACA-Py / Indy

Mail Configuration

Security

Build docs developers (and LLMs) love