8000 DPoP support for token exchange by graziang · Pull Request #47571 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ In the following cases, {project_name} will verify the client sending the access
* A UserInfo request is sent to UserInfo endpoint with a holder-of-key access token.
* A logout request is sent to non-OIDC compliant {project_name} proprietary Logout endpoint with a holder-of-key refresh token.

See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-mtls-08#section-3[Mutual TLS Client Certificate Bound Access Tokens] in the OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens for more details.
See https://datatracker.ietf.org/doc/rfc8705/[Mutual TLS Client Certificate Bound Access Tokens] in the OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens for more details.

[NOTE]
====
Sender-constrained tokens like X.509 certificate-bound tokens (mTLS tokens with the `x5t#S256` confirmation claim), cannot be used as the `subject_token` parameter in link:{securing_apps_token_exchange_link}#_standard-token-exchange[Standard Token Exchange].
====


[NOTE]
====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ For example, `spi-truststore--file--hostname-verification-policy` was marked as
To ensure that the further introduction of new first-class options does not immediately break existing configurations, the logic for determining the value of a second-class option
will now consider the direct usage of the second-class value ahead of a default, or derived default, from the first-class option.

Please review the server startup to see if there are any warnings about the continued usage of second-class options, and update them accordingly.
Please review the server startup to see if there are any warnings about the continued usage of second-class options, and update them accordingly.

=== New option for transaction timeout

Expand Down Expand Up @@ -212,6 +212,14 @@ A similar change was done for the `UserSessionPersisterProvider`.

Authentication failures resulting in an invalid_grant now correctly return an HTTP 400 status code instead of 401 for the Resource Owner Password Credentials and UMA Permission grants. This change was done to properly follow the OAuth 2.0 Authorization Framework specification.

=== Sender-constrained tokens are now rejected in Standard Token Exchange

Standard Token Exchange now rejects all sender-constrained tokens (as defined in link:https://datatracker.ietf.org/doc/html/rfc7800[RFC 7800]) used as the `subject_token` parameter. This includes both DPoP-bound tokens and X.509 certificate-bound tokens (mTLS).

This change ensures better security and alignment with the OAuth 2.0 Token Exchange specification. Sender-constrained tokens are bound to specific clients and should not be exchanged across different contexts.

If you attempt to exchange a sender-constrained token, the request will be rejected with an `invalid_request` error. For more details, see the link:{securing_apps_token_exchange_link}[Token Exchange] guide.

=== Re-indexing required for external Infinispan Server

With the `clusterless` feature enabled and `persistent-user-sessions` disabled, a re-index is required due to schema changes.
Expand Down
13 changes: 13 additions & 0 deletions docs/guides/securing-apps/dpop.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,17 @@ For granular control, you can use the **dpop-bind-enforcer** executor within Cli
* **Refresh Token Only:** Enforce DPoP binding only for the Refresh Token while keeping the access token as a standard Bearer token. This increases security for public clients while maintaining compatibility with legacy resource servers that do not support DPoP.
* **Strict OIDC Enforcement:** Require clients to send the `dpop_jkt` parameter during the initial Authorization Code flow, binding the entire flow.

== DPoP with Standard Token Exchange

* Standard Token Exchange does not support DPoP-bound tokens (as defined in RFC 7800) as the `subject_token` parameter. Only Bearer access tokens can be exchanged. If you attempt to use a DPoP-bound token as the subject token, the request will be rejected with an `invalid_request` error.
* While DPoP-bound tokens cannot be used as `subject_token` to token exchange, you can obtain DPoP-bound tokens as **output**.

=== When to Use Token Exchange with DPoP

Token exchange with DPoP is useful when:

* You need to upgrade a Bearer token to a DPoP-bound token for increased security
* You want to down-scope or change audiences while simultaneously adding DPoP binding
* Your backend services require DPoP but the original token was issued as Bearer

</@tmpl.guide>
2 changes: 2 additions & 0 deletions docs/guides/securing-apps/token-exchange.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ This use case can be replaced by refresh token grant.
* The `subject_token` sent to the token exchange endpoint must have the requester client set as an audience in the `aud` claim. Otherwise, the request would be rejected. The only exception is, if client
exchanges his own token, which was issued to it. Exchanging to itself might be useful to downscope/upscope the token or filter unneeded token audiences and so on.

* Sender-constrained tokens (as defined in RFC 7800) cannot be used as `subject_token`. This includes DPoP-bound tokens and X.509 certificate-bound tokens. Standard Token Exchange only accepts Bearer access tokens as subject tokens. If you provide a sender-constrained token, the request will be rejected with an `invalid_request` error. However, you can obtain DPoP-bound tokens as **output** from token exchange by including a valid DPoP proof in the request. For more details, see <@links.securingapps id="dpop" />.

* Consents - If the requester client has *Consent required* enabled, the token exchange is allowed only if the user is already granted consent to all requested scopes

* link:{adminguide_link}#_fine_grain_permissions[Fine-grained admin permissions (FGAP)] are not needed for the standard token exchange. We plan to eventually integrate with FGAP for the future, but that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ protected Response tokenExchange() {
UserSessionModel tokenSession = authResult.session();
AccessToken token = authResult.token();

// Reject sender-constrained tokens (RFC 7800) as subject_token
if (isSenderConstrainedToken(token)) {
event.detail(Details.REASON, "Sender-constrained tokens (RFC 7800) cannot be used as subject_token in Token Exchange");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
"Sender-constrained tokens are not supported as subject_token. Use a Bearer token instead.",
Response.Status.BAD_REQUEST);
}

event.user(tokenUser);
event.detail(Details.USERNAME, tokenUser.getUsername());
if (token.getSessionId() != null) {
Expand Down Expand Up @@ -364,4 +373,8 @@ protected String getRequestedTokenType() {
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
}

private static boolean isSenderConstrainedToken(AccessToken token) {
return token.getConfirmation() != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
Expand Down Expand Up @@ -1455,4 +1456,28 @@ private void assertAccessTokenContext(String jti, AccessTokenContext.SessionType
assertEquals(tokenType, ctx.getTokenType());
assertEquals(grantType, ctx.getGrantType());
}

@Test
public void testSenderConstrainedTokenRejection() throws Exception {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(TEST), "subject-client");
// Create a protocol mapper that adds the cnf claim
ProtocolMapperModel mapper = HardcodedClaim.create("test-cnf-mapper", "cnf", "{\"jkt\":\"test-thumbprint-12345\"}", "JSON", true, false, false);

Response mapperResponse = client.getProtocolMappers().createMapper(ModelToRepresentation.toRepresentation(mapper));
String mapperId = ApiUtil.getCreatedId(mapperResponse);

// Get a new token with the cnf claim
String senderConstrainedToken = resourceOwnerLogin("john", "password", "subject-client", "secret").getAccessToken();
AccessToken token = TokenVerifier.create(senderConstrainedToken, AccessToken.class).parse().getToken();
assertNotNull(token.getConfirmation());

AccessTokenResponse response = tokenExchange(senderConstrainedToken, "requester-client", "secret", null, null);

assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
assertTrue("Error should mention sender-constrained tokens", response.getErrorDescription().contains("Sender-constrained"));

client.getProtocolMappers().delete(mapperId);

}
}
Loading
0