Skip to content

MX-239: add confirmation flow for client-user endpoint#141

Merged
IOhacker merged 1 commit intoopenMF:developfrom
DeathGun44:MX-239-add-enrollment-confirmation-flow
Apr 13, 2026
Merged

MX-239: add confirmation flow for client-user endpoint#141
IOhacker merged 1 commit intoopenMF:developfrom
DeathGun44:MX-239-add-enrollment-confirmation-flow

Conversation

@DeathGun44
Copy link
Copy Markdown
Contributor

@DeathGun44 DeathGun44 commented Apr 13, 2026

  • selfEnroll creates client + disabled user (enabled=false)
  • Enrollment token stored in request_audit_table with type ENROLLMENT
  • New POST /self/registration/client-user/confirm endpoint validates token and enables the user via AppSelfServiceUser.enable()
  • User cannot login until enrollment is confirmed
  • Token replay protection via markConsumed() + optimistic locking
  • Updated enrollment + forgot-password tests for 2-step flow
  • Wrapped token delivery in try-catch (swallowed for audit/retry)
  • Added BEGIN/COMMIT transaction wrapper to executeSqlInPostgres()

Summary by CodeRabbit

Release Notes

  • New Features
    • Self-enrollment now implements a two-step verification process: account creation followed by email or SMS token confirmation for activation.
    • New enrollment confirmation endpoint to activate accounts after token verification.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@DeathGun44 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 52 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 42 minutes and 52 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 77fb32c8-9733-426e-88f1-1d4bdf89b9ed

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2cfcc and 4916c6a.

📒 Files selected for processing (9)
  • src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceConfirmationRequest.java
  • src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceRegistrationApiResource.java
  • src/main/java/org/apache/fineract/selfservice/registration/domain/SelfServiceRequestType.java
  • src/main/java/org/apache/fineract/selfservice/registration/service/SelfServiceRegistrationWritePlatformService.java
  • src/main/java/org/apache/fineract/selfservice/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java
  • src/main/java/org/apache/fineract/selfservice/security/starter/SelfServiceSecurityConfiguration.java
  • src/main/java/org/apache/fineract/selfservice/useradministration/domain/AppSelfServiceUser.java
  • src/test/java/org/apache/fineract/selfservice/registration/api/SelfServiceEnrollmentIntegrationTest.java
  • src/test/java/org/apache/fineract/selfservice/security/api/SelfForgotPasswordApiResourceIntegrationTest.java
📝 Walkthrough

Walkthrough

The PR refactors the self-service enrollment flow from single-step to two-step: selfEnroll now creates a disabled user, generates an enrollment confirmation token, and delivers it via email/SMS; a new confirmEnrollment endpoint validates the token and activates the user. Supporting changes include service layer modifications, domain object updates, security configuration adjustments, and integration tests validating the workflow.

Changes

