From 9d0960d405f2898eb81d2f166350ede02f8e7a45 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Fri, 4 Aug 2023 16:01:28 +0900 Subject: [PATCH] Using DPoP token type in the access-token and as token_type in introspection response closes #21919 --- .../main/java/org/keycloak/TokenVerifier.java | 22 +++++++++---------- .../java/org/keycloak/util/TokenUtil.java | 2 ++ .../AccessTokenIntrospectionProvider.java | 4 ++++ .../oidc/endpoints/LogoutEndpoint.java | 4 +++- .../oidc/endpoints/TokenEndpoint.java | 3 +-- .../endpoints/TokenRevocationEndpoint.java | 2 +- .../managers/AuthenticationManager.java | 3 ++- .../keycloak/testsuite/oauth/DPoPTest.java | 4 +++- .../oauth/TokenIntrospectionTest.java | 2 ++ 9 files changed, 29 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index 12435b314a7..f6b93b59304 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -116,20 +116,20 @@ public class TokenVerifier { public static class TokenTypeCheck implements Predicate { - private static final TokenTypeCheck INSTANCE_BEARER = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_BEARER); + private static final TokenTypeCheck INSTANCE_DEFAULT_TOKEN_TYPE = new TokenTypeCheck(Arrays.asList(TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_DPOP)); - private final String tokenType; + private final List tokenTypes; - public TokenTypeCheck(String tokenType) { - this.tokenType = tokenType; + public TokenTypeCheck(List tokenTypes) { + this.tokenTypes = tokenTypes; } @Override public boolean test(JsonWebToken t) throws VerificationException { - if (! tokenType.equalsIgnoreCase(t.getType())) { - throw new VerificationException("Token type is incorrect. Expected '" + tokenType + "' but was '" + t.getType() + "'"); + for (String tokenType : tokenTypes) { + if (tokenType.equalsIgnoreCase(t.getType())) return true; } - return true; + throw new VerificationException("Token type is incorrect. Expected '" + tokenTypes.toString() + "' but was '" + t.getType() + "'"); } }; @@ -190,7 +190,7 @@ public class TokenVerifier { private PublicKey publicKey; private SecretKey secretKey; private String realmUrl; - private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER; + private List expectedTokenType = Arrays.asList(TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_DPOP); private boolean checkTokenType = true; private boolean checkRealmUrl = true; private final LinkedList> checks = new LinkedList<>(); @@ -254,7 +254,7 @@ public class TokenVerifier { return withChecks( RealmUrlCheck.NULL_INSTANCE, SUBJECT_EXISTS_CHECK, - TokenTypeCheck.INSTANCE_BEARER, + TokenTypeCheck.INSTANCE_DEFAULT_TOKEN_TYPE, IS_ACTIVE ); } @@ -344,8 +344,8 @@ public class TokenVerifier { * * @return This token verifier */ - public TokenVerifier tokenType(String tokenType) { - this.expectedTokenType = tokenType; + public TokenVerifier tokenType(List tokenTypes) { + this.expectedTokenType = tokenTypes; return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType)); } diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 5e48b65c573..41432d68e7a 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -40,6 +40,8 @@ public class TokenUtil { public static final String TOKEN_TYPE_BEARER = "Bearer"; + public static final String TOKEN_TYPE_DPOP = "DPoP"; + public static final String TOKEN_TYPE_KEYCLOAK_ID = "Serialized-ID"; public static final String TOKEN_TYPE_ID = "ID"; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java index 066868dfccf..dcbcc08c4ed 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java @@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc; import com.fasterxml.jackson.databind.node.ObjectNode; import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.crypto.SignatureProvider; @@ -85,6 +86,9 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi } } } + + tokenMetadata.put(OAuth2Constants.TOKEN_TYPE, accessToken.getType()); + } else { tokenMetadata = JsonSerialization.createObjectNode(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 6df51ad1dfe..a07172e38e2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -92,6 +92,8 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; + +import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -200,7 +202,7 @@ public class LogoutEndpoint { if (encodedIdToken != null) { try { idToken = tokenManager.verifyIDTokenSignature(session, encodedIdToken); - TokenVerifier.createWithoutSignature(idToken).tokenType(TokenUtil.TOKEN_TYPE_ID).verify(); + TokenVerifier.createWithoutSignature(idToken).tokenType(Arrays.asList(TokenUtil.TOKEN_TYPE_ID)).verify(); } catch (OAuthErrorException | VerificationException e) { event.event(EventType.LOGOUT); event.error(Errors.INVALID_TOKEN); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 1a108d92177..059e11477be 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -549,8 +549,7 @@ public class TokenEndpoint { if (clientConfig.isUseDPoP() || dPoP != null) { DPoPUtil.bindToken(responseBuilder.getAccessToken(), dPoP); - // TODO Probably uncomment as the accessToken type "DPoP" will have more sense than "Bearer". It will require some changes in the introspection endpoint too... - // responseBuilder.getAccessToken().type(DPoPUtil.DPOP_TOKEN_TYPE); + responseBuilder.getAccessToken().type(DPoPUtil.DPOP_TOKEN_TYPE); responseBuilder.responseTokenType(DPoPUtil.DPOP_TOKEN_TYPE); // Bind refresh tokens for public clients, See "Section 5. DPoP Access Token Request" from DPoP specification diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java index ef433ac0960..981d9266c89 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenRevocationEndpoint.java @@ -182,7 +182,7 @@ public class TokenRevocationEndpoint { throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.OK); } - if (!(TokenUtil.TOKEN_TYPE_REFRESH.equals(token.getType()) || TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType()) || TokenUtil.TOKEN_TYPE_BEARER.equals(token.getType()))) { + if (!(TokenUtil.TOKEN_TYPE_REFRESH.equals(token.getType()) || TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType()) || TokenUtil.TOKEN_TYPE_BEARER.equals(token.getType())|| TokenUtil.TOKEN_TYPE_DPOP.equals(token.getType()))) { event.error(Errors.INVALID_TOKEN_TYPE); throw new CorsErrorResponseException(cors, OAuthErrorException.UNSUPPORTED_TOKEN_TYPE, "Unsupported token type", Response.Status.BAD_REQUEST); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 89c4515a2ef..5a1f87f1e9b 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -100,6 +100,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -170,7 +171,7 @@ public class AuthenticationManager { // Parameter of LogoutEndpoint public static final String INITIATING_IDP_PARAM = "initiating_idp"; - private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID); + private static final TokenTypeCheck VALIDATE_IDENTITY_COOKIE = new TokenTypeCheck(Arrays.asList(TokenUtil.TOKEN_TYPE_KEYCLOAK_ID)); public static boolean isSessionValid(RealmModel realm, UserSessionModel userSession) { if (userSession == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java index 1b7c1f335f6..4c86e1a41b1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/DPoPTest.java @@ -78,6 +78,7 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.ServerURLs; import org.keycloak.util.JWKSUtils; import org.keycloak.util.JsonSerialization; +import org.keycloak.util.TokenUtil; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; @@ -193,6 +194,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest { TokenMetadataRepresentation tokenMetadataRepresentation = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class); Assert.assertTrue(tokenMetadataRepresentation.isActive()); assertEquals(jkt, tokenMetadataRepresentation.getConfirmation().getKeyThumbprint()); + assertEquals(TokenUtil.TOKEN_TYPE_DPOP, tokenMetadataRepresentation.getOtherClaims().get(OAuth2Constants.TOKEN_TYPE)); CloseableHttpResponse closableHttpResponse = oauth.doTokenRevoke(response.getAccessToken(), "access_token", TEST_CONFIDENTIAL_CLIENT_SECRET); tokenResponse = oauth.introspectTokenWithClientCredential(TEST_CONFIDENTIAL_CLIENT_ID, TEST_CONFIDENTIAL_CLIENT_SECRET, "access_token", response.getAccessToken()); @@ -309,7 +311,7 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest { String[] headers = response.getHeaders(Cors.ACCESS_CONTROL_ALLOW_HEADERS)[0].getValue().split(", "); Set allowedHeaders = new HashSet(Arrays.asList(headers)); - assertTrue(allowedHeaders.contains("DPoP")); + assertTrue(allowedHeaders.contains(TokenUtil.TOKEN_TYPE_DPOP)); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index 363b6353395..c6d17209a24 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -60,6 +60,7 @@ import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; +import org.keycloak.util.TokenUtil; import jakarta.ws.rs.core.UriBuilder; @@ -352,6 +353,7 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { assertEquals("test-user@localhost", rep.getUserName()); assertEquals("test-app", rep.getClientId()); assertEquals(loginEvent.getUserId(), rep.getSubject()); + assertEquals(TokenUtil.TOKEN_TYPE_BEARER, rep.getOtherClaims().get(OAuth2Constants.TOKEN_TYPE)); // Assert expected scope OIDCScopeTest.assertScopes("openid email profile", rep.getScope());