Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions docs/UAA-APIs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,10 @@ Change Email Flow:
"email": "example@newemail.com"
}

* ``redirect_url`` is the requested ``redirect_uri`` when it matches one of the client's registered redirect URIs.
Otherwise, it is the value of the client's ``change_email_redirect_url`` attribute, or ``null`` when the client has
no such attribute or no client was given.

* Response Codes::

200 - OK
Expand All @@ -2136,6 +2140,55 @@ Change Email Flow:
$ curl 'http://localhost:8080/email_changes' -i -X POST -H 'Content-Type: application/json' -d 6FfI4o


Create a Code: ``POST /Codes``
------------------------------

Stores a one time code and returns it. Codes are stored per identity zone and can be read back once with ``GET /Codes/{code}``.

* Request: ``POST /Codes``

* Authorization: a token with the scope ``oauth.login`` or a UAA admin token.

* Request Body::

{
"data": "{\"user_id\":\"a-user-id\"}",
"expiresAt": 4102444800000,
"intent": "MY_APPLICATION_CODE"
}

``data`` (required) is stored with the code, as a string. ``expiresAt`` (required) is milliseconds since the epoch and must be in the future.
``intent`` (required, not blank) says what the code may be used for.

* Intents:

The UAA flows accept a code only when its intent matches the flow, and the match is case sensitive:

* ``EMAIL``: email change verification (``POST /email_changes`` and ``/verify_email``)
* ``INVITATION``: invitation acceptance (``/invitations/accept``)
* ``REGISTRATION``: account activation (``/verify_user``)

Any other value is stored as given; use it for codes that your own application creates and redeems.
Intents that the UAA creates for its own flows (for example the ones for password reset, autologin and passcodes) are also stored as given.
Codes stored without an intent before this requirement was added remain redeemable until they expire.

* Response Codes::

201 - Created
400 - Bad Request (the intent is missing or blank, data or expiresAt are missing, or expiresAt is in the past)
401 - Unauthorized
403 - Forbidden

* Error message for a missing intent::

intent is required. Known intents: EMAIL, INVITATION, REGISTRATION; custom values are accepted.

* Example CURL ::

$ curl 'http://localhost:8080/Codes' -i -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer <token>' \
-d '{"data":"{}","expiresAt":4102444800000,"intent":"MY_APPLICATION_CODE"}'


Verify User Links: ``GET /Users/{id}/verify-link``
--------------------------------------------------

Expand All @@ -2144,8 +2197,8 @@ Verify User Links: ``GET /Users/{id}/verify-link``

* Request Parameters::

client_id; the id of the client requesting the verification link (optional)
redirect_uri; the eventual URI that will be redirected after the user finishes resetting their password (optional)
client_id; the id of the client requesting the verification link (optional; defaults to the client id from the OAuth2 bearer token when available)
redirect_uri; the eventual URI that will be redirected after the user finishes resetting their password (required)
the redirect_uri whitelist that can be set per client accepts wildcards, and matches against this redirect_uri using Ant-style path pattern matching (as per <http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html>)

* Request Headers: Authorization header containing an `OAuth2`_ bearer token with::
Expand Down Expand Up @@ -2353,6 +2406,28 @@ The redirect_uri will be validated against allowed redirect_uri for the client.
"failed_invites":[]
}

.. note::

**Verified users and** ``scim.invite.allow_reinvite``

By default an email address that belongs to a verified user is invited like any other address: the response
contains the user's ``userId`` and a new ``inviteLink``. When the server sets ``scim.invite.allow_reinvite: false``
and the token has ``scim.invite`` but may not read users (``GET /Users`` would refuse it, for example because it has
neither ``scim.read`` nor the zone administrator scope), the address is reported in ``failed_invites`` instead. No
user id or link is returned and no invitation is created::

{
"new_invites":[],
"failed_invites":[
{"email":"user1@cqv4f7.com","userId":null,"origin":null,"success":false,"errorCode":"user.already-verified","errorMessage":"The user is already verified.","inviteLink":null}
]
}

