Skip to content

self service improvement - #4100

Merged
fhanik merged 10 commits into
developfrom
pr/self-service-improvement
Nov 7, 2026
Merged

fhanik merged 10 commits into
developfrom
pr/self-service-improvement

Conversation

@fhanik

@fhanik fhanik commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Self-service signup, invitations, redirects and expiring codes

This pull request changes how a self-service signup is confirmed, how signup and email change answer for an address that
already has an account, how zone path requests are matched to a host, and how several flows handle redirects and
expiring codes. It adds one configuration property, scim.invite.allow_reinvite, which keeps today's behavior by
default.

Read before upgrading

Changes that need a check or an action

# What changes Who notices What to do
1 Self-service signup confirmation. Confirming the email address no longer activates the password entered on the signup form. After the confirmation link, the user lands on /reset_password?code=…&force_change=true and sets a password there. A signup redirect (redirect_uri or the client's signup_redirect_url) is applied after the password is set. Accounts confirmed through GET /Users/{id}/verify-link are not affected: the password set by the caller keeps working and the user is sent to /login?success=verify_success. A link issued before the upgrade carries no marker for this and follows the new path for its one-hour lifetime Automation or tests that sign up, confirm and then log in with the signup password Set the password through the reset page after confirming
2 Signup and email change for an address that already has an account. POST /create_account.do and POST /change_email.do no longer answer 422 with username_exists. Both go to the "email sent" page, as for a new address, and the owner of the address receives a "You already have an account" email instead of an activation or verification link UI tests or clients that look for the 422 or its message Update such tests; nothing to configure
3 Zone path requests need a recognized host. With zones.paths.enabled: true, a request to /z/{subdomain}/… whose host is not recognized is answered with 404 (zone.not.found, body Cannot find identity zone, unrecognized hostname: <hostname>) instead of being served for the zone in the path. Recognized hosts: the host of uaa.url, the host of login.url, localhost, and every entry of zones.internal.hostnames. A setup that lists only localhost still accepts any host Deployments that serve zone paths through a hostname that is not listed, for example a proxy or an internal service name Add every hostname that serves zone path requests to zones.internal.hostnames
4 POST /Codes requires an intent. A request without intent, or with a blank one, is answered with 400 and intent is required. Known intents: EMAIL, INVITATION, REGISTRATION; custom values are accepted. Any other value is stored as given. Codes that already exist without an intent keep working until they expire. Documented in docs/UAA-APIs.rst and in the generated API guide (section "Expiring Codes") Callers of POST /Codes that send no intent Send an intent
5 Passcode login checks the code's intent. A passcode must carry the intent that the passcode endpoint stores (PASSCODE <user id>); any other code is refused with Invalid passcode Only codes created through POST /Codes and then used as a passcode Create passcodes through the passcode endpoint
6 Password reset links keep their lifetime. Opening a reset link, or submitting a rejected password, no longer extends the code. Opening the link a second time shows the form instead of an error Users who keep a reset link open for a long time None

Behavior changes in narrow cases

  • Email change redirect. After an email change is verified, the user is redirected to the requested redirect_uri
    only when it matches one of the client's registered redirect URIs, or to the client's change_email_redirect_url.
    Otherwise the user goes to the profile page, or to the login page when signed out. To keep a redirect, register the
    URI or set change_email_redirect_url on the client.
  • Invitation acceptance through SAML. The SAML response must carry an email that matches the invited address. A
    response without an email is accepted only when the invited account is still unverified and its username equals the
    invited email, which is the state of an account created by an invitation that has not been accepted yet. UAA does not
    record whether the invitation created the account, so an existing account in the same state is accepted too. Other
    cases are refused with a message that names the reason. Map the email attribute at the identity provider if it does
    not release one: every acceptance is then compared with the invited address.
  • Verification links issued before the upgrade. A link from GET /Users/{id}/verify-link issued before the upgrade
    has no marker saying that the caller set the password. For its one-hour lifetime it follows the new signup path: the
    user sets a new password. For a user of another origin it only verifies the user, as before. After the upgrade, new
    links are not affected.
  • form_redirect_uri is URL-encoded. The URL carried to /login after an email change, an invitation acceptance or
    a password reset is percent-encoded, so a URL with & or # arrives as one parameter. The decoded value is unchanged.
  • Password change page. /force_password_change/ is handled like /force_password_change, and a password
    change submit is refused (redirect to /) when no password change is pending for the session.
  • LDAP invitation acceptance. The invitation code issued again during LDAP acceptance keeps the intent of the
    original code. No visible change.

New configuration

  • scim.invite.allow_reinvite (uaa.yml, global), default true: no change. With false, POST /invite_users
    lists a user who is already verified under failed_invites (error code user.already-verified, message
    The user is already verified.), with no user id and no link, when the token has scim.invite but may not read users
    (neither scim.read nor the zone administrator scope). A token that may read users gets the same response as today.
    Users who are not verified yet and new users are always invited, so an expired invitation can be sent again. Before
    setting false, give scim.read to any client that holds scim.invite, re-invites existing users and relies on the
    user id or the link in the response.

Customizations to check

  • New mail template account_exists.html (the "You already have an account" email).
  • The signup "email sent" page now names the address: new message key email_sent.activation_address
    (Please check {0} for an activation link.).
  • Removed message key email_change.username_exists; it is no longer used.
  • After a self-service confirmation, the user sees the password reset page and its messages instead of
    login.verify_success. login.verify_success is still shown after an account is confirmed through
    GET /Users/{id}/verify-link.

Known condition

  • Two activation links opened at the same moment. When an address was registered twice before it was confirmed,
    two activation links exist. Opening a second link after the first one was used changes nothing (the password the owner
    set stays). If both links are opened within the same few milliseconds, the requests can overlap: both replace the
    registration password and the later one invalidates the reset link of the earlier one, so one of the two pages reports
    that the link is no longer valid. The owner then uses "Forgot password". Replacing a password the owner has already
    set would need one request to stall for as long as the owner takes to type and submit a new password. Making the
    transition atomic would need a conditional update across the password, the change-required flag and verified; it is
    not part of this change.

Java API

  • EmailAccountCreationService takes a ResetPasswordService constructor argument. Spring wiring is updated; only code
    that builds this bean by hand needs a change.
  • ScimUtils.getExpiringCode has an overload that takes extra entries for the code data.

Commits

Commit Contents
9b2013ead fix: confirming a self-service signup leads the user to set a password Item 1 (self-service part)
03313203e fix: redirects after email change, invitation acceptance and password reset Email change redirect, form_redirect_uri encoding
d5ace057f fix: expiring code intents and reset code lifetime Items 4, 5 and 6, docs for POST /Codes
9fb964055 fix: signup and email change for an existing address, and verification links from the user API Item 2, the verify-link part of item 1, the reset code on AccountCreationResponse is no longer serialized, docs
b42f54825 fix: password change page guard and zone path request hosts Item 3, password change page, docs
d755d0457 feat: invitations: email for external acceptance, LDAP code intent, scim.invite.allow_reinvite Invitation acceptance through SAML, LDAP invitation code, scim.invite.allow_reinvite, docs
73f2c1d7e4 test: another activation link, an unregistered signup redirect and a reset code expiring during a submit Tests for the next commit
df2c742b70 fix: another activation link, an unregistered signup redirect and a reset code expiring during a submit Another activation link keeps the owner's password; signup_redirect_url applies after the password is set; an expired reset code during a rejected submit shows the invalid code page; docs
2c30db62e3 fix: opening an old activation code for a user of another origin only verifies the user Old verification links for users of another origin; tests

Documentation

  • docs/UAA-Configuration-Reference.md: scim.invite.allow_reinvite; zone paths and zones.internal.hostnames;
    self-service confirmation and addresses that already have an account (under login.selfServiceLinksEnabled); the
    client attributes signup_redirect_url and change_email_redirect_url (under oauth.clients).
  • docs/UAA-APIs.rst and the generated API guide (CodeStoreEndpointDocs, section "Expiring Codes"): POST /Codes and its
    intent; redirect_url of POST /email_changes;
    invitations accepted through an external identity provider; failed_invites with scim.invite.allow_reinvite: false.

Testing

  • Every behavior change has tests that failed before the change and pass after it: unit tests, and MockMvc tests through
    the full filter chain, in the default zone and in a non-default zone in both zone resolution modes (subdomain and zone
    path) where the area has those tests.
  • Integration tests updated for the new behavior: CreateAccountIT (confirmation page text), ResetPasswordIT,
    InvitationsIT (POST /Codes with an intent), and the shared signup helper in IntegrationTestUtils.
  • ./gradlew clean test integrationTest --continue on the final tree: unit tests 5578 (server) and 3074 (uaa), integration
    tests 3 (server) and 378 (uaa), no failures.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

@fhanik
fhanik force-pushed the pr/self-service-improvement branch from 838c001 to 6b46280 Compare October 7, 2026 21:00
@fhanik
fhanik marked this pull request as ready for review October 7, 2026 21:29
@strehle
strehle requested a balanced review from Copilot October 8, 2026 14:09
@fhanik
fhanik marked this pull request as draft October 8, 2026 14:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

@fhanik
fhanik force-pushed the pr/self-service-improvement branch from 6b46280 to db06278 Compare October 22, 2026 13:33
@github-actions github-actions Bot added size/l and removed size/m labels Oct 22, 2026
@fhanik
fhanik force-pushed the pr/self-service-improvement branch from 020b13e to 472c9ce Compare October 22, 2026 16:08
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown

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.

@fhanik
fhanik force-pushed the pr/self-service-improvement branch 6 times, most recently from ca187f8 to f929bf5 Compare October 28, 2026 22:17
Confirming the email address of a self-service signup no longer activates the
password entered on the signup form. The account is verified after that
password is replaced, and the user is sent to the password reset page
(force_change=true) to set a password. A signup redirect (an explicit
redirect_uri or the client's signup_redirect_url) is applied after the password
is set.
… reset

- Email change verification only redirects to a requested redirect_uri that
  matches a registered redirect URI of the client, or to the client's
  change_email_redirect_url; otherwise the user goes to the profile page (or
  the login page when signed out).
- The URL carried to /login as form_redirect_uri is URL-encoded after email
  change, invitation acceptance and password reset, so it arrives as one
  parameter.
- POST /Codes requires a non-blank intent and stores it as given; a missing or
  blank intent is answered with 400. Documented in UAA-APIs.rst and the API
  docs.
- Passcode login requires the intent the passcode endpoint stores
  ("PASSCODE <user id>").
- Opening a password reset link, or submitting a rejected password, no longer
  extends the lifetime of the reset code; opening the link again shows the
  form.
…n links from the user API

- POST /create_account.do and POST /change_email.do answer the same way for an
  address that already has an account as for a new address. The owner of the
  address receives an email (account_exists.html) instead of an activation or
  verification link. The signup confirmation page names the address the email
  was sent to.
- The duplicate-email handling in ChangeEmailController that can no longer be
  reached and the email_change.username_exists message are removed.
- A link from GET /Users/{id}/verify-link only verifies the user: the password
  set by the caller keeps working, and the user is redirected to
  /login?success=verify_success.
- The password reset code on AccountCreationResponse is not serialized.
- Docs: login.selfServiceLinksEnabled in UAA-Configuration-Reference.md.
- The password change page guard treats /force_password_change/ like
  /force_password_change, and a password change submit is refused when no
  change is pending.
- A zone path request (/z/{subdomain}/...) must come from a recognized host,
  like any other request; otherwise it is answered with 404, and the message
  names the hostname.
- Docs: zones.paths.enabled and zones.internal.hostnames in
  UAA-Configuration-Reference.md.
…cim.invite.allow_reinvite

- An invited user who accepts through SAML must supply an email that matches
  the invited address, unless the user is a fresh invitee (unverified, username
  equal to the email).
- The invitation code re-issued during LDAP acceptance keeps its intent.
- New property scim.invite.allow_reinvite (default true, unchanged behavior).
  With false, POST /invite_users reports a verified user under failed_invites
  (user.already-verified) when the caller holds scim.invite but cannot read
  users (scim.read or zone admin).
- Docs: invitations accepted through an external identity provider, the client
  attributes signup_redirect_url and change_email_redirect_url, and the new
  property.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved activation-link and SAML invitation vulnerabilities can overwrite users’ passwords or bind an existing account to an unrelated identity.

5 open issues
1 resolved since last review

Review effort: Balanced

fhanik added a commit that referenced this pull request Nov 3, 2026
… of backwards compatibility to minimize breaking changes.

#4100 (review)
…reset code expiring during a submit

Opening another activation link for an account that is already confirmed
keeps the password the owner has set and asks for no further change.

A client's signup_redirect_url applies after the owner sets a password,
also when it is not one of the client's registered redirect URIs. Any
other unregistered redirect still falls back to home.

A reset password submit that fails validation after the code has expired
is reported as an invalid code, instead of failing while the code is
issued again.

#4100 (review)
…eset code expiring during a submit

Completing activation for an account that is already verified changes
nothing: no new password, no password change flag and no reset code. The
user is sent to the login page with the verify_success message.

Resetting a password follows the stored redirect when it is the client's
signup_redirect_url, which an administrator sets on the client, also when
it is not a registered redirect URI. Any other redirect still has to match
a registered redirect URI.

When a reset password submit fails validation and the code has expired
meanwhile, the user is sent to the forgot password page with the invalid
code message, instead of an error while the code is issued again.

Docs: the SAML part of the invitation acceptance note says what UAA checks
when the assertion has no email attribute, and that mapping the email
attribute compares every acceptance with the invited address. The
self-service confirmation note describes the signup redirect fallback and
opening another activation link. The two tests for SAML acceptance without
an email are dropped, as that behavior stays unchanged.

The account MockMvc tests also run in another zone (subdomain and zone
path). createTestClient takes the signup redirect as a parameter.
@fhanik
fhanik force-pushed the pr/self-service-improvement branch from a4c8466 to df2c742 Compare November 4, 2026 17:24
@duanemay
duanemay requested a balanced review from Copilot November 5, 2026 15:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because an unpublished critical activation race could overwrite the owner’s newly established password and invalidate another request’s setup link.

No open issues 🎉

5 resolved since last review

Review effort: Balanced

@strehle
strehle requested review from duanemay and a lite review from Copilot November 6, 2026 07:27

@strehle strehle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@fhanik pls check my commit mainly from local review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The change still has active correctness gaps: a null request body can produce an unintended 500, and the normal invitation POST does not enforce the documented intent check.

2 open issues

Review effort: Lite

@strehle
strehle force-pushed the pr/self-service-improvement branch from b00db24 to d16758a Compare November 6, 2026 09:48
@strehle
strehle requested a lite review from Copilot November 6, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The documentation contains three active inconsistencies with the implemented API behavior, including an expired example timestamp, incorrect client_id requirements, and incomplete redirect URI behavior.

4 open issues
1 resolved since last review

Review effort: Lite

Comment thread docs/UAA-APIs.rst Resolved
Comment thread docs/UAA-APIs.rst Resolved
Comment thread docs/UAA-APIs.rst Resolved
@strehle
strehle force-pushed the pr/self-service-improvement branch from d16758a to e5f9bf2 Compare November 6, 2026 10:19
@strehle
strehle requested a lite review from Copilot November 6, 2026 10:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The PR spans security-sensitive authentication, invitation, password-reset, redirect, and zone-routing changes that require unexecuted full integration and real-provider callback validation, including rolling-upgrade coverage.

No open issues 🎉

4 resolved since last review

Review effort: Lite

strehle
strehle previously approved these changes Nov 6, 2026
@github-project-automation github-project-automation Bot moved this from Inbox to Pending Merge | Prioritized in Foundational Infrastructure Working Group Nov 6, 2026
… verifies the user

An activation code issued before codes carried the user_provisioned marker
takes the self-service path. That path ends in a reset code that is looked
up by username in the uaa origin, so for a user of another origin it could
be the reset code of a different account with the same username.

Completing activation for a user whose origin is not uaa now only verifies
the user, as it did before the self-service path existed: no password
change and no reset code.
@fhanik
fhanik merged commit 543c1ba into develop Nov 7, 2026
27 of 28 checks passed
@fhanik
fhanik deleted the pr/self-service-improvement branch November 7, 2026 00:11
@github-project-automation github-project-automation Bot moved this from Pending Merge | Prioritized to Done in Foundational Infrastructure Working Group Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Development

Successfully merging this pull request may close these issues.

4 participants