Cohort / File(s) Summary
API Resource & Service Interface
src/main/java/org/apache/fineract/.../SelfServiceRegistrationApiResource.java, src/main/java/org/apache/fineract/.../SelfServiceRegistrationWritePlatformService.java
Updated selfEnroll() to return SelfServiceRegistration instead of capturing user ID; added new confirmEnrollment() endpoint accepting a JSON payload with confirmation token. Updated OpenAPI documentation and Swagger response codes (403 for expired/consumed, 404 for token not found).
Service Implementation
src/main/java/org/apache/fineract/.../SelfServiceRegistrationWritePlatformServiceImpl.java
Implemented two-step enrollment flow: selfEnroll() creates disabled user and persists enrollment SelfServiceRegistration with generated token and expiry; new confirmEnrollment() method validates token, enables user, and marks registration as consumed. Added helper methods for enrollment request resolution and validation.
Domain Objects
src/main/java/org/apache/fineract/.../SelfServiceRequestType.java, src/main/java/org/apache/fineract/.../AppSelfServiceUser.java
Added ENROLLMENT enum constant to SelfServiceRequestType describing token/workflow for self-enrollment confirmation. Added enable() method to AppSelfServiceUser to activate previously disabled users.
Security Configuration
src/main/java/org/apache/fineract/.../SelfServiceSecurityConfiguration.java
Added two new public POST route exceptions for confirmation endpoint paths to permit unauthenticated access.
Integration Tests
src/test/java/org/apache/fineract/.../SelfServiceEnrollmentIntegrationTest.java, src/test/java/org/apache/fineract/.../SelfForgotPasswordApiResourceIntegrationTest.java
Replaced single-step enrollment test with new testSuccessfulTwoStepEnrollment() validating: client/user creation, disabled state assertion, token retrieval, enrollment confirmation, enabled state assertion, and token replay rejection. Updated enrollUser() helper to perform complete two-step flow with token confirmation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as SelfServiceRegistrationApiResource
    participant Service as SelfServiceRegistrationWritePlatformService
    participant DB as Database
    participant Email as Email/SMS Service

    Client->>API: POST /client-user (enrollment request)
    API->>Service: selfEnroll(request)
    Service->>DB: Create Client
    Service->>DB: Create disabled AppSelfServiceUser
    Service->>DB: Persist SelfServiceRegistration with token
    Service->>Email: Deliver enrollment token
    Note over Email: Failures logged & swallowed
    Service-->>API: Return SelfServiceRegistration
    API-->>Client: 200 (success message)

    Client->>Client: Retrieve token from email/SMS
    Client->>API: POST /client-user/confirm (token)
    API->>Service: confirmEnrollment(request)
    Service->>DB: Resolve SelfServiceRegistration by token
    Service->>DB: Validate token state & expiry
    Service->>DB: Enable AppSelfServiceUser
    Service->>DB: Mark registration consumed
    Service-->>API: Return enabled AppSelfServiceUser
    API-->>Client: 200 (user ID)

    Client->>Client: Authenticate with credentials
    Client-->>API: Credentials now accepted (user enabled)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

⏱️ 10-30 Min Review

Suggested reviewers

  • IOhacker
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: a new confirmation flow (two-step enrollment process) for the client-user endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@DeathGun44
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/test/java/org/apache/fineract/selfservice/registration/api/SelfServiceEnrollmentIntegrationTest.java (1)

75-82: Avoid formatting username into the SQL text.

Lines 79-81 still build the query with String.format(...). Please route this through a parameterized helper so the test follows the same SQL-safety rule as the rest of the codebase.