The other addresses of the request are processed as usual, and users who are not verified yet, as well as new
users, are always invited. A client that re-invites existing users and needs the user id or the link should hold
``scim.read`` before the property is set to ``false``. See ``scim.invite.allow_reinvite`` in
``docs/UAA-Configuration-Reference.md``.

.. note::

**Existing unverified UAA-origin users and password handling**
Expand All @@ -2372,6 +2447,26 @@ The redirect_uri will be validated against allowed redirect_uri for the client.
Newly created users (first-time invitations where the account did not exist
before) are unaffected: the supplied password is set normally.

.. note::

**Accepting an invitation through an external identity provider**

When the invited user accepts the invitation by signing in at a SAML or OIDC provider, the response of the provider
must carry an email address, and it must match the invited email address (the comparison ignores case). A sign-in that
carries a different address is rejected.

* SAML: if the assertion has no email attribute, there is no address to compare. UAA then accepts the sign-in only
when the invited account is still unverified and its username equals the invited email address. That is the state
of an account created by an invitation that has not been accepted yet. Any other account is rejected: one that is
verified, or one whose username differs from its email address.

UAA does not record whether the invitation created the account, so an existing account in the same state is
accepted too. UAA marks a SAML user verified only when the provider's ``email_verified`` attribute says so. To have
every acceptance compared with the invited address, map the ``email`` attribute for the provider.
* OIDC: if the token has no email claim, UAA derives an address from the username claim. That address has to match
the invited email address as well, so a token without an email claim only works when the username claim equals the
invited email address.



