Using DPoP token type in the access-token and as token_type in introspection response

closes #21919
This commit is contained in:
Takashi Norimatsu 2023-08-04 16:01:28 +09:00 committed by Marek Posolda
parent 10ccc439e4
commit 9d0960d405
9 changed files with 29 additions and 17 deletions

View file

@ -116,20 +116,20 @@ public class TokenVerifier<T extends JsonWebToken> {
public static class TokenTypeCheck implements Predicate<JsonWebToken> {
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<String> tokenTypes;
public TokenTypeCheck(String tokenType) {
this.tokenType = tokenType;
public TokenTypeCheck(List<String> 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<T extends JsonWebToken> {
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
private List<String> expectedTokenType = Arrays.asList(TokenUtil.TOKEN_TYPE_BEARER, TokenUtil.TOKEN_TYPE_DPOP);
private boolean checkTokenType = true;
private boolean checkRealmUrl = true;
private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
@ -254,7 +254,7 @@ public class TokenVerifier<T extends JsonWebToken> {
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<T extends JsonWebToken> {
*
* @return This token verifier
*/
public TokenVerifier<T> tokenType(String tokenType) {
this.expectedTokenType = tokenType;
public TokenVerifier<T> tokenType(List<String> tokenTypes) {
this.expectedTokenType = tokenTypes;
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}

View file

@ -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";

View file

@ -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();
}

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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) {

View file

@ -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<String> allowedHeaders = new HashSet<String>(Arrays.asList(headers));
assertTrue(allowedHeaders.contains("DPoP"));
assertTrue(allowedHeaders.contains(TokenUtil.TOKEN_TYPE_DPOP));
}
@Test

View file

@ -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());