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");