Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ public enum Feature {

TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW, 1),
TOKEN_EXCHANGE_STANDARD_V2("Standard Token Exchange version 2", Type.EXPERIMENTAL, 2),
TOKEN_EXCHANGE_FEDERATED_V2("Federated Token Exchange for external-internal and internal-external token exchange", Type.EXPERIMENTAL, 2, Feature.ADMIN_FINE_GRAINED_AUTHZ),
TOKEN_EXCHANGE_SUBJECT_IMPERSONATION_V2("Subject impersonation Token Exchange", Type.EXPERIMENTAL, 2, Feature.ADMIN_FINE_GRAINED_AUTHZ),

WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,22 @@ public class TokenExchangeContext {
private final KeycloakSession session;
private final MultivaluedMap<String, String> formParams;

// TODO: resolve deps issue and use correct types
private final Cors cors;
private final Object tokenManager;

private final ClientModel client;
private final RealmModel realm;
private final EventBuilder event;

private ClientConnection clientConnection;
private HttpHeaders headers;
private Map<String, String> clientAuthAttributes;
private final ClientConnection clientConnection;
private final HttpHeaders headers;
private final Map<String, String> clientAuthAttributes;

private final Params params = new Params();

// Reason why the particular tokenExchange provider cannot be supported
private String unsupportedReason;

public TokenExchangeContext(KeycloakSession session,
MultivaluedMap<String, String> formParams,
Cors cors,
Expand Down Expand Up @@ -121,6 +123,14 @@ public Params getParams() {
return params;
}

public String getUnsupportedReason() {
return unsupportedReason;
}

public void setUnsupportedReason(String unsupportedReason) {
this.unsupportedReason = unsupportedReason;
}

public class Params {

public String getActorToken() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import jakarta.ws.rs.core.Response;

import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.protocol.oidc.TokenExchangeProvider;
Expand Down Expand Up @@ -66,7 +68,15 @@ public Response process(Context context) {
.map(f -> session.getProvider(TokenExchangeProvider.class, f.getId()))
.filter(p -> p.supports(exchange))
.findFirst()
.orElseThrow(() -> new InternalServerErrorException("No token exchange provider available"));
.orElseThrow(() -> {
if (exchange.getUnsupportedReason() != null) {
event.detail(Details.REASON, exchange.getUnsupportedReason());
event.error(Errors.INVALID_REQUEST);
return new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, exchange.getUnsupportedReason(), Response.Status.BAD_REQUEST);
} else {
return new InternalServerErrorException("No token exchange provider available");
}
});

try {
//trigger if there is a supported token exchange provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ public OAuth2GrantType create(KeycloakSession session) {
@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE)
|| Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE_STANDARD_V2)
|| Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE_FEDERATED_V2)
|| Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE_SUBJECT_IMPERSONATION_V2);
|| Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE_STANDARD_V2);
}

@Override
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
Expand All @@ -47,7 +46,6 @@
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.TokenExchangeContext;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.grants.TokenExchangeGrantTypeFactory;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
Expand All @@ -70,43 +68,62 @@ public class StandardTokenExchangeProvider extends AbstractTokenExchangeProvider

@Override
public boolean supports(TokenExchangeContext context) {
return true;
}
// Subject impersonation request
String requestedSubject = context.getFormParams().getFirst(OAuth2Constants.REQUESTED_SUBJECT);
if (requestedSubject != null) {
context.setUnsupportedReason("Parameter 'requested_subject' is not supported for standard token exchange");
return false;
}

@Override
protected Response tokenExchange() {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
ClientConnection clientConnection = context.getClientConnection();
Cors cors = context.getCors();
EventBuilder event = context.getEvent();
// Internal-external token exchange
String requestedIssuer = context.getFormParams().getFirst(OAuth2Constants.REQUESTED_ISSUER);
if (requestedIssuer != null) {
context.setUnsupportedReason("Parameter 'requested_issuer' is not supported for standard token exchange");
return false;
}

// External-internal token exchange
String subjectIssuer = context.getFormParams().getFirst(OAuth2Constants.SUBJECT_ISSUER);
if (subjectIssuer != null) {
context.setUnsupportedReason("Parameter 'subject_issuer' is not supported for standard token exchange");
return false;
}

if(!OIDCAdvancedConfigWrapper.fromClientModel(context.getClient()).isStandardTokenExchangeEnabled()) {
event.detail(Details.REASON, "Standard token exchange is not enabled for the requested client");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Standard token exchange is not enabled for the requested client", Response.Status.BAD_REQUEST);
context.setUnsupportedReason("Standard token exchange is not enabled for the requested client");
return false;
}

String subjectToken = context.getParams().getSubjectToken();
if (subjectToken == null) {
event.detail(Details.REASON, "subject_token parameter not provided");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "subject_token parameter not provided", Response.Status.BAD_REQUEST);
context.setUnsupportedReason("Parameter 'subject_token' required for standard token exchange");
return false;
}

String subjectTokenType = context.getParams().getSubjectTokenType();
if (subjectTokenType == null) {
event.detail(Details.REASON, "subject_token_type parameter not provided");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "subject_token_type parameter not provided", Response.Status.BAD_REQUEST);
context.setUnsupportedReason("Parameter 'subject_token_type' required for standard token exchange");
return false;
}

if (!subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
event.detail(Details.REASON, "subject_token supports access tokens only");
event.error(Errors.INVALID_TOKEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);

context.setUnsupportedReason("Parameter 'subject_token' supports access tokens only");
return false;
}

return true;
}

@Override
protected Response tokenExchange() {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
ClientConnection clientConnection = context.getClientConnection();
Cors cors = context.getCors();
EventBuilder event = context.getEvent();

String subjectToken = context.getParams().getSubjectToken();

AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, session.getContext().getUri(), clientConnection, true, true, null, false, subjectToken, context.getHeaders());
if (authResult == null) {
event.detail(Details.REASON, "subject_token validation failure");
Expand All @@ -118,21 +135,6 @@ protected Response tokenExchange() {
UserSessionModel tokenSession = authResult.getSession();
AccessToken token = authResult.getToken();


String requestedSubject = context.getFormParams().getFirst(OAuth2Constants.REQUESTED_SUBJECT);
if (requestedSubject != null) {
event.detail(Details.REASON, "Parameter '" + OAuth2Constants.REQUESTED_SUBJECT + "' not supported for standard token exchange");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Parameter '" + OAuth2Constants.REQUESTED_SUBJECT + "' not supported for standard token exchange", Response.Status.BAD_REQUEST);
}

String requestedIssuer = context.getFormParams().getFirst(OAuth2Constants.REQUESTED_ISSUER);
if (requestedIssuer != null) {
event.detail(Details.REASON, "Parameter '" + OAuth2Constants.REQUESTED_ISSUER + "' not supported for standard token exchange");
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Parameter '" + OAuth2Constants.REQUESTED_ISSUER + "' not supported for standard token exchange", Response.Status.BAD_REQUEST);
}

return exchangeClientToClient(tokenUser, tokenSession, token, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public boolean isSupported(Config.Scope config) {

@Override
public int order() {
// Smaller priority than other V2 providers (due other providers can be detected based on parameters), but bigger than V1, so it has preference if both V1 and V2 enabled
return 1;
// Bigger priority than V1, so it has preference if both V1 and V2 enabled
return 10;
}
}
Loading
Loading