As per coding guidelines, "Always use prepared statements, parameterized queries, or Spring Data repositories."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/org/apache/fineract/selfservice/registration/api/SelfServiceEnrollmentIntegrationTest.java`
around lines 75 - 82, The queryEnrollmentToken method currently injects the
username via String.format into the SQL string; change it to use a parameterized
query helper (e.g., the existing querySingleValueInPostgres that supports
parameters) so the SQL becomes a constant with a placeholder and the username
passed as a parameter (ensure proper escaping via the prepared statement, not
manual replace). Update queryEnrollmentToken to build the SQL "SELECT
external_authorization_token FROM request_audit_table WHERE username = ? AND
request_type = 'ENROLLMENT' ORDER BY id DESC LIMIT 1" and call the parameterized
variant of querySingleValueInPostgres (or wrap it with a new helper) passing
username as the parameter. Ensure you reference the existing method name
queryEnrollmentToken and the helper querySingleValueInPostgres when making the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceRegistrationApiResource.java`:
- Around line 89-99: Update the OpenAPI `@ApiResponse` for the selfEnroll endpoint
to accurately describe the actual response: in
SelfServiceRegistrationApiResource.selfEnroll change the 200-response
description to indicate it returns a confirmation token (e.g. "200 - OK; returns
confirmation token string") and add a content/schema entry describing a string
response (or reference java.lang.String) so the generated docs match the method
return value (which currently returns
SelfServiceApiConstants.createRequestSuccessMessage).
- Around line 107-116: The POST confirmation endpoint in
SelfServiceRegistrationApiResource (path "client-user/confirm") lacks a
`@RequestBody` schema so OpenAPI clients can't tell whether to send
externalAuthenticationToken or the legacy requestId/authenticationToken pair;
add a documented request model (e.g., ConfirmSelfEnrollmentRequest DTO) or
inline `@RequestBody` with `@Schema` describing fields externalAuthenticationToken
(string) and legacy requestId and authenticationToken (strings), specify
required/oneOf semantics (externalAuthenticationToken OR the legacy pair),
annotate the controller method parameter (e.g.,
confirmEnrollment(ConfirmSelfEnrollmentRequest request)) with `@RequestBody` and
example values, and update the endpoint's Swagger annotations so the generated
OpenAPI spec clearly documents which payload to send.

In
`@src/main/java/org/apache/fineract/selfservice/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java`:
- Around line 485-488: Concurrent confirmation races can cause flush-time
optimistic locking exceptions and surface as 500s; wrap the critical section
around appUser.enable(), appSelfServiceUserRepository.saveAndFlush(appUser),
request.markConsumed(), and
selfServiceRegistrationRepository.saveAndFlush(request) in a try/catch that
catches the JPA/ Spring optimistic locking exception (e.g.,
OptimisticLockingFailureException / ObjectOptimisticLockingFailureException) and
translate it into the documented invalid/consumed-token response by throwing the
same domain/ API exception used for expired/consumed tokens in
SelfServiceRegistrationWritePlatformServiceImpl (or returning that error); this
ensures concurrent token replays map to the expected invalid-token result
instead of a 500.
- Around line 446-450: Remove logging of raw usernames in
SelfServiceRegistrationWritePlatformServiceImpl: update the log.error call that
currently logs username (failing delivery of enrollment confirmation token) and
the log.info call ("Self-enrollment created") and the other log at the
referenced location to avoid emitting PII. Replace the username field in those
messages with only the internal id (newClientId) or a one-way hashed identifier
(e.g., SHA-256 of username) if correlation is required, and keep the exception e
in the error log as before; ensure the changes are applied to the error and info
logging statements in the method handling self-enrollment delivery/creation so
no raw usernames are written to logs.

In
`@src/test/java/org/apache/fineract/selfservice/security/api/SelfForgotPasswordApiResourceIntegrationTest.java`:
- Around line 55-59: The test in SelfForgotPasswordApiResourceIntegrationTest
uses querySingleValue(...) to fetch enrollmentToken but only
assertsNotNull(enrollmentToken), which misses empty-string results; update the
assertion to ensure enrollmentToken is non-blank (e.g., replace or add an
assertion after retrieving enrollmentToken that checks it is not empty/blank
such as assertTrue(!enrollmentToken.isBlank()) or
assertFalse(enrollmentToken.trim().isEmpty())) so a missing-token query fails
the test immediately.

---

Nitpick comments:
In
`@src/test/java/org/apache/fineract/selfservice/registration/api/SelfServiceEnrollmentIntegrationTest.java`:
- Around line 75-82: The queryEnrollmentToken method currently injects the
username via String.format into the SQL string; change it to use a parameterized
query helper (e.g., the existing querySingleValueInPostgres that supports
parameters) so the SQL becomes a constant with a placeholder and the username
passed as a parameter (ensure proper escaping via the prepared statement, not
manual replace). Update queryEnrollmentToken to build the SQL "SELECT
external_authorization_token FROM request_audit_table WHERE username = ? AND
request_type = 'ENROLLMENT' ORDER BY id DESC LIMIT 1" and call the parameterized
variant of querySingleValueInPostgres (or wrap it with a new helper) passing
username as the parameter. Ensure you reference the existing method name
queryEnrollmentToken and the helper querySingleValueInPostgres when making the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83a3609b-ad29-4487-a5a2-806662a0de83

📥 Commits

Reviewing files that changed from the base of the PR and between 451592d and 1d2cfcc.

📒 Files selected for processing (8)
  • src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceRegistrationApiResource.java
  • src/main/java/org/apache/fineract/selfservice/registration/domain/SelfServiceRequestType.java
  • src/main/java/org/apache/fineract/selfservice/registration/service/SelfServiceRegistrationWritePlatformService.java
  • src/main/java/org/apache/fineract/selfservice/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java
  • src/main/java/org/apache/fineract/selfservice/security/starter/SelfServiceSecurityConfiguration.java
  • src/main/java/org/apache/fineract/selfservice/useradministration/domain/AppSelfServiceUser.java
  • src/test/java/org/apache/fineract/selfservice/registration/api/SelfServiceEnrollmentIntegrationTest.java
  • src/test/java/org/apache/fineract/selfservice/security/api/SelfForgotPasswordApiResourceIntegrationTest.java

Comment on lines +89 to +99
@Operation(summary = "Self Enrollment Flow", description = "Creates a Fineract Client and a disabled Self Service User. Returns a confirmation token that must be validated to activate the user.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = SelfServiceEnrollmentRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "400", description = "Bad Request"),
@ApiResponse(responseCode = "409", description = "Conflict (Duplicate Username, Email, etc)")
})
public String selfEnroll(final String apiRequestBodyAsJson) {
AppSelfServiceUser user = this.selfServiceRegistrationWritePlatformService.selfEnroll(apiRequestBodyAsJson);
this.selfServiceRegistrationWritePlatformService.selfEnroll(apiRequestBodyAsJson);
return SelfServiceApiConstants.createRequestSuccessMessage;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the OpenAPI response description for self-enrollment.

Lines 96-99 return only SelfServiceApiConstants.createRequestSuccessMessage, but Line 89 says the endpoint returns a confirmation token. That will generate incorrect API docs for clients.

As per coding guidelines, "Maintain up-to-date API documentation via SpringDoc (OpenAPI 3) or REST Docs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceRegistrationApiResource.java`
around lines 89 - 99, Update the OpenAPI `@ApiResponse` for the selfEnroll
endpoint to accurately describe the actual response: in
SelfServiceRegistrationApiResource.selfEnroll change the 200-response
description to indicate it returns a confirmation token (e.g. "200 - OK; returns
confirmation token string") and add a content/schema entry describing a string
response (or reference java.lang.String) so the generated docs match the method
return value (which currently returns
SelfServiceApiConstants.createRequestSuccessMessage).

Comment on lines +107 to +116
@POST
@Path("client-user/confirm")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@Operation(summary = "Confirm Self Enrollment", description = "Validates the enrollment token and activates the self-service user.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "Token expired or already consumed"),
@ApiResponse(responseCode = "404", description = "Token not found")
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the confirmation request payload.

The new public confirmation endpoint has response codes, but no @RequestBody schema for the token fields. The OpenAPI spec will not tell clients whether to send externalAuthenticationToken or the legacy requestId/authenticationToken pair.

As per coding guidelines, "Maintain up-to-date API documentation via SpringDoc (OpenAPI 3) or REST Docs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/apache/fineract/selfservice/registration/api/SelfServiceRegistrationApiResource.java`
around lines 107 - 116, The POST confirmation endpoint in
SelfServiceRegistrationApiResource (path "client-user/confirm") lacks a
`@RequestBody` schema so OpenAPI clients can't tell whether to send
externalAuthenticationToken or the legacy requestId/authenticationToken pair;
add a documented request model (e.g., ConfirmSelfEnrollmentRequest DTO) or
inline `@RequestBody` with `@Schema` describing fields externalAuthenticationToken
(string) and legacy requestId and authenticationToken (strings), specify
required/oneOf semantics (externalAuthenticationToken OR the legacy pair),
annotate the controller method parameter (e.g.,
confirmEnrollment(ConfirmSelfEnrollmentRequest request)) with `@RequestBody` and
example values, and update the endpoint's Swagger annotations so the generated
OpenAPI spec clearly documents which payload to send.

- selfEnroll creates client + disabled user (enabled=false)
- Enrollment token stored in request_audit_table with type ENROLLMENT
- New POST /self/registration/client-user/confirm endpoint validates
  token and enables the user via AppSelfServiceUser.enable()
- User cannot login until enrollment is confirmed
- Token replay protection via markConsumed() + optimistic locking
- Updated enrollment + forgot-password tests for 2-step flow
- Wrapped token delivery in try-catch (swallowed for audit/retry)
- Added BEGIN/COMMIT transaction wrapper to executeSqlInPostgres()

All 30 tests pass (0 failures, 0 errors).
@DeathGun44 DeathGun44 force-pushed the MX-239-add-enrollment-confirmation-flow branch from 1d2cfcc to 4916c6a Compare April 13, 2026 18:50
@IOhacker IOhacker merged commit 8632ce4 into openMF:develop Apr 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants