8000 Customize token exchange endpoint to issue ID-JAG by bucchi · Pull Request #49998 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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
1 change: 1 addition & 0 deletions core/src/main/java/org/keycloak/OAuth2Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public interface OAuth2Constants {

// https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/
String IDENTITY_ASSERTION_JWT_HEADER_TYPE = "oauth-id-jag+jwt";
String IDENTITY_ASSERTION_JWT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id-jag";


// https://tools.ietf.org/html/draft-ietf-oauth-assertions-01#page-5
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/keycloak/util/TokenUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public class TokenUtil {

public static final String TOKEN_TYPE_LOGOUT = "Logout";

public static final String TOKEN_TYPE_IDJAG = "IDJAG";

public static final String TOKEN_BACKCHANNEL_LOGOUT_EVENT = "http://schemas.openid.net/event/backchannel-logout";

public static final String TOKEN_BACKCHANNEL_LOGOUT_EVENT_REVOKE_OFFLINE_TOKENS = "revoke_offline_access";
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.keycloak.protocol.oidc.tokenexchange;

import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.oidc.TokenExchangeProvider;
import org.keycloak.protocol.oidc.TokenExchangeProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;

/**
* Provider factory for token exchange of Identity Assertion JWT Authorization Grant (ID-JAG)(*),
* which is based on the token exchange specification RFC8693(**).
*
* (*)https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/
* (**)https://datatracker.ietf.org/doc/html/rfc8693
*
* @author <a href="mailto:yutaka.obuchi.sd@hitachi.com">Yutaka Obuchi</a>
*/
public class IDJWTTokenExchangeProviderFactory implements TokenExchangeProviderFactory, EnvironmentDependentProviderFactory {
Comment thread
bucchi marked this conversation as resolved.
Resolved

@Override
public TokenExchangeProvider create(KeycloakSession session) {
return new IDJWTTokenExchangeProvider();
}

@Override
public void init(Config.Scope config) {

}

@Override
public void postInit(KeycloakSessionFactory factory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return "idjag";
}

@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.IDENTITY_ASSERTION_JWT);
}

@Override
public int order() {
// Higher priority than V1TokenExchangeProviderFactory and StandardTokenExchangeProviderFactory,
// so ID-JAG is preferred when multiple token exchange providers are enabled.
return 30;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ protected Response tokenExchange() {
}

protected AuthenticationManager.AuthResult processSubjectToken() {
if (!OAuth2Constants.ACCESS_TOKEN_TYPE.equals(context.getParams().getSubjectTokenType())) {
if (!getSupportedSubjectTokenType().equals(context.getParams().getSubjectTokenType())) {
event.detail(Details.REASON, "subject_token_type invalid");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Invalid subject token type", Response.Status.BAD_REQUEST);
Expand All @@ -134,8 +134,7 @@ protected AuthenticationManager.AuthResult processSubjectToken() {

event.detail(Details.REQUESTED_TOKEN_TYPE, context.getParams().getRequestedTokenType());

AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, session.getContext().getUri(), clientConnection, true, true, null,
false, subjectToken, context.getHeaders(), verifier -> {});
AuthenticationManager.AuthResult authResult = verifySubjectToken(subjectToken);
if (authResult == null) {
event.detail(Details.REASON, "subject_token validation failure");
event.error(Errors.INVALID_TOKEN);
Expand All @@ -161,6 +160,22 @@ protected AuthenticationManager.AuthResult processSubjectToken() {
return authResult;
}

/**
* The subject_token_type accepted by this provider.
*/
protected String getSupportedSubjectTokenType() {
return OAuth2Constants.ACCESS_TOKEN_TYPE;
}

/**
* Verifies the subject token and resolves its user and session.
* Returns null if the token is invalid.
*/
protected AuthenticationManager.AuthResult verifySubjectToken(String subjectToken) {
return AuthenticationManager.verifyIdentityToken(session, realm, session.getContext().getUri(), clientConnection, true,
true, null, false, subjectToken, context.getHeaders(), verifier -> {});
}


protected void validateSubjectToken(AccessToken subjectToken) {
if (subjectToken.getOtherClaims().containsKey(IDToken.MAY_ACT) || subjectToken.getOtherClaims().containsKey(IDToken.ACT)) {
Expand Down Expand Up @@ -263,6 +278,33 @@ protected void validateConsents(UserModel targetUser, String scope) {
}
}

protected Response buildTokenExchangeResponse(TokenManager.AccessTokenResponseBuilder responseBuilder, String requestedTokenType, List<ClientModel> targetAudienceClients) {

AccessTokenResponse res;
if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedTokenType)) {
// Using the id-token inside "access_token" parameter as per description of "access_token" parameter under https://datatracker.ietf.org/doc/html/rfc8693#name-successful-response
res = responseBuilder.generateIDToken().build();
res.setToken(res.getIdToken());
res.setIdToken(null);
res.setTokenType(TokenUtil.TOKEN_TYPE_NA);
} else {
String scopeParam = params.getScope();
if (TokenUtil.isOIDCRequest(scopeParam)) {
responseBuilder.generateIDToken().generateAccessTokenHash();
}
res = responseBuilder.build();
}

res.setOtherClaims(OAuth2Constants.ISSUED_TOKEN_TYPE, requestedTokenType);

if (responseBuilder.getAccessToken().getAudience() != null) {
event.detail(Details.AUDIENCE, CollectionUtil.join(List.of(responseBuilder.getAccessToken().getAudience()), " "));
}
event.success();

return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE));
}

// For now, include "scope" parameter as is
@Override
protected String getRequestedScope(AccessToken token, List<ClientModel> targetAudienceClients) {
Expand Down Expand Up @@ -371,30 +413,8 @@ protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionM
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}

AccessTokenResponse res;
if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedTokenType)) {
// Using the id-token inside "access_token" parameter as per description of "access_token" parameter under https://datatracker.ietf.org/doc/html/rfc8693#name-successful-response
res = responseBuilder.generateIDToken().build();
res.setToken(res.getIdToken());
res.setIdToken(null);
res.setTokenType(TokenUtil.TOKEN_TYPE_NA);
} else {
String scopeParam = params.getScope();
if (TokenUtil.isOIDCRequest(scopeParam)) {
responseBuilder.generateIDToken().generateAccessTokenHash();
}
res = responseBuilder.build();
}

res.setOtherClaims(OAuth2Constants.ISSUED_TOKEN_TYPE, requestedTokenType);

if (responseBuilder.getAccessToken().getAudience() != null) {
event.detail(Details.AUDIENCE, CollectionUtil.join(List.of(responseBuilder.getAccessToken().getAudience()), " "));
}

event.success();
return buildTokenExchangeResponse(responseBuilder, requestedTokenType, targetAudienceClients);

return cors.add(Response.ok(res, MediaType.APPLICATION_JSON_TYPE));
} catch (RuntimeException e) {
// Cleanup client-session if created in this request
if (newClientSessionCreated) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.keycloak.protocol.oidc.tokenexchange.V1TokenExchangeProviderFactory
org.keycloak.protocol.oidc.tokenexchange.StandardTokenExchangeProviderFactory
org.keycloak.protocol.oidc.tokenexchange.TokenExchangeDelegationProviderFactory
org.keycloak.protocol.oidc.tokenexchange.IDJWTTokenExchangeProviderFactory
Comment thread
Copilot marked this conversation as resolved.
Resolved
Loading
Loading
0