Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Account verification has a credential race, and the configured signup redirect fallback is lost.
4 open findings
What changed in this PR
Updates self-service signup so activation verifies email ownership, invalidates the registration password, and redirects users through password setup.
Changes:
- Generates a reset code during account activation.
- Adds forced-password messaging and updates browser flows.
- Expands unit, MockMvc, and integration coverage.
| File | Description |
|---|---|
server/.../AccountCreationService.java |
Adds reset code to activation responses. |
server/.../AccountsController.java |
Redirects activation to password setup. |
server/.../EmailAccountCreationService.java |
Invalidates signup passwords and creates reset codes. |
server/.../ResetPasswordController.java |
Adds forced-change page messaging. |
server/.../AccountsControllerTest.java |
Updates activation redirect tests. |
server/.../EmailAccountCreationServiceTests.java |
Updates service construction and reset mocks. |
uaa/.../AccountsControllerMockMvcTests.java |
Covers the new default-zone flow. |
uaa/.../AccountsControllerMockMvcZonePathTests.java |
Covers zone-path activation flows. |
uaa/.../CreateAccountIT.java |
Updates end-to-end signup scenarios. |
uaa/.../IntegrationTestUtils.java |
Completes password setup in Selenium fixtures. |
uaa/src/main/resources/messages.properties |
Adds forced-change text. |
docs/UAA-Configuration-Reference.md |
Documents the changed signup behavior. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
838c001 to
6b46280
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Reset-code generation can fail after verification, leaving an activated account with an unknown password and no retryable activation link.
1 open finding
4 resolved since last review
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
6b46280 to
db06278
Compare
Self-service account confirmation (/verify_user) currently marks the account verified while leaving the password that was submitted on the registration form in place. Clicking the confirmation link only proves ownership of the email address; it does not prove that the clicker chose the stored password. As a result the password supplied by whoever registered the address becomes a usable credential once the owner confirms the account. Add two red MockMvc tests on the self-service flow: - confirmingAccountDoesNotAuthorizeThePreRegisteredPassword: a single registration followed by confirmation must not let the registration password authenticate. - confirmingAccountDoesNotAuthorizePasswordFromEarlierRegistration: when the address is registered twice before confirmation, the earlier password must not authenticate after the owner confirms via the most recent link. Both tests fail today, demonstrating the gap. The following commit closes it.
Self-service registration stored the password from the sign-up form and activated it the moment the account was confirmed. Because confirming the activation link only proves ownership of the email address — not that the person confirming chose the stored password — a password set before the address was verified could end up authorizing access to the confirmed account. Establish the password at confirmation instead: - EmailAccountCreationService.completeActivation now verifies the account, discards the stored password by replacing it with an unguessable random value, flags the account as requiring a password change, and issues an ownership-bound, single-use reset code for the user. - AccountsController redirects /verify_user to the reset-password page (with force_change=true) so the confirming owner sets their own password through the same code-authenticated flow used for password recovery; no previous password is required. - ResetPasswordController surfaces a "password change required due to changes in the system" notice on that page (new reset_password.force_change message). A setting-independent consequence: the password typed on the sign-up form is never usable to authenticate; only the owner who confirms via the emailed link can set the account's password. Existing self-service tests are updated to complete the new confirm-then-set-password flow, and the configuration reference documents the behavior. The security tests added in the previous commit now pass.
The confirmation flow now carries the resolved signup redirect (including the client's signup_redirect_url fallback) into the reset code, so the owner is returned to it after setting their password. Previously only an explicit, registered redirect_uri survived; the signup_redirect_url fallback was lost and the owner landed on "home". completeActivation now passes the resolved redirect location (from getRedirect, which honours signup_redirect_url) to forgotPassword instead of the raw redirect_uri. The reset flow re-matches it against the client's registered redirect URIs before applying it. Restores the redirect assertions in the self-service tests for both the explicit and fallback client-redirect cases. test: cover confirmation password/redirect behavior end to end Add coverage for the self-service confirmation changes: - EmailAccountCreationServiceTests: assert completeActivation discards the registration-form password (replacing it with a different value), marks the account as requiring a password change, and issues an ownership-bound reset code. - CreateAccountIT: end-to-end test confirming that the registration-form password cannot sign in after confirmation, and that only the password the owner sets through the link works. Also completes the configuration-reference note to describe that a configured client/signup redirect is applied after the owner sets their password.
completeActivation verified the account before replacing the registration password and setting the change-required flag. Because these are separate provisioning updates, a concurrent login in the gap could authenticate with the registration password against the already-verified account. Replace the password and set the flag while the account is still unverified, then verify it last (version -1, matching the password-reset flow), so the account is never simultaneously verified and holding the registration password. Add an ordering assertion to the service test to lock this in. Addresses PR review feedback.
…gistered redirect rules Two behaviors, both covered at service, controller and full-flow level: - When the client has no change-email redirect URL, completeVerification returns the requested URL unchanged even if it matches none of the client's registered redirect URIs. The tests expect no redirect in that case, for a client without registered URIs, for a requested URL that matches none, and for no requested URL, plus a guard that a matching requested URL is still returned. - For a user who is not signed in, the verification redirect to the login page appends the redirect URL to the query string as is, so a URL containing '&' or '#' is split into extra parameters or a fragment. The tests expect the URL to arrive as the single decoded value of form_redirect_uri. The existing controller test that pinned the raw string now checks the decoded value, so it holds for either encoding. Full-flow MockMvc tests run in the default zone and, per zone resolution mode, in a created zone. These tests fail until the production changes follow. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…istered redirect rules - completeVerification returns no redirect when the client has no change-email redirect URL and the requested URL matches none of the client's registered redirect URIs, so the controller falls back to the profile or login page. - The login redirect for a user who is not signed in carries the redirect URL as the single encoded value of form_redirect_uri, as the LDAP invitation path already does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
020b13e to
472c9ce
Compare
…ernal login, unless a fresh invitee When a user accepts an invitation through an external identity provider, the email in the response is compared with the invited email. - SAML: a response without an email is accepted for any invited user. Add tests that expect the login to be rejected, with a message for each reason, when the invited user is already verified and when the invited user's username differs from the invited email, and that the stored user is left unchanged. Add a guard that a fresh invitee (unverified, username equal to the email) can still accept without an email. The two rejection tests fail until the production change follows. - OIDC: a token without an email claim gets an email generated from the username claim, which does not match the invited email. Add a test that pins this rejection and that no invitation event is published. It passes today. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…less a fresh invitee When the SAML response carries no email, the invited user's own email was filled in for every invited user. Do that only for a fresh invitee (unverified, username equal to the email). An invited user who is already verified, or whose username differs from the invited email, is rejected with a message naming the reason, and the stored user is left unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…in path The controller serves /force_password_change and /force_password_change/, but the filter that guards the page only recognises the plain path. - A signed-in user with no password change pending who requests the page with a trailing slash is not redirected away, and a submit to that path replaces the password. Add filter tests for GET and POST, and MockMvc tests (default zone and per zone resolution mode) that submit to each path and check the old password still signs in. The plain path passes today; the trailing-slash path fails. - A user whose password change is pending and who requests the trailing-slash path is redirected to the plain path instead of being let through. Add filter tests that expect the request to pass through. These tests fail until the production change follows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n path The filter that guards the password change page now recognises both paths the controller serves, /force_password_change and /force_password_change/. A signed-in user with no password change pending is redirected away from either, and a user whose change is pending is let through on either. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The password change page is guarded by a filter, but the controller itself accepts a submit whether or not a change is pending for the session. Add a test that expects a submit with no pending change to be redirected to the home page without changing the password, for both paths the controller serves. The existing submit tests now mark the change as pending in the session, as a real flow does. This test fails until the production change follows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The controller now checks that a password change is pending for the session before it accepts a submit, and otherwise redirects to the home page without changing the password. The filter in front of the page already does the same; this keeps the controller correct on its own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
This PR exceeds the recommended size of 1000 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size. |
…ect parameter After an invitation is accepted, the redirect URL stored with the invitation is appended to the login redirect as is. A URL with a query string or a fragment, which a client with a wildcard redirect pattern allows, is split into extra parameters or a fragment instead of arriving as the single value of form_redirect_uri. Add controller tests (default and per zone request path mode) and an end-to-end test with a client that has a wildcard redirect pattern. They expect the URL to arrive as the single decoded value of form_redirect_uri. The two existing end-to-end checks that pinned the raw string now check the decoded value, so they hold for either encoding. These tests fail until the production change follows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rect parameter The redirect URL stored with the invitation is now URL-encoded when it is appended to the login redirect, as the LDAP invitation path already does, so a URL with a query string or a fragment arrives as the single value of form_redirect_uri. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rect parameter After a password is set through the reset flow, which the self-service signup confirmation also uses, the redirect URL carried with the reset code is appended to the login redirect as is. A URL with a query string or a fragment is split into extra parameters or a fragment instead of arriving as the single value of form_redirect_uri. Add a filter test, signup-flow tests (default zone and the zone path variant) and a browser based test with a client that has a wildcard redirect pattern. They expect the URL to arrive as the single decoded value of form_redirect_uri. The existing checks that pinned the raw string, including the existing browser based one, now check the decoded value, so they hold for either encoding. These tests fail until the production change follows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rameter The redirect URL carried with the reset code is now URL-encoded when it is appended to the login redirect, as the LDAP invitation path already does, so a URL with a query string or a fragment arrives as the single value of form_redirect_uri. This covers the redirect after a self-service signup is confirmed and the password is set. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When a user accepts an invitation by signing in with LDAP credentials, the invitation code is re-issued, and the re-issued code is handed to the acceptance step. It is created without an intent, although the code it replaces has one, and unlike the other places in this controller that re-issue the code. Add tests, for the default request path and per zone request path mode, that expect the code re-issued on a successful acceptance, and both codes re-issued when the authenticated email does not match the invited email, to carry the intent of the code they replace. The existing tests, which use a code without an intent, are unchanged. These tests fail until the production change follows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When a user accepts an invitation by signing in with LDAP credentials, the invitation code is re-issued without an intent, so the code handed to the acceptance step no longer says what it was issued for. Re-issue it with the intent of the code it replaces, in the successful acceptance and in the retry after an email mismatch, as the other places in this controller already do. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /Codes now has tests that expect an intent to be given, with a clear 400 message when it is missing. Any given intent is stored as supplied. Existing POST /Codes tests now send an intent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /Codes returns a 400 with a clear message when the intent is missing or blank, and stores the given intent. Codes already stored without an intent stay redeemable; redeeming one is logged at debug level. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds the endpoint to UAA-APIs.rst and the API documentation, with a REST Docs test that generates the snippets. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A code whose data parses as passcode information is only accepted when its intent is PASSCODE followed by the user id. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…k or a rejected password Opening the reset link twice keeps showing the form, and neither opening the link nor a rejected password gives the code a later expiry. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… or a rejected password Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A verified user gets no invitation code and no link; users who are not verified yet are invited as before. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, and the confirmation page shows the address An address that already has a verified account gets the same redirect as a new address and the owner is emailed instead; the signup confirmation page shows the address the email was sent to. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… and the confirmation page shows the address Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r that can no longer be reached Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ady has an account Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
8bb5db4 to
35558ee
Compare


Summary
Self-service account confirmation now requires the owner to set their password
through the activation link. Previously the password typed on the sign-up form was
activated as soon as the account was confirmed — but confirming the link only proves
ownership of the email address, not that the person confirming chose that password.
Change
On confirmation,
completeActivationnow:password can be used to sign in,
/verify_userthen sends the owner to the reset-password page (showing "A passwordchange is required due to changes in the system.") to set their own password via the
existing code-authenticated flow.
Behavior changes
password is never used to sign in.
then login once the password is set.
redirect_uriis still honored; thesignup_redirect_urlfallback currently lands on
home(see open item).Tests
(
server,uaa);integrationTestto run in CI.Open item
signup_redirect_urlfallback redirect and its test assertions.