8000 DPoP: Refresh token created with DPoP can be refreshed without proof by tnorimat · Pull Request #38136 · keycloak/keycloak · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

DPoP: Refresh token created with DPoP can be refreshed without proof #38136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 17, 2025
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 @@ -104,7 +104,7 @@ DPoP binds an access token and a refresh token together with the public part of

This type of token is a holder-of-key token. Unlike bearer tokens, the recipient of a holder-of-key token can verify if the sender of the token is legitimate.

If the client switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is on, the workflow is:
If the client switch `Require Demonstrating Proof of Possession (DPoP) header in token requests` is on, the workflow is:

. A token request is sent to the token endpoint in an authorization code flow or hybrid flow.
. {project_name} requests a DPoP proof.
Expand All @@ -113,7 +113,7 @@ If the client switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is on, the wor

If verification fails, {project_name} rejects the token.

If the switch `OAuth 2.0 DPoP Bound Access Tokens Enabled` is off, the client can still send `DPoP` proof in the token request. In that case, {project_name} will verify DPoP proof
If the switch `Require Demonstrating Proof of Possession (DPoP) header in token requests` is off, the client can still send `DPoP` proof in the token request. In that case, {project_name} will verify DPoP proof
and will add the thumbprint to the token. But if the switch is off, DPoP binding is not enforced by the {project_name} server for this client. It is recommended to have this switch
on if you want to make sure that particular client always uses DPoP binding.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ policyUrl=Policy URL
clientDescriptionHelp=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}.
rolesPermissionsHint=Determines if fine grained permissions are enabled for managing this role. Disabling will delete all current permissions that have been set up.
passwordPoliciesHelp.regexPattern=Requires that the password matches one or more defined Java regular expression patterns.
oAuthDPoP=OAuth 2.0 DPoP Bound Access Tokens Enabled
oAuthDPoP=Require Demonstrating Proof of Possession (DPoP) header in token requests
invalidRealmName=Realm name can't contain special characters
validRedirectURIsHelp=Valid URI pattern a browser can redirect to after a successful login. Simple wildcards are allowed such as 'http\://example.com/*'. Relative path can be specified too such as /my/relative/path/*. Relative paths are relative to the client root URL, or if none is specified the auth server root URL is used. For SAML, you must set valid URI patterns if you are relying on the consumer service URL embedded with the login request.
realmNameTitle={{name}} realm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,11 @@ public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm
}

if (Profile.isFeatureEnabled(Profile.Feature.DPOP)) {
DPoP dPoP = (DPoP) session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE);
if (client.isPublicClient() && (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseDPoP() || dPoP != null )) {
if (DPoPUtil.isDPoPToken(refreshToken)) {
DPoP dPoP = (DPoP) session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE);
if (dPoP == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "DPoP proof is missing");
}
try {
DPoPUtil.validateBinding(refreshToken, dPoP);
} catch (VerificationException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import jakarta.ws.rs.core.UriInfo;

import org.apache.commons.codec.binary.Hex;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.TokenVerifier;
Expand Down Expand Up @@ -88,7 +87,7 @@ public class DPoPUtil {
public static final String DPOP_SCHEME = "DPoP";
public final static String DPOP_SESSION_ATTRIBUTE = "dpop";

public static enum Mode {
public enum Mode {
ENABLED,
OPTIONAL,
DISABLED
Expand Down Expand Up @@ -197,7 +196,7 @@ public static Optional<DPoP> retrieveDPoPHeaderIfPresent(KeycloakSession keycloa

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("")) {
if (token == null || token.trim().isEmpty()) {
throw new VerificationException("DPoP proof is missing");
}

Expand Down Expand Up @@ -310,6 +309,10 @@ public static void validateDPoPJkt(String dpopJkt, KeycloakSession session, Even

}

public static boolean isDPoPToken(AccessToken refreshToken) {
return refreshToken.getConfirmation() != null && refreshToken.getConfirmation().getKeyThumbprint() != null;
}

private static class DPoPClaimsCheck implements TokenVerifier.Predicate<DPoP> {

static final TokenVerifier.Predicate<DPoP> INSTANCE = new DPoPClaimsCheck();
Expand Down Expand Up @@ -426,7 +429,7 @@ public boolean test(DPoP t) throws DPoPVerificationException {

private static class DPoPAccessTokenHashCheck implements TokenVerifier.Predicate<DPoP> {

private String hash;
private final String hash;

public DPoPAccessTokenHashCheck(String tokenString) {
hash = HashUtils.accessTokenHash(DPOP_ATH_ALG, tokenString, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ public void testDPoPByPublicClient() throws Exception {
successTokenProceduresWithDPoP(dpopProofEcEncoded, jktEc);
}

@Test
public void testDPoPByPublicClientTokenRefreshWithoutDPoPProof() throws Exception {
// use pre-computed EC key

int clockSkew = 10; // acceptable clock skew is +-10sec

sendAuthorizationRequestWithDPoPJkt(null);

String dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(), (long) (Time.currentTime() + clockSkew), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate());

changeDPoPBound(TEST_PUBLIC_CLIENT_ID, false); // not enforce DPoP proof, but the refresh token is a DPoP type token.
failureRefreshTokenProceduresWithoutDPoP(dpopProofEcEncoded, jktEc);
}

@Test
public void testDPoPProofByConfidentialClient() throws Exception {
// use pre-computed RSA key
Expand Down Expand Up @@ -907,6 +921,26 @@ private void successTokenProceduresWithDPoP(String dpopProofEncoded, String jkt)
oauth.logoutForm().idTokenHint(response.getIdToken()).open();
}

private void failureRefreshTokenProceduresWithoutDPoP(String dpopProofEncoded, String jkt) throws Exception {
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse response = oauth.accessTokenRequest(code).dpopProof(dpopProofEncoded).send();
assertEquals(TokenUtil.TOKEN_TYPE_DPOP, response.getTokenType());
assertEquals(Status.OK.getStatusCode(), response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
assertEquals(jkt, accessToken.getConfirmation().getKeyThumbprint());
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
assertEquals(jkt, refreshToken.getConfirmation().getKeyThumbprint());

// token refresh without DPoP Proof
response = oauth.refreshRequest(response.getRefreshToken()).dpopProof(null).send();
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("DPoP proof is missing", response.getErrorDescription());

// logout
oauth.logoutForm().idTokenHint(response.getIdToken()).open();
}

private void failureTokenProceduresWithDPoP(String dpopProofEncoded, String error) throws Exception {
String code = oauth.parseLoginResponse().getCode();
AccessTokenResponse response = oauth.accessTokenRequest(code).dpopProof(dpopProofEncoded).send();
Expand Down
Loading
0