KEYCLOAK-5516
This commit is contained in:
parent
8ace0e68c3
commit
eb4f7f3b21
5 changed files with 115 additions and 33 deletions
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.broker.provider;
|
package org.keycloak.broker.provider;
|
||||||
|
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
@ -38,5 +39,5 @@ public interface ExchangeTokenToIdentityProviderToken {
|
||||||
* @param params form parameters received for requested exchange
|
* @param params form parameters received for requested exchange
|
||||||
* @return
|
* @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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,26 +148,30 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
// 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
|
// 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;
|
if (tokenResponse != null) return tokenResponse;
|
||||||
|
|
||||||
// going further we only support access token type? Why?
|
// going further we only support access token type? Why?
|
||||||
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
||||||
if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_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();
|
return exchangeUnsupportedRequiredType();
|
||||||
}
|
}
|
||||||
if (!getConfig().isStoreToken()) {
|
if (!getConfig().isStoreToken()) {
|
||||||
// if token isn't stored, we need to see if this session has been linked
|
// if token isn't stored, we need to see if this session has been linked
|
||||||
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
|
String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
|
||||||
if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
|
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 exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
return exchangeSessionToken(uriInfo, event, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
} else {
|
} 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
|
* @param params
|
||||||
* @return
|
* @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))) {
|
if (getConfig().getAlias().equals(tokenUserSession.getNote(OIDCIdentityProvider.EXCHANGE_PROVIDER))) {
|
||||||
|
|
||||||
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
|
||||||
|
@ -193,6 +197,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
tokenResponse.setExpiresIn(0);
|
tokenResponse.setExpiresIn(0);
|
||||||
tokenResponse.getOtherClaims().clear();
|
tokenResponse.getOtherClaims().clear();
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
} else if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedType)) {
|
} else if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedType)) {
|
||||||
|
@ -206,6 +211,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
tokenResponse.setExpiresIn(0);
|
tokenResponse.setExpiresIn(0);
|
||||||
tokenResponse.getOtherClaims().clear();
|
tokenResponse.getOtherClaims().clear();
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE);
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,15 +221,19 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
return null;
|
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());
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
if (model == null || model.getToken() == null) {
|
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);
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
|
String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
model.setToken(null);
|
model.setToken(null);
|
||||||
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
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);
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
@ -234,12 +244,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
tokenResponse.getOtherClaims().clear();
|
tokenResponse.getOtherClaims().clear();
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
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);
|
String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
|
event.detail(Details.REASON, "requested_issuer is not linked");
|
||||||
|
event.error(Errors.INVALID_TOKEN);
|
||||||
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
|
@ -250,6 +263,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
tokenResponse.getOtherClaims().clear();
|
tokenResponse.getOtherClaims().clear();
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -229,10 +229,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
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());
|
FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
|
||||||
if (model == null || model.getToken() == null) {
|
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);
|
return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -252,6 +254,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
||||||
model.setToken(null);
|
model.setToken(null);
|
||||||
session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
|
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);
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
@ -280,18 +284,26 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
tokenResponse.getOtherClaims().clear();
|
tokenResponse.getOtherClaims().clear();
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(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 {
|
try {
|
||||||
long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
|
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()) {
|
if (expiration == 0 || expiration > Time.currentTime()) {
|
||||||
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
AccessTokenResponse tokenResponse = new AccessTokenResponse();
|
||||||
tokenResponse.setExpiresIn(expiration);
|
tokenResponse.setExpiresIn(expiration);
|
||||||
|
@ -301,6 +313,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
tokenResponse.setRefreshExpiresIn(0);
|
tokenResponse.setRefreshExpiresIn(0);
|
||||||
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
}
|
}
|
||||||
String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
|
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();
|
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
|
||||||
if (response.contains("error")) {
|
if (response.contains("error")) {
|
||||||
logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
|
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);
|
return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
|
||||||
}
|
}
|
||||||
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
|
||||||
|
@ -324,6 +339,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
newResponse.getOtherClaims().clear();
|
newResponse.getOtherClaims().clear();
|
||||||
newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
|
||||||
newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
|
||||||
|
event.success();
|
||||||
return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
|
|
@ -608,6 +608,7 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
|
String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
|
||||||
if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_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);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
|
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);
|
AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
|
||||||
if (authResult == null) {
|
if (authResult == null) {
|
||||||
|
event.detail(Details.REASON, "subject_token validation failure");
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
@ -634,7 +636,7 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
if (requestedUser == null) {
|
if (requestedUser == null) {
|
||||||
// We always returned access denied to avoid username fishing
|
// 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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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.
|
// for this case, the user represented by the token, must have permission to impersonate.
|
||||||
AdminAuth auth = new AdminAuth(realm, token, tokenUser, client);
|
AdminAuth auth = new AdminAuth(realm, token, tokenUser, client);
|
||||||
if (!AdminPermissions.evaluator(session, realm, auth).users().canImpersonate(requestedUser)) {
|
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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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
|
// no token is being exchanged, this is a direct exchange. Client must be authenticated, not public, and must be allowed
|
||||||
// to impersonate
|
// to impersonate
|
||||||
if (client.isPublicClient()) {
|
if (client.isPublicClient()) {
|
||||||
logger.debug("Public clients cannot exchange tokens");
|
event.detail(Details.REASON, "public clients not allowed");
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
||||||
|
|
||||||
}
|
}
|
||||||
if (!AdminPermissions.management(session, realm).users().canClientImpersonate(client, requestedUser)) {
|
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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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);
|
event.detail(Details.REQUESTED_ISSUER, requestedIssuer);
|
||||||
IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
|
IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
|
||||||
if (providerModel == null) {
|
if (providerModel == null) {
|
||||||
|
event.detail(Details.REASON, "unknown requested_issuer");
|
||||||
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
|
IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
|
||||||
if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
|
if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
|
||||||
|
event.detail(Details.REASON, "exchange unsupported by requested_issuer");
|
||||||
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
|
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)) {
|
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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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();
|
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) {
|
if (requestedTokenType == null) {
|
||||||
requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
|
requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
|
||||||
} else if (!requestedTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE) && !requestedTokenType.equals(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);
|
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;
|
ClientModel targetClient = client;
|
||||||
|
@ -719,12 +724,13 @@ public class TokenEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetClient.isConsentRequired()) {
|
if (targetClient.isConsentRequired()) {
|
||||||
|
event.detail(Details.REASON, "audience requires consent");
|
||||||
event.error(Errors.CONSENT_DENIED);
|
event.error(Errors.CONSENT_DENIED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
|
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)) {
|
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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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);
|
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())) {
|
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);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
|
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);
|
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 = session.users().addUser(realm, username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
|
|
@ -451,6 +451,8 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
|
||||||
@Test
|
@Test
|
||||||
public void testExternalExchange() throws Exception {
|
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();
|
String accessToken = oauth.doGrantAccessTokenRequest(PARENT_IDP, PARENT2_USERNAME, "password", null, PARENT_CLIENT, "password").getAccessToken();
|
||||||
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
|
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.USE_JWKS_URL, String.valueOf(true));
|
||||||
rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl());
|
rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl());
|
||||||
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
|
adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
|
||||||
|
|
||||||
|
String exchangedUserId = null;
|
||||||
|
String exchangedUsername = null;
|
||||||
|
|
||||||
{
|
{
|
||||||
// valid exchange
|
// valid exchange
|
||||||
Response response = exchangeUrl.request()
|
Response response = exchangeUrl.request()
|
||||||
|
@ -502,11 +508,11 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
|
||||||
AccessToken token = jws.readJsonContent(AccessToken.class);
|
AccessToken token = jws.readJsonContent(AccessToken.class);
|
||||||
response.close();
|
response.close();
|
||||||
|
|
||||||
String childUserId = token.getSubject();
|
exchangedUserId = token.getSubject();
|
||||||
String username = token.getPreferredUsername();
|
exchangedUsername = token.getPreferredUsername();
|
||||||
|
|
||||||
System.out.println("childUserId: " + childUserId);
|
System.out.println("exchangedUserId: " + exchangedUserId);
|
||||||
System.out.println("username: " + username);
|
System.out.println("exchangedUsername: " + exchangedUsername);
|
||||||
|
|
||||||
|
|
||||||
// test that we can exchange back to external token
|
// 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());
|
Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
|
||||||
|
|
||||||
|
|
||||||
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
|
List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
|
||||||
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
|
|
||||||
Assert.assertEquals(1, links.size());
|
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()
|
Response response = exchangeUrl.request()
|
||||||
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(UNAUTHORIZED_CHILD_CLIENT, "password"))
|
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(UNAUTHORIZED_CHILD_CLIENT, "password"))
|
||||||
.post(Entity.form(
|
.post(Entity.form(
|
||||||
|
|
Loading…
Reference in a new issue