8000 DPoP-binding only for refresh token by graziang · Pull Request #42783 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ public AccessTokenResponseBuilder generateRefreshToken(RefreshToken oldRefreshTo

private void generateRefreshToken(boolean offlineTokenRequested) {
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
final AccessToken.Confirmation confirmation = getConfirmation(clientSession, accessToken);
AccessToken.Confirmation confirmation = getConfirmation(clientSession, accessToken);
refreshToken = new RefreshToken(accessToken, confirmation);
refreshToken.id(SecretGenerator.getInstance().generateSecureID());
refreshToken.issuedNow();
Expand All @@ -1189,6 +1189,15 @@ private void generateRefreshToken(boolean offlineTokenRequested) {
.map(ClientModel::getClientId)
.collect(Collectors.toSet()));
}
Boolean bindOnlyRefreshToken = session.getAttributeOrDefault(DPoPUtil.DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, false);
if (bindOnlyRefreshToken) {
DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
if (dPoP != null) {
confirmation = new AccessToken.Confirmation();
confirmation.setKeyThumbprint(dPoP.getThumbprint());
refreshToken.setConfirmation(confirmation);
}
}
}

public void createOrUpdateOfflineSession() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public Response process(Context context) {
DPoPUtil.validateDPoPJkt(codeData.getDpopJkt(), session, event, cors);

try {
session.clientPolicy().triggerOnEvent(new TokenRequestContext(formParams, parseResult));
session.clientPolicy().triggerOnEvent(new TokenRequestContext(formParams, parseResult, client));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, Details.CLIENT_POLICY_ERROR);
event.detail(Details.CLIENT_POLICY_ERROR, cpe.getError());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public Response process(Context context) {
String scopeParameter = getRequestedScopes();

try {
session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams));
session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams, client));
refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, Details.CLIENT_POLICY_ERROR);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import jakarta.ws.rs.core.MultivaluedMap;

import org.keycloak.models.ClientModel;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;

