Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
Signed-off-by: Takashi Norimatsu <[email protected]>
  • Loading branch information
tnorimat committed Nov 14, 2024
1 parent a8ce16e commit fc4030e
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="mailto:[email protected]">Stian Thorgersen</a>
*/
Expand Down Expand Up @@ -185,6 +187,12 @@ private Response process(final MultivaluedMap<String, String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -137,6 +147,7 @@ public Response request() {
checker.checkOIDCRequest();
checker.checkOIDCParams();
checker.checkPKCEParams();
checker.checkParDPoPParams();
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
ex.throwAsCorsErrorResponseException(cors);
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions services/src/main/java/org/keycloak/services/util/DPoPUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,31 @@ public static Optional<DPoP> retrieveDPoPHeaderIfPresent(KeycloakSession keycloa
}
}

public static Optional<DPoP> 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("")) {
Expand Down

0 comments on commit fc4030e

Please sign in to comment.