Skip to content

Commit 62f68b2

Browse files
rmartincmposolda
authored andcommitted
DPoP replay check should take clockSkew into account
Closes keycloak#43505 Signed-off-by: rmartinc <[email protected]>
1 parent a25a026 commit 62f68b2

File tree

2 files changed

+41
-3
lines changed
  • services/src/main/java/org/keycloak/services/util
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth

2 files changed

+41
-3
lines changed

services/src/main/java/org/keycloak/services/util/DPoPUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ private static DPoP validateDPoP(KeycloakSession session, URI uri, String method
210210
DPoPClaimsCheck.INSTANCE,
211211
new DPoPHTTPCheck(uri, method),
212212
new DPoPIsActiveCheck(session, lifetime, clockSkew),
213-
new DPoPReplayCheck(session, lifetime));
213+
new DPoPReplayCheck(session, lifetime + clockSkew));
214214

215215
if (accessToken != null) {
216216
verifier.withChecks(new DPoPAccessTokenHashCheck(accessToken));

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,36 @@ public void testDPoPByPublicClient() throws Exception {
259259
successTokenProceduresWithDPoP(dpopProofEcEncoded, jktEc, true, true);
260260
}
261261

262+
@Test
263+
public void testDPoPByPublicClientClockSkew() throws Exception {
264+
getTestingClient().testing().setTestingInfinispanTimeService();
265+
try {
266+
sendAuthorizationRequestWithDPoPJkt(null);
267+
268+
// get a DPoP proof 10 seconds in the future
269+
String dpopProofEcEncoded = generateSignedDPoPProof(UUID.randomUUID().toString(), HttpMethod.POST, oauth.getEndpoints().getToken(),
270+
(long) (Time.currentTime() + 10), Algorithm.ES256, jwsEcHeader, ecKeyPair.getPrivate(), null);
271+
272+
AccessTokenResponse response = successTokenProceduresWithDPoP(dpopProofEcEncoded, jktEc, true, true, false);
273+
274+
setTimeOffset(25); // 25 <= 10+10+15, proof not expired because clockSkew, detected by replay check
275+
response = oauth.refreshRequest(response.getRefreshToken()).dpopProof(dpopProofEcEncoded).send();
276+
assertEquals(400, response.getStatusCode());
277+
assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
278+
assertEquals("DPoP proof has already been used", response.getErrorDescription());
279+
280+
setTimeOffset(36); // 36 > 10+10+15, proof expired definitely
281+
response = oauth.refreshRequest(response.getRefreshToken()).dpopProof(dpopProofEcEncoded).send();
282+
assertEquals(400, response.getStatusCode());
283+
assertEquals(response.getError(), OAuthErrorException.INVALID_REQUEST);
284+
assertEquals("DPoP proof is not active", response.getErrorDescription());
285+
286+
oauth.logoutForm().idTokenHint(response.getIdToken()).open();
287+
} finally {
288+
getTestingClient().testing().revertTestingInfinispanTimeService();
289+
}
290+
}
291+
262292
@Test
263293
public void testDPoPByPublicClientTokenRefreshWithoutDPoPProof() throws Exception {
264294
// use pre-computed EC key
@@ -1205,7 +1235,12 @@ private void sendAuthorizationRequestWithDPoPJkt(String dpopJkt) {
12051235
oauth.loginForm().dpopJkt(dpopJkt).doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
12061236
}
12071237

1208-
private void successTokenProceduresWithDPoP(String dpopProofEncoded, String jkt, boolean accessTokenBound, boolean refreshTokenBound) throws Exception {
1238+
private AccessTokenResponse successTokenProceduresWithDPoP(String dpopProofEncoded, String jkt, boolean accessTokenBound, boolean refreshTokenBound) throws Exception {
1239+
return successTokenProceduresWithDPoP(dpopProofEncoded, jkt, accessTokenBound, refreshTokenBound, true);
1240+
}
1241+
1242+
private AccessTokenResponse successTokenProceduresWithDPoP(String dpopProofEncoded, String jkt, boolean accessTokenBound,
1243+
boolean refreshTokenBound, boolean performLogout) throws Exception {
12091244
String code = oauth.parseLoginResponse().getCode();
12101245
AccessTokenResponse response = oauth.accessTokenRequest(code).dpopProof(dpopProofEncoded).send();
12111246
assertEquals(accessTokenBound ? TokenUtil.TOKEN_TYPE_DPOP : TokenUtil.TOKEN_TYPE_BEARER, response.getTokenType());
@@ -1256,7 +1291,10 @@ private void successTokenProceduresWithDPoP(String dpopProofEncoded, String jkt,
12561291
}
12571292

12581293
// logout
1259-
oauth.logoutForm().idTokenHint(response.getIdToken()).open();
1294+
if (performLogout) {
1295+
oauth.logoutForm().idTokenHint(response.getIdToken()).open();
1296+
}
1297+
return response;
12601298
}
12611299

12621300
private void failureRefreshTokenProceduresWithoutDPoP(String dpopProofEncoded, String jkt) throws Exception {

0 commit comments

Comments
 (0)