Expand All @@ -28,9 +29,11 @@
public class TokenRefreshContext implements ClientPolicyContext {

private final MultivaluedMap<String, String> params;
private final ClientModel client;

public TokenRefreshContext(MultivaluedMap<String, String> params) {
public TokenRefreshContext(MultivaluedMap<String, String> params, ClientModel client) {
this.params = params;
this.client = client;
}

@Override
Expand All @@ -42,4 +45,7 @@ public MultivaluedMap<String, String> getParams() {
return params;
}

public ClientModel getClient() {
return client;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import jakarta.ws.rs.core.MultivaluedMap;

import org.keycloak.models.ClientModel;
import org.keycloak.protocol.oidc.utils.OAuth2CodeParser;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
Expand All @@ -30,11 +31,12 @@ public class TokenRequestContext implements ClientPolicyContext {

private final MultivaluedMap<String, String> params;
private final OAuth2CodeParser.ParseResult parseResult;
private final ClientModel client;

public TokenRequestContext(MultivaluedMap<String, String> params,
OAuth2CodeParser.ParseResult parseResult) {
public TokenRequestContext(MultivaluedMap<String, String> params, OAuth2CodeParser.ParseResult parseResult, ClientModel client) {
this.params = params;
this.parseResult = parseResult;
this.client = client;
}

@Override
Expand All @@ -50,4 +52,7 @@ public OAuth2CodeParser.ParseResult getParseResult() {
return parseResult;
}

public ClientModel getClient() {
return client;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.keycloak.common.VerificationException;
import org.keycloak.common.Profile.Feature;
import org.keycloak.http.HttpRequest;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.AccessToken;
Expand All @@ -34,6 +35,8 @@
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
import org.keycloak.services.clientpolicy.context.ClientCRUDContext;
import org.keycloak.services.clientpolicy.context.TokenRefreshContext;
import org.keycloak.services.clientpolicy.context.TokenRequestContext;
import org.keycloak.services.clientpolicy.context.TokenRevokeContext;
import org.keycloak.services.util.DPoPUtil;

Expand Down Expand Up @@ -69,6 +72,9 @@ public static class Configuration extends ClientPolicyExecutorConfigurationRepre
@JsonProperty("enforce-authorization-code-binding-to-dpop")
protected Boolean enforceAuthorizationCodeBindingToDpop;

@JsonProperty("allow-only-refresh-token-binding")
protected Boolean allowOnlyRefreshTokenBinding;

public Boolean isAutoConfigure() {
return autoConfigure;
}
Expand All @@ -84,6 +90,14 @@ public Boolean getEnforceAuthorizationCodeBindingToDpop() {
public void setEnforceAuthorizationCodeBindingToDpop(Boolean enforceAuthorizationCodeBindingToDpop) {
this.enforceAuthorizationCodeBindingToDpop = enforceAuthorizationCodeBindingToDpop;
}

public Boolean getAllowOnlyRefreshTokenBinding() {
return allowOnlyRefreshTokenBinding;
}

public void setAllowOnlyRefreshTokenBinding(Boolean allowOnlyRefreshTokenBinding) {
this.allowOnlyRefreshTokenBinding = allowOnlyRefreshTokenBinding;
}
}

@Override
Expand Down Expand Up @@ -112,7 +126,13 @@ public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyExcep
checkOnAuthorizationRequest(authzRequestContext);
break;
case TOKEN_REQUEST:
TokenRequestContext authorizationRequestContext = (TokenRequestContext) context;
validateAndBindOnlyRefreshToken(authorizationRequestContext.getClient());
break;
case TOKEN_REFRESH:
TokenRefreshContext tokenRefreshContext = (TokenRefreshContext) context;
validateAndBindOnlyRefreshToken(tokenRefreshContext.getClient());
break;
case USERINFO_REQUEST:
case BACKCHANNEL_TOKEN_REQUEST:
// Codes for processing these requests verifies DPoP.
Expand Down Expand Up @@ -156,6 +176,19 @@ private void checkTokenRevoke(TokenRevokeContext context, HttpRequest request) t
validateBinding(token, dPoP);
}

private void validateAndBindOnlyRefreshToken(ClientModel client) throws ClientPolicyException {
boolean useDPoPToken = OIDCAdvancedConfigWrapper.fromClientModel(client).isUseDPoP();
if (useDPoPToken || configuration.getAllowOnlyRefreshTokenBinding() == null || !configuration.getAllowOnlyRefreshTokenBinding() || !client.isPublicClient()) {
return;
}

DPoP dPoP = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
if (dPoP == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_DPOP_PROOF, "DPoP proof is missing");
}
session.setAttribute(DPoPUtil.DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, true);
}

private void checkOnAuthorizationRequest(AuthorizationRequestContext authzRequestContext) throws ClientPolicyException {
if (configuration.getEnforceAuthorizationCodeBindingToDpop() != null && configuration.getEnforceAuthorizationCodeBindingToDpop() && (authzRequestContext.getAuthorizationEndpointRequest().getDpopJkt() == null)) {
// Checking only the presence of the parameter here. As long as parameter is present, it is automatically saved to authenticationSession and checked later in token request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,19 @@ public class DPoPBindEnforcerExecutorFactory implements ClientPolicyExecutorPro

public static final String ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP = "enforce-authorization-code-binding-to-dpop";

public static final String ALLOW_ONLY_REFRESH_BINDING = "allow-only-refresh-token-binding";

private static final ProviderConfigProperty AUTO_CONFIGURE_PROPERTY = new ProviderConfigProperty(
AUTO_CONFIGURE, "Auto-configure", "If On, then the during client creation or update, the configuration of the client will be auto-configured to use DPoP bind token", ProviderConfigProperty.BOOLEAN_TYPE, false);

private static final ProviderConfigProperty ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP_KEY = new ProviderConfigProperty(
ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP, "Enforce Authorization Code binding to DPoP key", "If On, then there is enforced authorization code binding to DPoP key. This means that parameter 'dpop_jkt' will be required in the OIDC/OAuth2 authentication requests and will be verified during token request if it matches DPoP proof. When this is false, it is still possible to use 'dpop_jkt' parameter, but it will not be required", ProviderConfigProperty.BOOLEAN_TYPE, false);

private static final ProviderConfigProperty ALLOW_ONLY_REFRESH_BINDING_PROPERTY = new ProviderConfigProperty(
ALLOW_ONLY_REFRESH_BINDING, "Bind only refresh token for public client", "If On and the client is public the DPoP binding is enforced only for refresh token, this option is ignored if the DPoP is enforced in client settings or if the client is not public",
ProviderConfigProperty.BOOLEAN_TYPE, false);
10BC8

@Override
public ClientPolicyExecutorProvider create(KeycloakSession session) {
return new DPoPBindEnforcerExecutor(session);
Expand Down Expand Up @@ -67,6 +74,6 @@ public String getHelpText() {

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of(AUTO_CONFIGURE_PROPERTY, ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP_KEY);
return List.of(AUTO_CONFIGURE_PROPERTY, ENFORCE_AUTHORIZATION_CODE_BINDING_TO_DPOP_KEY, ALLOW_ONLY_REFRESH_BINDING_PROPERTY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public class DPoPUtil {
public static final String DPOP_TOKEN_TYPE = "DPoP";
public static final String DPOP_SCHEME = "DPoP";
public final static String DPOP_SESSION_ATTRIBUTE = "dpop";
public final static String DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE = "dpop-binding-only-refresh-token";

public enum Mode {
ENABLED,
Expand Down Expand Up @@ -595,6 +596,11 @@ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel m
if (dPoP == null) {
return super.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);
}

Boolean bindOnlyRefreshToken = session.getAttributeOrDefault(DPOP_BINDING_ONLY_REFRESH_TOKEN_SESSION_ATTRIBUTE, false);
if (bindOnlyRefreshToken) {
return super.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);
}
AccessToken.Confirmation confirmation = (AccessToken.Confirmation) token.getOtherClaims()
.get(OAuth2Constants.CNF);
if (confirmation == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@
"executor": "dpop-bind-enforcer",
"configuration": {
"auto-configure": "true",
"enforce-authorization-code-binding-to-dpop": "false"
"enforce-authorization-code-binding-to-dpop": "false",
"allow-only-refresh-token-binding": "false"
}
},
9868 {
Expand Down Expand Up @@ -456,7 +457,8 @@
"executor": "dpop-bind-enforcer",
"configuration": {
"auto-configure": "true",
"enforce-authorization-code-binding-to-dpop": "false"
"enforce-authorization-code-binding-to-dpop": "false",
"allow-only-refresh-token-binding": "false"
}
}
]
Expand Down Expand Up @@ -535,7 +537,8 @@
"executor": "dpop-bind-enforcer",
"configuration": {
"auto-configure": "true",
"enforce-authorization-code-binding-to-dpop": "false"
"enforce-authorization-code-binding-to-dpop": "false",
"allow-only-refresh-token-binding": "false"
}
},
{
Expand Down
Loading
0