Merge pull request #4494 from patriot1burke/master

KEYCLOAK-5516
This commit is contained in:
Bill Burke 2017-09-22 16:38:13 -04:00 committed by GitHub
commit 537081ec9d
6 changed files with 116 additions and 34 deletions

View file

@ -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);
}

View file

@ -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();
}

View file

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

View file

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

View file

@ -103,7 +103,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
}
@Override
public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
public Response exchangeFromToken(UriInfo uriInfo, EventBuilder builder, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
return exchangeUnsupportedRequiredType();

View file

@ -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(