Skip to content
Open
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
44 changes: 39 additions & 5 deletions docs/UAA-Client-Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,9 @@ present a certificate whose chain validates to the configured CA; no separate
| `tls-client-auth-required-claims` | no | Map of `claimName -> requiredValue`, checked against the values produced by `tls-client-auth-claim-mappings`. A UAA extension applied in addition to the subject binding, not instead of it -- authentication fails unless every entry matches exactly. |
| `tls-client-auth-claim-mappings` | no | List of `{field, pattern, claim}` mappings from certificate subject fields (`subject_cn`, `subject_ou`, `subject_o`) to JWT claim names. `subject_cn` and `subject_o` map their values directly; `pattern` is rejected for either, since it is applied only to `subject_ou`, where it must contain at least one capturing group -- a pattern that only matches without capturing (e.g. `space:.+` instead of `space:(.+)`) is rejected too, since it would never produce a value. Patterns use RE2/J syntax and a linear-time matcher. OU values and pattern source are limited to 256 characters, and compiled patterns to 4096 instructions. Backreferences, lookaround, Java comments mode, and counted repetitions of groups are unsupported and rejected at registration; counts on literals and character classes are allowed. Invalid or unsupported patterns in stored configuration yield no claim, so a dependent required-claim check refuses authentication. Prefer simple anchored patterns such as `^space:(.+)$`. |
| `tls-client-auth-sub-template` | no | Template string rendered (using the mapped claim values) to produce the JWT `sub` claim. At most 256 characters; must contain at least one `{claim}` placeholder; every placeholder must name a claim declared in `tls-client-auth-claim-mappings`. |
| `tls-client-auth-aud-templates` | no | List of template strings rendered to produce the JWT `aud` claim. Each entry is subject to the same constraints as `tls-client-auth-sub-template`: at most 256 characters, at least one `{claim}` placeholder, and every placeholder must name a declared claim. |
| `tls-client-auth-allowed-resources` | no | List of exact [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) resource values (absolute URIs, no fragment) this client may request at token time via the `resource` request parameter -- see "RFC 8707 resource indicators" below. Mutually exclusive with `tls-client-auth-aud-templates`: two mechanisms for setting `aud` on one client is a footgun. Must be non-empty if present. |
| `tls-client-auth-aud-templates` | no | List of template strings rendered to produce the JWT `aud` claim. Each entry is subject to the same constraints as `tls-client-auth-sub-template`: at most 256 characters, at least one `{claim}` placeholder, and every placeholder must name a declared claim. Mutually exclusive with both allowed-resource properties. |
| `tls-client-auth-allowed-resources` | no | List of exact [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) resource values (absolute URIs, no fragment) this client may request at token time via the `resource` request parameter -- see "RFC 8707 resource indicators" below. Mutually exclusive with `tls-client-auth-allowed-resources-ref` and `tls-client-auth-aud-templates`. Must be non-empty if present. |
| `tls-client-auth-allowed-resources-ref` | no | Name of an operator-managed allowed-resource policy in this client's identity zone. UAA resolves it for every token request, so policy changes apply without rewriting clients. Unknown or blank references are rejected during create, update, and bootstrap. Mutually exclusive with the inline list and audience templates. |

Example (Gorouter-fronted; a Cloud Foundry app instance identity certificate mapped to
`cf_instance_guid`/`app_guid`/`space_guid`/`org_guid` claims):
Expand Down Expand Up @@ -332,8 +333,8 @@ returns `404`. This endpoint never returns the bundle contents.

#### RFC 8707 resource indicators

A client configured with `tls-client-auth-allowed-resources` may pass a `resource` parameter to
`/oauth/mtls/token`:
A client configured with an inline `tls-client-auth-allowed-resources` list or a named policy
reference may pass a `resource` parameter to `/oauth/mtls/token`:

