Details
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
index 0478051..d0048b2 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
@@ -16,6 +16,7 @@
*/
package org.keycloak.broker.provider;
+import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -38,5 +39,5 @@ public interface ExchangeTokenToIdentityProviderToken {
* @param params form parameters received for requested exchange
* @return
*/
- Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params);
+ Response exchangeFromToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params);
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index c3a26dc..918a385 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -148,26 +148,30 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
@Override
- public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
+ public Response exchangeFromToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
// check to see if we have a token exchange in session
// in other words check to see if this session was created by an external exchange
- Response tokenResponse = hasExternalExchangeToken(tokenUserSession, params);
+ Response tokenResponse = hasExternalExchangeToken(event, tokenUserSession, params);
if (tokenResponse != null) return tokenResponse;
// going further we only support access token type? Why?
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
+ event.detail(Details.REASON, "requested_token_type unsupported");
+ event.error(Errors.INVALID_REQUEST);
return exchangeUnsupportedRequiredType();
}
if (!getConfig().isStoreToken()) {
// if token isn't stored, we need to see if this session has been linked
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
+ event.detail(Details.REASON, "requested_issuer has not linked");
+ event.error(Errors.INVALID_REQUEST);
return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
- return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
+ return exchangeSessionToken(uriInfo, event, authorizedClient, tokenUserSession, tokenSubject);
} else {
- return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
+ return exchangeStoredToken(uriInfo, event, authorizedClient, tokenUserSession, tokenSubject);
}
}
@@ -178,7 +182,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
* @param params
* @return
*/
- protected Response hasExternalExchangeToken(UserSessionModel tokenUserSession, MultivaluedMap<String, String> params) {
+ protected Response hasExternalExchangeToken(EventBuilder event, UserSessionModel tokenUserSession, MultivaluedMap<String, String> params) {
if (getConfig().getAlias().equals(tokenUserSession.getNote(OIDCIdentityProvider.EXCHANGE_PROVIDER))) {
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
@@ -193,6 +197,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
tokenResponse.setExpiresIn(0);
tokenResponse.getOtherClaims().clear();
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
} else if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedType)) {
@@ -206,6 +211,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
tokenResponse.setExpiresIn(0);
tokenResponse.getOtherClaims().clear();
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE);
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
@@ -215,15 +221,19 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return null;
}
- protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+ protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
if (model == null || model.getToken() == null) {
+ event.detail(Details.REASON, "requested_issuer is not linked");
+ event.error(Errors.INVALID_TOKEN);
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
if (accessToken == null) {
model.setToken(null);
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+ event.detail(Details.REASON, "requested_issuer token expired");
+ event.error(Errors.INVALID_TOKEN);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
@@ -234,12 +244,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
tokenResponse.getOtherClaims().clear();
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
- protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+ protected Response exchangeSessionToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
if (accessToken == null) {
+ event.detail(Details.REASON, "requested_issuer is not linked");
+ event.error(Errors.INVALID_TOKEN);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse tokenResponse = new AccessTokenResponse();
@@ -250,6 +263,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
tokenResponse.getOtherClaims().clear();
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 2963783..ac7decf 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -229,10 +229,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
-
- protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+ @Override
+ protected Response exchangeStoredToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
if (model == null || model.getToken() == null) {
+ event.detail(Details.REASON, "requested_issuer is not linked");
+ event.error(Errors.INVALID_TOKEN);
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
try {
@@ -252,6 +254,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
model.setToken(null);
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+ event.detail(Details.REASON, "requested_issuer token expired");
+ event.error(Errors.INVALID_TOKEN);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@@ -280,18 +284,26 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
tokenResponse.getOtherClaims().clear();
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
- protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+ @Override
+ protected Response exchangeSessionToken(UriInfo uriInfo, EventBuilder event, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+ String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
+ String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+ String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
+
+ if (accessToken == null) {
+ event.detail(Details.REASON, "requested_issuer is not linked");
+ event.error(Errors.INVALID_TOKEN);
+ return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
+ }
try {
long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
- String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
- String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
- String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
if (expiration == 0 || expiration > Time.currentTime()) {
AccessTokenResponse tokenResponse = new AccessTokenResponse();
tokenResponse.setExpiresIn(expiration);
@@ -301,6 +313,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
tokenResponse.setRefreshExpiresIn(0);
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
+ event.success();
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
}
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
@@ -310,6 +323,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
if (response.contains("error")) {
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
+ event.detail(Details.REASON, "requested_issuer token expired");
+ event.error(Errors.INVALID_TOKEN);
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
}
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@@ -324,6 +339,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
newResponse.getOtherClaims().clear();
newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
+ event.success();
return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
} catch (IOException e) {
throw new RuntimeException(e);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 30e52be..a28f283 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -608,6 +608,7 @@ public class TokenEndpoint {
String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
+ event.detail(Details.REASON, "subject_token supports access tokens only");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
@@ -615,6 +616,7 @@ public class TokenEndpoint {
AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
if (authResult == null) {
+ event.detail(Details.REASON, "subject_token validation failure");
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
}
@@ -634,7 +636,7 @@ public class TokenEndpoint {
if (requestedUser == null) {
// We always returned access denied to avoid username fishing
- logger.debug("Requested subject not found");
+ event.detail(Details.REASON, "requested_subject not found");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
@@ -645,7 +647,7 @@ public class TokenEndpoint {
// for this case, the user represented by the token, must have permission to impersonate.
AdminAuth auth = new AdminAuth(realm, token, tokenUser, client);
if (!AdminPermissions.evaluator(session, realm, auth).users().canImpersonate(requestedUser)) {
- logger.debug("Token user not allowed to exchange for requested subject");
+ event.detail(Details.REASON, "subject not allowed to impersonate");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
@@ -654,13 +656,13 @@ public class TokenEndpoint {
// no token is being exchanged, this is a direct exchange. Client must be authenticated, not public, and must be allowed
// to impersonate
if (client.isPublicClient()) {
- logger.debug("Public clients cannot exchange tokens");
+ event.detail(Details.REASON, "public clients not allowed");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
if (!AdminPermissions.management(session, realm).users().canClientImpersonate(client, requestedUser)) {
- logger.debug("Client not allowed to exchange for requested subject");
+ event.detail(Details.REASON, "client not allowed to impersonate");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
@@ -684,21 +686,23 @@ public class TokenEndpoint {
event.detail(Details.REQUESTED_ISSUER, requestedIssuer);
IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
if (providerModel == null) {
+ event.detail(Details.REASON, "unknown requested_issuer");
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
}
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
+ event.detail(Details.REASON, "exchange unsupported by requested_issuer");
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
}
if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, providerModel)) {
- logger.debug("Client not allowed to exchange for linked token");
+ event.detail(Details.REASON, "client not allowed to exchange for requested_issuer");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
- Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, targetUserSession, targetUser, formParams);
+ Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, event, client, targetUserSession, targetUser, formParams);
return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
@@ -708,8 +712,9 @@ public class TokenEndpoint {
if (requestedTokenType == null) {
requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
} else if (!requestedTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE) && !requestedTokenType.equals(OAuth2Constants.REFRESH_TOKEN_TYPE)) {
+ event.detail(Details.REASON, "requested_token_type unsupported");
event.error(Errors.INVALID_REQUEST);
- throw new ErrorResponseException("unsupported_requested_token_type", "Unsupported requested token type", Response.Status.BAD_REQUEST);
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "requested_token_type unsupported", Response.Status.BAD_REQUEST);
}
ClientModel targetClient = client;
@@ -719,12 +724,13 @@ public class TokenEndpoint {
}
if (targetClient.isConsentRequired()) {
+ event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
}
if (!targetClient.equals(client) && !AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
- logger.debug("Client does not have exchange rights for target audience");
+ event.detail(Details.REASON, "client not allowed to exchange to audience");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
@@ -782,7 +788,7 @@ public class TokenEndpoint {
throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
}
if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, context.getIdpConfig())) {
- logger.debug("Client not allowed to exchange for linked token");
+ event.detail(Details.REASON, "client not allowed to exchange subject_issuer");
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
}
@@ -852,8 +858,6 @@ public class TokenEndpoint {
throw new ErrorResponseException(Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
}
- // don't allow user that already exists
- // firstBroker login
user = session.users().addUser(realm, username);
user.setEnabled(true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
index 9ff6938..be579e2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
@@ -451,6 +451,8 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
@Test
public void testExternalExchange() throws Exception {
+ RealmResource childRealm = adminClient.realms().realm(CHILD_IDP);
+
String accessToken = oauth.doGrantAccessTokenRequest(PARENT_IDP, PARENT2_USERNAME, "password", null, PARENT_CLIENT, "password").getAccessToken();
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
@@ -483,6 +485,10 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true));
rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl());
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
+
+ String exchangedUserId = null;
+ String exchangedUsername = null;
+
{
// valid exchange
Response response = exchangeUrl.request()
@@ -502,11 +508,11 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
AccessToken token = jws.readJsonContent(AccessToken.class);
response.close();
- String childUserId = token.getSubject();
- String username = token.getPreferredUsername();
+ exchangedUserId = token.getSubject();
+ exchangedUsername = token.getPreferredUsername();
- System.out.println("childUserId: " + childUserId);
- System.out.println("username: " + username);
+ System.out.println("exchangedUserId: " + exchangedUserId);
+ System.out.println("exchangedUsername: " + exchangedUsername);
// test that we can exchange back to external token
@@ -537,13 +543,54 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
- RealmResource realm = adminClient.realms().realm(CHILD_IDP);
- List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
+ List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
Assert.assertEquals(1, links.size());
- realm.users().get(childUserId).remove();
}
{
- // unauthorized client
+ // check that we can request an exchange again and that the previously linked user is obtained
+ Response response = exchangeUrl.request()
+ .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
+ .post(Entity.form(
+ new Form()
+ .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+ .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+ .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+ .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
+
+ ));
+ Assert.assertEquals(200, response.getStatus());
+ AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+ String exchangedAccessToken = tokenResponse.getToken();
+ JWSInput jws = new JWSInput(tokenResponse.getToken());
+ AccessToken token = jws.readJsonContent(AccessToken.class);
+ response.close();
+
+ String exchanged2UserId = token.getSubject();
+ String exchanged2Username = token.getPreferredUsername();
+
+ // assert that we get the same linked account as was previously imported
+
+ Assert.assertEquals(exchangedUserId, exchanged2UserId);
+ Assert.assertEquals(exchangedUsername, exchanged2Username);
+
+ // test logout
+ response = childLogoutWebTarget(httpClient)
+ .queryParam("id_token_hint", exchangedAccessToken)
+ .request()
+ .get();
+ response.close();
+
+ Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
+
+ List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
+ Assert.assertEquals(1, links.size());
+ }
+ // cleanup remove the user
+ childRealm.users().get(exchangedUserId).remove();
+
+ {
+ // test unauthorized client gets 403
Response response = exchangeUrl.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(UNAUTHORIZED_CHILD_CLIENT, "password"))
.post(Entity.form(