Group Management APIs
Expand Down
72 changes: 71 additions & 1 deletion docs/UAA-Configuration-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ or `$CLOUDFOUNDRY_CONFIG_PATH/uaa.yml`.
| [↪](#scimuserids_enabled) `scim.userids_enabled` | `true`| Enable /ids/Users endpoint|
| [↪](#scimuseroverride) `scim.user.override` | `false`| Override existing bootstrap users|
| [↪](#scimdeletedeactivate) `scim.delete.deactivate` | `false`| Deactivate instead of delete|
| [↪](#sciminviteallow_reinvite) `scim.invite.allow_reinvite` | `true`| Invite verified users for any `scim.invite` caller|
| [↪](#usermaxcount) `userMaxCount` | `500`| Max users returned by list endpoint|
| [↪](#groupmaxcount) `groupMaxCount` | `500`| Max groups returned by list endpoint|
| [↪](#deleteusers) `delete.users` | — (null)| Users to delete on bootstrap|
Expand Down Expand Up @@ -1184,6 +1185,16 @@ Bootstrap client definitions loaded at startup. Each entry is keyed by client ID
- `jwks` — Inline JWKS for private_key_jwt auth
- `jwks_uri` — URI to fetch JWKS
- `name` — Display name
- `signup_redirect_url` — Where a user is sent after confirming a self-service signup and setting a password, when the
signup request carries no `redirect_uri` that matches one of the client's registered redirect URIs. Without it the
default is the UAA home page.
- `change_email_redirect_url` — Where a user is sent after verifying an email change, when the verification link
carries no `redirect_uri` that matches one of the client's registered redirect URIs. Without it the user is sent to
the profile page (or the login page when signed out). `POST /email_changes` returns this value as `redirect_url`.

`signup_redirect_url` and `change_email_redirect_url` are stored as additional information of the client. They are
keys of the client definition (see `scripts/boot/uaa.yml`) and top-level attributes of the client in the client
registration API.

[Back to table](#oauth-clients--users)

Expand Down Expand Up @@ -1848,6 +1859,37 @@ of permanently deleting the record.

---

### `scim.invite.allow_reinvite`

**Default:** `true`
**Source:** `@Value("${scim.invite.allow_reinvite:true}")` in [`InvitationsEndpoint`](../server/src/main/java/org/cloudfoundry/identity/uaa/invitations/InvitationsEndpoint.java)
**Type:** `boolean`

Controls what `POST /invite_users` returns for an email address that belongs to a user who is already verified.

- `true` (default): the user is invited like any other user. The response lists the address under `new_invites` with
the user's `userId` and a new `inviteLink`.
- `false`: when the token has `scim.invite` but may not read users, that is when `GET /Users` would refuse it (for
example it has neither `scim.read` nor the zone administrator scope), the address is listed under `failed_invites`
with the error code `user.already-verified` and the message `The user is already verified.`. No user id and no link
are returned, and no invitation code is created. A token that may read users still gets the user id and the link.

With either value, users who are not verified yet and new users are invited as before, so an expired invitation can
always be sent again, and the other addresses of a batch are processed as usual.

Before setting `false`, check the clients that call `/invite_users`. A client that holds `scim.invite` without
`scim.read`, re-invites existing users and relies on the user id or the link in the response (for example to assign
permissions or to send the link itself) receives `failed_invites` for verified users. Give such a client `scim.read`,
or leave the property at `true`.

Which users are verified: users created with `POST /Users` are verified unless the request says otherwise, users of
the `uaa` origin become verified when they accept an invitation, and users of an external origin are verified only
when their identity provider says so (for example the `email_verified` claim of an OpenID Connect provider).

[Back to table](#scim-user-provisioning)

---

### `userMaxCount`

**Default:** `500`
Expand Down Expand Up @@ -1892,6 +1934,26 @@ List of usernames to delete on bootstrap. Used to remove specific users during s

When `true`, "Create Account" and "Forgot Password" links are displayed on the login page.

> **Note on self-service account confirmation.** Clicking the activation link in the confirmation
> email verifies ownership of the email address but does **not** activate the password typed on the
> registration form. The owner is sent to a password-setup page (stating that a password change is
> required due to changes in the system) and must establish their own password through the link
> before the account can be used to sign in. This ensures a password chosen before the email address
> was verified can never be used to authenticate. The redirect configured for the registration is
> applied after the owner has set their password: an explicit `redirect_uri` when it matches one of the
> client's registered redirect URIs, otherwise the client's `signup_redirect_url`, which does not have
> to be a registered redirect URI. Opening another activation link for an account that is already
> confirmed changes nothing: the password stays as it is and the user is sent to the login page.
>
> **Note on addresses that already have an account.** The registration form (`/create_account.do`) and
> the change email form (`/change_email.do`) answer the same way whether or not the address already
> has a verified account in the zone's `uaa` origin: the user is sent to the "email sent" page, and no
> `422` response or "There is already a user with that username" message is shown. After a
> registration the page names the address the email was sent to. When the address already has an
> account, no activation or verification link is created. The owner of the address instead receives an
> email stating that the address already has an account and that nothing has changed. Clients that
> parsed the previous `422` page of these two forms must be updated.

[Back to table](#login--branding)

---
Expand Down Expand Up @@ -3296,6 +3358,13 @@ URL paths instead of different subdomains.
metadata generation in non-default zones, as the zone information is already encoded in the URL path. See the
`login.entityBaseURL` documentation for details on how this affects SAML endpoint URLs.

**Required since 2026-10-26:** A zone path request must come from a recognized host, like any other request. The
hostname must be the host of [`uaa.url`](#uaaurl), the host of [`login.url`](#loginurl), `localhost`, or one of the
[`zones.internal.hostnames`](#zonesinternalhostnames); otherwise the request is answered with a `404` before it reaches
the zone. Earlier versions accepted any hostname for a zone path request. Deployments that reach zone paths through a
hostname that is not yet listed must add it to `zones.internal.hostnames`. When only `localhost` is configured, any
hostname is accepted.

[Back to table](#zone-paths)

---
Expand All @@ -3308,7 +3377,8 @@ metadata generation in non-default zones, as the zone information is already enc

A comprehensive list of hostnames that route to the UAA default zone. The UAA uses these
to distinguish zone subdomains. If a request's hostname is not in this list and has extra
subdomain levels, it is treated as a zone subdomain.
subdomain levels, it is treated as a zone subdomain. A hostname that matches none of the
configured root domains is not served, including for zone path requests (`/z/{subdomain}/`) since 2026-10-26.

```yaml
zones:
Expand Down
14 changes: 12 additions & 2 deletions docs/zone-path-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ This is useful in deployments where wildcard DNS or wildcard TLS certificates ar

The refactoring into `resolveZoneBySubdomain()`, `handleZoneNotFound()`, and `doFilterWithZone()` is a readability improvement that also makes each responsibility independently testable.

**Default zone path (`/z/default/`):** `ZonePathContextRewritingFilter` does **not** set `ZONE_SUBDOMAIN_FROM_PATH` for `/z/default/` requests. This means `resolveEffectiveSubdomain()` sees `null` for the path attribute and falls through to hostname-based resolution, which naturally resolves to the default zone. No special-case "default" handling is needed in this filter.
**Default zone path (`/z/default/`):** `ZonePathContextRewritingFilter` sets
`ZONE_SUBDOMAIN_FROM_PATH` to the empty string for `/z/default/` requests. This
explicitly selects the default zone. A tenant hostname combined with
`/z/default/` is rejected as ambiguous instead of allowing the hostname to
override the path.

### 2.2 `OpenIdConnectEndpoints.java`

Expand Down Expand Up @@ -131,7 +135,13 @@ The outermost filter in the chain (`Ordered.HIGHEST_PRECEDENCE + 1`). For reques
- **Rewrites the request** by wrapping it in `ZonePathRewrittenRequest` (a private `HttpServletRequestWrapper`) that moves `/z/{subdomain}` from the servlet path into the context path. Downstream code sees `getContextPath()` = `/uaa/z/myzone` and `getServletPath()` = `/login`.
- **Sets request attributes** `ZONE_SUBDOMAIN_FROM_PATH` (the subdomain string) and `ZONE_ORIGINAL_CONTEXT_PATH` (the pre-rewrite context path like `/uaa`), consumed by `IdentityZoneResolvingFilter` and `ZoneAwareStaticResourceLinkBuilder`.
- **Wraps the response** in `CookiePathRewritingResponse` which intercepts `addCookie()`, `addHeader("Set-Cookie", ...)`, and `setHeader("Set-Cookie", ...)` to rewrite cookie paths from `/` back to the original context path (e.g., `/uaa`).
- **Default zone path:** For the subdomain `"default"`, rewrites so the context path **includes** `/z/default` (e.g. `/uaa/z/default`), like any other zone path. Does **not** set `ZONE_SUBDOMAIN_FROM_PATH` so the default zone is resolved. `ZoneContextPathSessionRequestWrapper` maps this context path to the same session key as the root, so `/profile` and `/z/default/profile` share the same cookie/session. See "Default zone path" below.
- **Default zone path:** For the subdomain `"default"`, rewrites so the context
path **includes** `/z/default` (e.g. `/uaa/z/default`), like any other zone
path. Sets `ZONE_SUBDOMAIN_FROM_PATH` to the empty string to explicitly select
the default zone. `ZoneContextPathSessionRequestWrapper` maps this context
path to the same session key as the root, so `/profile` and
`/z/default/profile` share the same cookie/session. See "Default zone path"
below.
- **Passes through** non-zone requests unchanged (but still wraps the response for cookie path normalization when a context path is present).

#### Default zone path
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cloudfoundry.identity.uaa.account;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.cloudfoundry.identity.uaa.scim.ScimUser;

Expand All @@ -19,6 +20,8 @@ class AccountCreationResponse {
private String email;
@JsonProperty("redirect_location")
private String redirectLocation;
@JsonIgnore
private String passwordResetCode;

public AccountCreationResponse(String userId, String username, String email, String redirectLocation) {
this.userId = userId;
Expand Down Expand Up @@ -50,5 +53,13 @@ public String getRedirectLocation() {
public String getEmail() {
return email;
}

public String getPasswordResetCode() {
return passwordResetCode;
}

public void setPasswordResetCode(String passwordResetCode) {
this.passwordResetCode = passwordResetCode;
}
}
}
Loading
Loading