```yaml
tls-client-auth-allowed-resources:
Expand All @@ -353,6 +354,39 @@ an unrestricted `aud`. Requesting a value outside the list, requesting more than
the same call, or a value that is not an absolute URI without a fragment (per the RFC) is refused
as `invalid_target`, and no token is issued.

#### Named allowed-resource policies

Operators can share one allowed-resource policy across many default-zone clients:

```yaml
uaa:
mtls:
resource_policies:
cf-workload-federation:
allowed_resources:
- https://sts.amazonaws.com
- api://AzureADTokenExchange
```

Other zones configure the equivalent `config.mtls.resourcePolicies` map through the identity-zone
API, using `allowedResources` inside each policy. Names follow the trust-bundle name rules and are
case-sensitive. Policy lists must be non-empty; every entry must be an absolute URI without a
fragment.

```json
{
"client_id": "cf:service-account:payments-worker",
"tls-client-auth-allowed-resources-ref": "cf-workload-federation"
}
```

Policy definitions and references resolve only in the client's current identity zone. An unknown
reference is rejected when a client is created, updated, or bootstrapped. UAA resolves the policy
again during token issuance: adding a resource permits subsequent requests, removing one rejects
subsequent requests, and removing the policy fails explicit resource requests as `invalid_target`.
Existing tokens keep their original lifetime and audience. The inline property remains supported
for existing clients.

#### Registering a `tls_client_auth` client

A client opts in by setting `tls-client-auth-ca` or `tls-client-auth-ca-ref` together with exactly one subject parameter
Expand Down Expand Up @@ -450,7 +484,7 @@ typed mTLS mirror does not change equality, hash codes, or set membership after
`uaa.mtls.enabled` defaults to `false`. With it off the feature does nothing observable. OIDC discovery includes
`tls_client_certificate_bound_access_tokens` only when mTLS is enabled for the deployment and the zone; RFC 8705
section 3.3 treats the omitted value as `false`. Identity-zone configuration includes `config.mtls` only once it
differs from the default (disabled, no bundles), and a `null` value reads as that default.
differs from the default (disabled, no bundles or resource policies), and a `null` value reads as that default.

**Token enhancers.** `MtlsClaimsEnhancer` is always registered alongside other `UaaTokenEnhancer` beans. It is active
only when deployment and zone mTLS are both enabled. It never activates refresh-claim copying, so refreshed access
Expand Down
50 changes: 50 additions & 0 deletions docs/UAA-Configuration-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ or `$CLOUDFOUNDRY_CONFIG_PATH/uaa.yml`.
| [↪](#clientmaxcount) `clientMaxCount` | `500`| Max clients returned by list endpoint|
| [↪](#uaamtlsenabled) `uaa.mtls.enabled` | `false`| Enables RFC 8705 mutual-TLS client authentication|
| [↪](#uaamtlstrust_bundles) `uaa.mtls.trust_bundles` | `{}` | Named mTLS CA bundles for the default identity zone |
| [↪](#uaamtlsresource_policies) `uaa.mtls.resource_policies` | `{}` | Named mTLS allowed-resource policies for the default identity zone |

### Password Policy

Expand Down Expand Up @@ -1387,6 +1388,55 @@ not returned. See [Named CA trust bundles](UAA-Client-Authentication.md#named-ca

---

### `uaa.mtls.resource_policies`

**Default:** `{}`
**Type:** `Map<String, { allowed_resources: List<String> }>`
**Source:** `Binder` in
[configuration wiring](../server/src/main/java/org/cloudfoundry/identity/uaa/SpringServletXmlBeansConfiguration.java)

Defines named RFC 8707 allowed-resource policies for mTLS clients in the default UAA identity
zone. Policy names are case-sensitive and follow the trust-bundle name rules: 1 to 128 letters,
digits, dots, underscores, or hyphens, starting with a letter or digit. Each
`allowed_resources` list must be non-empty. Every entry must be an absolute URI without a
fragment.
Use quoted bracket notation for YAML policy names containing dots or underscores, for example
`"[prod.federation]"`, to preserve the literal name during Spring Boot map binding.

```yaml
uaa:
mtls:
resource_policies:
cf-workload-federation:
allowed_resources:
- https://sts.amazonaws.com
- api://AzureADTokenExchange
"[prod.federation]":
allowed_resources:
- https://sts.amazonaws.com
```

Clients select a policy with `tls-client-auth-allowed-resources-ref`. The reference is mutually
exclusive with both `tls-client-auth-allowed-resources` and `tls-client-auth-aud-templates`.
Unknown or blank references are rejected during client create, update, and `oauth.clients`
bootstrap.

UAA resolves the policy from the client's current identity zone on every token request. Updating
the list therefore affects subsequent requests without rewriting client registrations. Removing
a resource denies new requests for it; removing the referenced policy fails explicit resource
requests closed as `invalid_target`. Existing tokens are not changed. No lookup falls back to
another zone.

Other zones configure the equivalent `config.mtls.resourcePolicies` map through the
identity-zone API, with `allowedResources` as the nested list property. Default-zone YAML is
reapplied at startup.

**Related Properties:** `uaa.mtls.enabled`, `uaa.mtls.trust_bundles`

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

---

### `password.policy.global.minLength`

**Default:** `0`
Expand Down
24 changes: 22 additions & 2 deletions docs/UAA-mTLS-Getting-Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,25 @@ An absent allow-list authorizes nothing — it does not silently fall back to an
This is mutually exclusive with `tls-client-auth-aud-templates` (Step 9): pick one mechanism per
client.

When many clients share the same audiences, an operator can define a named policy in the default
zone's `uaa.yml`:

```yaml
uaa:
mtls:
resource_policies:
workload-federation:
allowed_resources:
- https://billing.apps.internal
- https://reporting.apps.internal
```

The client then uses `"tls-client-auth-allowed-resources-ref": "workload-federation"` instead of
the inline list. Other zones use `config.mtls.resourcePolicies` in the identity-zone API. UAA
resolves the policy on each request, so adding or removing an audience affects new token requests
without updating clients. References never fall back to another zone; a removed policy fails
explicit resource requests as `invalid_target`.

Requesting a value that isn't on the list fails closed, without saying which value was tried:

```bash
Expand Down Expand Up @@ -569,8 +588,9 @@ whatever value was rejected — a caller cannot use the error message to probe w
| `tls-client-auth-required-claims` entry doesn't match | same message as above | Same deliberate non-disclosure | Re-derive the claim values via `tls-client-auth-claim-mappings` and compare by hand |
| Sent a `client_secret` (even an empty one), an `Authorization` header, or a `client_assertion` for a client that has `tls-client-auth-ca` | `401 invalid_client`: `tls_client_auth: configured clients must authenticate at /oauth/mtls/token without client credentials` | A certificate-bound client cannot also obtain unbound tokens | Send only `client_id` and `grant_type`; authenticate with the certificate only |
| Requested any grant other than `client_credentials` | `400 invalid_grant`: `the mTLS token endpoint only issues client_credentials tokens` | This endpoint exists for workload identity, not user grants | Use `client_credentials` |
| `resource=` not on the client's `tls-client-auth-allowed-resources`, or the client has no list | `400 invalid_target`: `client_id=<id> is not authorized to request the given resource` | An absent allow-list authorizes nothing | Add the value to the list (Step 8), or omit `resource` entirely |
| `tls-client-auth-allowed-resources` and `tls-client-auth-aud-templates` both set | `400 invalid_client`, registration refused: `...are mutually exclusive for client_id=...` | Two mechanisms for setting `aud` on one client | Pick one |
| `resource=` is not in the client's inline list or referenced policy, or the referenced policy was removed | `400 invalid_target`: `client_id=<id> is not authorized to request the given resource` | An absent or removed allow-list authorizes nothing | Add the value to the inline list or referenced policy (Step 8), or omit `resource` entirely |
| `tls-client-auth-allowed-resources-ref` is blank or names an unknown policy | `400 invalid_client`, registration refused | A reference must resolve in the client's identity zone | Configure the named policy first, then register the client with its exact case-sensitive name |
| More than one of `tls-client-auth-allowed-resources`, `tls-client-auth-allowed-resources-ref`, and `tls-client-auth-aud-templates` is set | `400 invalid_client`, registration refused: `...are mutually exclusive for client_id=...` | Only one mechanism may set `aud` on a client | Pick one |
| A `tls-client-auth-claim-mappings` `pattern` targets `subject_cn`/`subject_o` | `400 invalid_client`, registration refused | `pattern` only applies to `subject_ou` | Drop `pattern` for `subject_cn`/`subject_o`, or move the mapping to `subject_ou` |
| `PATCH /oauth/clients/{id}` | `405` (or an unrelated-looking `403`/CSRF error, depending on what else is mapped at that path) | The client-admin API has no `PATCH` | Use `PUT` with at least `client_id`; omitted fields are preserved |
| `curl: (60) SSL certificate problem` on *every* request, including plain `/oauth/token` | — | `--cacert` is pointing at the demo mTLS CA from Step 2 instead of whatever CA signed UAA's own HTTPS certificate | Set `$UAA_CACERT` correctly — see Prerequisites; the two files are unrelated |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class TlsClientAuthConfiguration {
* what the server is willing to believe it may ask for.
*/
public static final String TLS_CLIENT_AUTH_ALLOWED_RESOURCES = "tls-client-auth-allowed-resources";
public static final String TLS_CLIENT_AUTH_ALLOWED_RESOURCES_REF = "tls-client-auth-allowed-resources-ref";

