diff --git a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/AbstractTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/AbstractTokenExchangeProvider.java index 05411913f0d..9d82de3ea9c 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/AbstractTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/AbstractTokenExchangeProvider.java @@ -323,7 +323,7 @@ public abstract class AbstractTokenExchangeProvider implements TokenExchangeProv } UserModel user = null; - if (! context.getIdpConfig().isTransientUsers()) { + if (!context.getIdpConfig().isTransientUsers()) { FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(), context.getUsername(), context.getToken()); @@ -435,9 +435,9 @@ public abstract class AbstractTokenExchangeProvider implements TokenExchangeProv } } - record ExternalExchangeContext (ExchangeExternalToken provider, IdentityProviderModel idpModel) {}; + protected record ExternalExchangeContext (ExchangeExternalToken provider, IdentityProviderModel idpModel) {}; - private ExternalExchangeContext locateExchangeExternalTokenByAlias(String alias) { + protected ExternalExchangeContext locateExchangeExternalTokenByAlias(String alias) { try { IdentityProvider idp = IdentityBrokerService.getIdentityProvider(session, alias); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/ExternalToInternalTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/ExternalToInternalTokenExchangeProvider.java index 9d0fd86c022..e3842a3cf64 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/ExternalToInternalTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/ExternalToInternalTokenExchangeProvider.java @@ -20,16 +20,27 @@ package org.keycloak.protocol.oidc.tokenexchange; import jakarta.ws.rs.core.Response; +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.broker.provider.IdentityProvider; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenExchangeContext; +import org.keycloak.services.CorsErrorResponseException; +import org.keycloak.services.managers.UserSessionManager; + +import java.util.Arrays; +import java.util.List; /** * Provider for external-internal token exchange * - * TODO Should not extend from V1TokenExchangeProvider, but rather AbstractTokenExchangeProvider or from StandardTokenExchangeProvider (as issuing internal tokens might be done in a same/similar way like for standard V2 provider) - * * @author Marek Posolda */ -public class ExternalToInternalTokenExchangeProvider extends V1TokenExchangeProvider { +public class ExternalToInternalTokenExchangeProvider extends StandardTokenExchangeProvider { @Override public boolean supports(TokenExchangeContext context) { @@ -49,4 +60,53 @@ public class ExternalToInternalTokenExchangeProvider extends V1TokenExchangeProv return exchangeExternalToken(subjectIssuer, subjectToken); } + @Override + protected List getSupportedOAuthResponseTokenTypes() { + return Arrays.asList(OAuth2Constants.ACCESS_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE); + } + + @Override + protected String getRequestedTokenType() { + String requestedTokenType = params.getRequestedTokenType(); + if (requestedTokenType == null) { + requestedTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE; + return requestedTokenType; + } + if (getSupportedOAuthResponseTokenTypes().contains(requestedTokenType)) { + return requestedTokenType; + } + + event.detail(Details.REASON, "requested_token_type unsupported"); + event.error(Errors.INVALID_REQUEST); + throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST); + } + + protected Response exchangeExternalToken(String subjectIssuer, String subjectToken) { + // try to find the IDP whose alias matches the issuer or the subject issuer in the form params. + ExternalExchangeContext externalExchangeContext = this.locateExchangeExternalTokenByAlias(subjectIssuer); + + if (externalExchangeContext == null) { + event.error(Errors.INVALID_ISSUER); + throw new CorsErrorResponseException(cors, Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST); + } + BrokeredIdentityContext context = externalExchangeContext.provider().exchangeExternal(this, this.context); + if (context == null) { + event.error(Errors.INVALID_ISSUER); + throw new CorsErrorResponseException(cors, Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST); + } + + UserModel user = importUserFromExternalIdentity(context); + + UserSessionModel userSession = new UserSessionManager(session).createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteHost(), "external-exchange", false, null, null); + externalExchangeContext.provider().exchangeExternalComplete(userSession, context, formParams); + + // this must exist so that we can obtain access token from user session if idp's store tokens is off + userSession.setNote(IdentityProvider.EXTERNAL_IDENTITY_PROVIDER, externalExchangeContext.idpModel().getAlias()); + userSession.setNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, subjectToken); + + context.addSessionNotesToUserSession(userSession); + + return exchangeClientToClient(user, userSession, null, false); + } + } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java index d366e221330..e3b3dc996b1 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/tokenexchange/StandardTokenExchangeProvider.java @@ -248,23 +248,26 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider clientSessionCtx.setAttribute(Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE); TokenContextEncoderProvider encoder = session.getProvider(TokenContextEncoderProvider.class); - AccessTokenContext subjectTokenContext = encoder.getTokenContextFromTokenId(subjectToken.getId()); - //copy subject client from the client session notes if the subject token used has already been exchanged - if (OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE.equals(subjectTokenContext.getGrantType())) { - ClientModel subjectClient = session.clients().getClientByClientId(realm, subjectToken.getIssuedFor()); - if (subjectClient != null) { - AuthenticatedClientSessionModel subjectClientSession = targetUserSession.getAuthenticatedClientSessionByClient(subjectClient.getId()); - if (subjectClientSession != null) { - subjectClientSession.getNotes().entrySet().stream() - .filter(note -> note.getKey().startsWith(Constants.TOKEN_EXCHANGE_SUBJECT_CLIENT)) - .forEach(note -> clientSessionCtx.getClientSession().setNote(note.getKey(), note.getValue())); + if (subjectToken != null) { + AccessTokenContext subjectTokenContext = encoder.getTokenContextFromTokenId(subjectToken.getId()); + + //copy subject client from the client session notes if the subject token used has already been exchanged + if (OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE.equals(subjectTokenContext.getGrantType())) { + ClientModel subjectClient = session.clients().getClientByClientId(realm, subjectToken.getIssuedFor()); + if (subjectClient != null) { + AuthenticatedClientSessionModel subjectClientSession = targetUserSession.getAuthenticatedClientSessionByClient(subjectClient.getId()); + if (subjectClientSession != null) { + subjectClientSession.getNotes().entrySet().stream() + .filter(note -> note.getKey().startsWith(Constants.TOKEN_EXCHANGE_SUBJECT_CLIENT)) + .forEach(note -> clientSessionCtx.getClientSession().setNote(note.getKey(), note.getValue())); + } } } - } - //store client id of the subject token - clientSessionCtx.getClientSession().setNote(Constants.TOKEN_EXCHANGE_SUBJECT_CLIENT + subjectToken.getIssuedFor(), subjectToken.getId()); + //store client id of the subject token + clientSessionCtx.getClientSession().setNote(Constants.TOKEN_EXCHANGE_SUBJECT_CLIENT + subjectToken.getIssuedFor(), subjectToken.getId()); + } TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, client, event, session, clientSessionCtx.getClientSession().getUserSession(), clientSessionCtx).generateAccessToken(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java index 629fb8994a2..067828842fc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java @@ -54,11 +54,7 @@ import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; -import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; -import org.keycloak.services.resources.admin.fgap.AdminPermissionManagement; -import org.keycloak.services.resources.admin.fgap.AdminPermissions; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; -import org.keycloak.testsuite.arquillian.annotation.EnableFeatures; import org.keycloak.testsuite.broker.AbstractInitializedBaseBrokerTest; import org.keycloak.testsuite.broker.BrokerConfiguration; import org.keycloak.testsuite.broker.BrokerTestConstants; @@ -84,8 +80,7 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot; * * @author Marek Posolda */ -// TODO: Remove fine grained admin permissions should not be needed. They are neded now for token_exchange_external_internal:v2, but should not be needed in the future -@EnableFeatures({@EnableFeature(Profile.Feature.TOKEN_EXCHANGE_EXTERNAL_INTERNAL_V2), @EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)}) +@EnableFeature(Profile.Feature.TOKEN_EXCHANGE_EXTERNAL_INTERNAL_V2) public class ExternalInternalTokenExchangeV2Test extends AbstractInitializedBaseBrokerTest { @Override @@ -152,17 +147,6 @@ public class ExternalInternalTokenExchangeV2Test extends AbstractInitializedBase client.setFullScopeAllowed(false); client.setRedirectUris(Set.of(OAuthClient.AUTH_SERVER_ROOT + "/*")); - ClientModel brokerApp = realm.getClientByClientId("broker-app"); - - AdminPermissionManagement management = AdminPermissions.management(session, realm); - management.idps().setPermissionsEnabled(idp, true); - ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); - clientRep.setName("toIdp"); - clientRep.addClient(client.getId(), brokerApp.getId()); - ResourceServer server = management.realmResourceServer(); - Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep); - management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy); - realm = session.realms().getRealmByName(BrokerTestConstants.REALM_PROV_NAME); client = realm.getClientByClientId("brokerapp"); client.addRedirectUri(OAuthClient.APP_ROOT + "/auth");