Overview
CCDigital uses Hyperledger Indy and ACA-Py (Aries Cloud Agent - Python) to implement verifiable credential-based authentication and user access management. The system replaces traditional password-only login with a proof-based verification flow. Architecture:- Issuer Agent (ACA-Py): Issues credentials to users
- Holder Agent (ACA-Py): Stores user credentials
- Verifier (Backend): Requests and verifies proofs
- Indy Ledger: Credential schemas and definitions
The backend acts as a verifier during login and an issuer administrator for credential lifecycle management.
Service Components
IndyAdminClient
Package:co.edu.unbosque.ccdigital.service
Purpose: HTTP client for ACA-Py Admin API
Features:
- GET/POST requests to ACA-Py endpoints
- X-API-Key header management
- JSON response parsing to
JsonNode - Base URL normalization
IndyProofLoginService
Package:co.edu.unbosque.ccdigital.service
Purpose: Present-proof 2.0 protocol for login
Features:
- Sends proof requests filtered by
id_number - Polls proof exchange status
- Extracts revealed attributes (id_type, id_number, first_name, last_name, email)
- Resolves holder connection (fixed or auto-discovery)
UserAuthFlowService
Package:co.edu.unbosque.ccdigital.service
Purpose: Complete user authentication flow (proof + second factor)
Features:
- Orchestrates start/poll/verify/resend endpoints
- Manages session state for pending authentication
- Handles TOTP and email OTP as second factors
- Creates Spring Security session after verification
UserAccessGovernanceService
Package:co.edu.unbosque.ccdigital.service
Purpose: Admin control of user access state with Indy sync
Features:
- Updates user access state (ENABLED/SUSPENDED/DISABLED)
- Synchronizes state to ACA-Py connection metadata
- Records state changes on Fabric for audit
- Handles sync failures gracefully
Configuration
Indy/ACA-Py Properties
INDY_ISSUER_ADMIN_URLINDY_HOLDER_ADMIN_URLINDY_HOLDER_CONNECTION_IDINDY_HOLDER_LABELINDY_CRED_DEF_IDINDY_ADMIN_API_KEYINDY_USER_ACCESS_SYNC_ENABLEDINDY_USER_ACCESS_SYNC_PATH
IndyAdminClient Methods
get(String baseUrl, String path)
get(String baseUrl, String path)
Executes GET request to ACA-Py Admin API.Parameters:
baseUrl- Base URL (e.g.,http://localhost:8021)path- API path (e.g.,/status)
JsonNode - Parsed JSON responseThrows: IllegalStateException if response is not valid JSONExample:post(String baseUrl, String path, Object body)
post(String baseUrl, String path, Object body)
Executes POST request with JSON body.Parameters:
baseUrl- Base URLpath- API pathbody- Object to serialize to JSON
JsonNode - Parsed JSON responseThrows: IllegalStateException on serialization or JSON parse errorExample:IndyProofLoginService Methods
startLoginByIdNumber(String idNumber)
startLoginByIdNumber(String idNumber)
Initiates present-proof 2.0 request filtered by ID number.Flow:
- Resolves holder
connection_id(fixed or auto) - Builds proof request with restrictions:
cred_def_id= configured credential definitionattr::id_number::value= providedidNumber
- Sends POST to
/present-proof-2.0/send-request - Returns record with
presExId
idNumber- ID number to filter holder credentials
Map<String, Object> - Record with keys:presExId- Presentation exchange IDpres_ex_id- Raw ACA-Py fieldstate- Initial state (usuallyrequest-sent)
The proof request includes attributes:
id_type, id_number, first_name, last_name, emailgetProofStatus(String presExId)
getProofStatus(String presExId)
Polls the status of a proof exchange.Parameters:
presExId- Presentation exchange ID
Map<String, Object> with:presExId- Exchange IDstate- Current state (request-sent,presentation-received,done,abandoned)verified- Boolean (true if proof verified)done- Boolean (true if terminal state)error- Error message (if present)
request-sent- Waiting for holder to respondpresentation-received- Proof received, verifyingdone- Successfully verifiedabandoned- Failed or rejected
getVerifiedResultWithAttrs(String presExId)
getVerifiedResultWithAttrs(String presExId)
Extracts revealed attributes from a verified proof.Requirements:
- State must be
doneorpresentation-received verifiedmust betrue
Map<String, String> with keys:id_type- ID type (e.g., “CC”)id_number- ID numberfirst_name- First namelast_name- Last nameemail- Email address
IllegalStateException if proof not ready or not verifiedExample:listProofTraceEvents()
listProofTraceEvents()
Lists all proof exchanges for admin monitoring.Returns:
List<ProofTraceEvent> with fields:presExId- Exchange IDstate- Current stateverified- Verification statuseventAt- Timestamp (normalized to America/Bogota)idType,idNumber,firstName,lastName,email- Extracted or inferred
UserAuthFlowService Methods
start(String email, String password, HttpServletRequest request)
start(String email, String password, HttpServletRequest request)
Starts the authentication flow.Flow:
- Validates email/password against database
- Checks user access state (ENABLED/SUSPENDED/DISABLED)
- Looks up
id_numberfrom linkedPersonrecord - Calls
IndyProofLoginService.startLoginByIdNumber() - Stores expected
id_numberin session - Returns
presExIdfor polling
email- User emailpassword- User passwordrequest- HTTP request (for session)
ResponseEntity<Map<String, Object>> with:presExId- Exchange ID for polling
- 401: Invalid credentials
- 403: Account suspended/disabled
poll(String presExId, HttpServletRequest request)
poll(String presExId, HttpServletRequest request)
Polls proof status and prepares second factor.Flow:
- Validates session has expected
id_number - Calls
IndyProofLoginService.getProofStatus() - If proof verified:
- Validates credential matches expected user
- Determines second factor method (TOTP or email)
- Sends email OTP if needed
- Returns
otpRequired=true
- If proof failed: Returns
done=true
presExId- Exchange IDrequest- HTTP request (for session)
ResponseEntity<Map<String, Object>> with:authenticated- BooleanotpRequired- BooleanotpMethod- “totp” or “email”maskedEmail- Partially masked emailmessage- Instruction message
verifyOtp(String presExId, String code, HttpServletRequest request)
verifyOtp(String presExId, String code, HttpServletRequest request)
Verifies second factor code and completes authentication.Flow:
- Retrieves pending context from session
- Verifies code (TOTP or email OTP)
- If valid:
- Creates
IndyUserPrincipal - Authenticates Spring Security session
- Returns
authenticated=truewith redirect URL
- Creates
- If invalid:
- Increments failed attempts
- After 3 attempts: Locks and sends alert
presExId- Exchange IDcode- OTP coderequest- HTTP request
ResponseEntity<Map<String, Object>> with:authenticated- BooleanredirectUrl- Dashboard URL (if successful)displayName- User’s full name
- 401: Invalid code
- 403: Max attempts exceeded
resendOtp(String presExId, HttpServletRequest request)
resendOtp(String presExId, HttpServletRequest request)
Resends email OTP or activates email fallback from TOTP.Parameters:
presExId- Exchange IDrequest- HTTP request
ResponseEntity<Map<String, Object>> with:ok- BooleanotpMethod- “email”maskedEmail- Email addressmessage- Confirmation message
- If TOTP: Switches to email OTP fallback
- If email: Resends code with cooldown validation
UserAccessGovernanceService Methods
updateState(Long personId, UserAccessState targetState, String reason)
updateState(Long personId, UserAccessState targetState, String reason)
Updates user access state and syncs to Indy.Flow:
- Validates user is end-user role
- Updates
access_state,access_state_reason,access_state_updated_at - Sets
is_activebased on state - Syncs state to ACA-Py connection metadata (if enabled)
- Records audit event on Fabric
- Saves sync result to
indy_access_synced,indy_access_sync_at,indy_access_sync_error
personId- Person ID (user primary key)targetState-UserAccessState.ENABLED,SUSPENDED, orDISABLEDreason- Reason for state change
AccessUpdateResult with:access-UserAccessViewwith updated stateindyCallAttempted- BooleanindyCallSucceeded- BooleanindyMessage- Result message
IllegalArgumentException if user not found or not end-userExample:findByPersonId(Long personId)
findByPersonId(Long personId)
Retrieves current access state for a user.Parameters:
personId- Person ID
Optional<UserAccessView> with:personIdemailfullNameroleisActiveaccessState- “ENABLED”, “SUSPENDED”, or “DISABLED”reason- State change reasonupdatedAt- Last update timestampindySynced- Last sync success (Boolean)indySyncAt- Last sync timestampindySyncError- Last sync error message
Present-Proof Flow Diagram
Access State Synchronization
Metadata Path Format
Whenuser-access-sync-path contains {connection_id}:
State Enum Values
- ENABLED: User can log in normally
- SUSPENDED: Temporary access block (can be re-enabled)
- DISABLED: Permanent access block (sets
is_active=false)
Error Handling
Connection Resolution
Fixed Mode: Use configuredholder-connection-id
Auto Mode: Query /connections?state=active and match by their_label
Error:
Proof Verification Errors
| Error | Cause | Solution |
|---|---|---|
| ”El proof aún no está listo” | Polling before holder responds | Continue polling |
| ”Proof no verificado” | Invalid credential or proof | Check credential validity |
| ”La credencial verificada no coincide” | ID mismatch | Ensure user has correct credential |
Best Practices
- Always validate session state before accepting proof results
- Use email masking when displaying contact information
- Implement rate limiting on OTP verification (max 3 attempts)
- Log all Indy sync failures for admin review
- Handle connection auto-discovery failures gracefully
- Store presExId in session, not in URL parameters
- Clear session state after successful login or max attempts
Related Services
- Blockchain Services - Audit logging (UserAccessGovernanceService:111)
- UserLoginOtpService - Email OTP generation and verification
- UserTotpService - TOTP validation for second factor
- PersonService - Person data retrieval