/**
* RFC 8705 section 3.4 client registration metadata, named verbatim like the subject parameters
Expand Down Expand Up @@ -82,7 +83,8 @@ public class TlsClientAuthConfiguration {
TLS_CLIENT_AUTH_SUB_TEMPLATE,
TLS_CLIENT_AUTH_AUD_TEMPLATES,
TLS_CLIENT_AUTH_REQUIRED_CLAIMS,
TLS_CLIENT_AUTH_ALLOWED_RESOURCES);
TLS_CLIENT_AUTH_ALLOWED_RESOURCES,
TLS_CLIENT_AUTH_ALLOWED_RESOURCES_REF);

/** Keys that configure the proxy allowed to forward a client certificate. Each requires workload trust. */
public static final List<String> PROXY_TRUST_KEYS = List.of(
Expand Down Expand Up @@ -219,6 +221,9 @@ public static byte[] parseIpLiteral(String value) {
@JsonProperty(TLS_CLIENT_AUTH_ALLOWED_RESOURCES)
private List<String> allowedResources;

@JsonProperty(TLS_CLIENT_AUTH_ALLOWED_RESOURCES_REF)
private String allowedResourcesRef;

@JsonProperty(TLS_CLIENT_AUTH_SUBJECT_DN)
private String subjectDn;

Expand Down Expand Up @@ -267,6 +272,8 @@ public TlsClientAuthConfiguration(String trustedCaPem, List<ClaimMapping> claimM

public List<String> getAllowedResources() { return allowedResources; }
public void setAllowedResources(List<String> allowedResources) { this.allowedResources = allowedResources; }
public String getAllowedResourcesRef() { return allowedResourcesRef; }
public void setAllowedResourcesRef(String allowedResourcesRef) { this.allowedResourcesRef = allowedResourcesRef; }

public String getSubjectDn() { return subjectDn; }
public void setSubjectDn(String subjectDn) { this.subjectDn = subjectDn; }
Expand Down Expand Up @@ -322,13 +329,15 @@ public boolean equals(Object o) {
Objects.equals(sanUri, that.sanUri) &&
Objects.equals(sanIp, that.sanIp) &&
Objects.equals(sanEmail, that.sanEmail) &&
Objects.equals(allowedResources, that.allowedResources);
Objects.equals(allowedResources, that.allowedResources) &&
Objects.equals(allowedResourcesRef, that.allowedResourcesRef);
}

@Override
public int hashCode() {
return Objects.hash(trustedCaPem, trustedCaRef, trustedProxyCaRef, claimMappings, subTemplate, audTemplates, trustedProxyCaPem,
trustedProxySubjectDn, requiredClaims, subjectDn, sanDns, sanUri, sanIp, sanEmail, allowedResources);
trustedProxySubjectDn, requiredClaims, subjectDn, sanDns, sanUri, sanIp, sanEmail, allowedResources,
allowedResourcesRef);
}

public static boolean isConfigured(TlsClientAuthConfiguration config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ public void setTlsClientAuthConfiguration(TlsClientAuthConfiguration tlsClientAu
tlsClientAuthConfiguration.getRequiredClaims());
putOrRemove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_ALLOWED_RESOURCES,
tlsClientAuthConfiguration.getAllowedResources());
putOrRemove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_ALLOWED_RESOURCES_REF,
tlsClientAuthConfiguration.getAllowedResourcesRef());
putOrRemove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_SUBJECT_DN, tlsClientAuthConfiguration.getSubjectDn());
putOrRemove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_SAN_DNS, tlsClientAuthConfiguration.getSanDns());
putOrRemove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_SAN_URI, tlsClientAuthConfiguration.getSanUri());
Expand All @@ -350,6 +352,7 @@ public void setTlsClientAuthConfiguration(TlsClientAuthConfiguration tlsClientAu
this.additionalInformation.remove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_TRUSTED_PROXY_SUBJECT_DN);
this.additionalInformation.remove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_REQUIRED_CLAIMS);
this.additionalInformation.remove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_ALLOWED_RESOURCES);
this.additionalInformation.remove(TlsClientAuthConfiguration.TLS_CLIENT_AUTH_ALLOWED_RESOURCES_REF);
TlsClientAuthConfiguration.SUBJECT_BINDING_PARAMETERS.forEach(this.additionalInformation::remove);
// Not part of the typed configuration, but meaningless without TLS trust.
this.additionalInformation.remove(TlsClientAuthConfiguration.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKENS);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cloudfoundry.identity.uaa.zone;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import lombok.Data;
Expand All @@ -17,4 +18,8 @@ public class MtlsConfig {

@JsonSetter(nulls = Nulls.AS_EMPTY)
private Map<String, String> trustBundles = new LinkedHashMap<>();

@JsonSetter(nulls = Nulls.AS_EMPTY)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, MtlsResourcePolicy> resourcePolicies = new LinkedHashMap<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.cloudfoundry.identity.uaa.zone;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MtlsResourcePolicy {
@JsonSetter(nulls = Nulls.AS_EMPTY)
private List<String> allowedResources = new ArrayList<>();
}
Loading
Loading