diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index dbf55f2addd5..db0385e9caa6 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -67,6 +67,8 @@
import java.util.Map;
import java.util.function.BiConsumer;
+import static org.keycloak.protocol.oidc.par.endpoints.ParEndpoint.PAR_DPOP_PROF_JKT;
+
/**
* @author Stian Thorgersen
*/
@@ -185,6 +187,12 @@ private Response process(final MultivaluedMap params) {
return redirectErrorToClient(parsedResponseMode, ex.getError(), ex.getErrorDescription());
}
+ // If DPoP Proof existed with PAR request, its public key needs to be matched with the one with Token Request afterward
+ String dpopJkt = session.getAttribute(PAR_DPOP_PROF_JKT, String.class);
+ if (request.getDpopJkt() == null) {
+ request.setDpopJkt(dpopJkt);
+ }
+
try {
session.clientPolicy().triggerOnEvent(new AuthorizationRequestContext(parsedResponseType, request, redirectUri, params));
} catch (ClientPolicyException cpe) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java
index c382b6deae94..cf089fef87a6 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java
@@ -44,11 +44,13 @@
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.cors.Cors;
import org.keycloak.services.messages.Messages;
+import org.keycloak.services.util.DPoPUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
import org.keycloak.utils.StringUtil;
@@ -293,6 +295,21 @@ public void checkParRequired() throws AuthorizationCheckException {
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
+ public void checkParDPoPParams() throws AuthorizationCheckException {
+ DPoP dpop = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
+ if (dpop == null) {
+ return;
+ }
+ if (request.getDpopJkt() != null) {
+ if (!request.getDpopJkt().equals(dpop.getThumbprint())) {
+ String errorMessage = "DPoP Proof public key thumbprint does not match dpop_jkt.";
+ event.detail(Details.REASON, errorMessage);
+ event.error(Errors.INVALID_REQUEST);
+ throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
+ }
+ }
+ }
+
// https://tools.ietf.org/html/rfc7636#section-4
private boolean isValidPkceCodeChallenge(String codeChallenge) {
if (codeChallenge.length() < OIDCLoginProtocol.PKCE_CODE_CHALLENGE_MIN_LENGTH) {
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
index a8d213644333..9de8517e2188 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
@@ -137,6 +137,8 @@ public String getCodeChallengeMethod() {
public String getDpopJkt() { return dpopJkt; }
+ public void setDpopJkt(String dpopJkt) { this.dpopJkt = dpopJkt; }
+
public String getDisplay() {
return display;
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java
index 9c46fe7061f5..a4bcbc6e75bf 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/ParEndpoint.java
@@ -26,14 +26,18 @@
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.SingleUseObjectProvider;
+import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.par.ParResponse;
import org.keycloak.protocol.oidc.par.clientpolicy.context.PushedAuthorizationRequestContext;
import org.keycloak.protocol.oidc.par.endpoints.request.ParEndpointRequestParserProcessor;
+import org.keycloak.representations.dpop.DPoP;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.cors.Cors;
+import org.keycloak.services.util.DPoPUtil;
import org.keycloak.utils.ProfileHelper;
import jakarta.ws.rs.Consumes;
@@ -57,6 +61,7 @@
public class ParEndpoint extends AbstractParEndpoint {
public static final String PAR_CREATED_TIME = "par.created.time";
+ public static final String PAR_DPOP_PROF_JKT = "par.dpop.prof.jkt";
private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
public static final int REQUEST_URI_PREFIX_LENGTH = REQUEST_URI_PREFIX.length();
@@ -96,6 +101,11 @@ public Response request() {
throw throwErrorResponseException(OAuthErrorException.INVALID_REQUEST, "It is not allowed to include request_uri to PAR.", Response.Status.BAD_REQUEST);
}
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-10.1
+ DPoPUtil.retrieveDPoPHeaderIfPresent(session, event, cors).ifPresent(dPoP -> {
+ session.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
+ });
+
try {
authorizationRequest = ParEndpointRequestParserProcessor.parseRequest(event, session, client, decodedFormParameters);
} catch (Exception e) {
@@ -137,6 +147,7 @@ public Response request() {
checker.checkOIDCRequest();
checker.checkOIDCParams();
checker.checkPKCEParams();
+ checker.checkParDPoPParams();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
ex.throwAsCorsErrorResponseException(cors);
}
@@ -157,6 +168,11 @@ public Response request() {
flattenDecodedFormParametersToParamsMap(decodedFormParameters, params);
params.put(PAR_CREATED_TIME, String.valueOf(System.currentTimeMillis()));
+ // If DPoP Proof exists, its public key needs to be matched with the one with Token Request afterward
+ DPoP dpop = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class);
+ if (dpop != null) {
+ params.put(PAR_DPOP_PROF_JKT, dpop.getThumbprint());
+ }
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
singleUseStore.put(key, expiresIn, params);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java
index 57882c408f3a..989821fc88ba 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/par/endpoints/request/AuthzEndpointParParser.java
@@ -32,6 +32,7 @@
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
import static org.keycloak.protocol.oidc.par.endpoints.ParEndpoint.PAR_CREATED_TIME;
+import static org.keycloak.protocol.oidc.par.endpoints.ParEndpoint.PAR_DPOP_PROF_JKT;
/**
* Parse the parameters from PAR
@@ -70,6 +71,11 @@ public AuthzEndpointParParser(KeycloakSession session, ClientModel client, Strin
} else {
throw new RuntimeException("PAR expired.");
}
+ // If DPoP Proof existed with PAR request, its public key needs to be matched with the one with Token Request afterward
+ String dpopJkt = retrievedRequest.get(PAR_DPOP_PROF_JKT);
+ if (dpopJkt != null) {
+ session.setAttribute(PAR_DPOP_PROF_JKT, dpopJkt);
+ }
}
@Override
diff --git a/services/src/main/java/org/keycloak/services/util/DPoPUtil.java b/services/src/main/java/org/keycloak/services/util/DPoPUtil.java
index 18ed9425ae02..176280f90b6d 100644
--- a/services/src/main/java/org/keycloak/services/util/DPoPUtil.java
+++ b/services/src/main/java/org/keycloak/services/util/DPoPUtil.java
@@ -170,6 +170,31 @@ public static Optional retrieveDPoPHeaderIfPresent(KeycloakSession keycloa
}
}
+ public static Optional retrieveDPoPHeaderIfPresent(KeycloakSession keycloakSession,
+ EventBuilder event,
+ Cors cors) {
+ boolean isDPoPSupported = Profile.isFeatureEnabled(Profile.Feature.DPOP);
+ if (!isDPoPSupported) {
+ return Optional.empty();
+ }
+
+ HttpRequest request = keycloakSession.getContext().getHttpRequest();
+ final boolean isDpopHeaderPresent = request.getHttpHeaders().getHeaderString(DPoPUtil.DPOP_HTTP_HEADER) != null;
+ if (!isDpopHeaderPresent) {
+ return Optional.empty();
+ }
+
+ try {
+ DPoP dPoP = new DPoPUtil.Validator(keycloakSession).request(request).uriInfo(keycloakSession.getContext().getUri()).validate();
+ keycloakSession.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
+ return Optional.of(dPoP);
+ } catch (VerificationException ex) {
+ event.detail(Details.REASON, ex.getMessage());
+ event.error(Errors.INVALID_DPOP_PROOF);
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, ex.getMessage(), Response.Status.BAD_REQUEST);
+ }
+ }
+
private static DPoP validateDPoP(KeycloakSession session, URI uri, String method, String token, String accessToken, int lifetime, int clockSkew) throws VerificationException {
if (token == null || token.trim().equals("")) {