Using DPoP token type in the access-token and as token_type in introspection response
closes #21919
This commit is contained in:
parent
10ccc439e4
commit
9d0960d405
9 changed files with 29 additions and 17 deletions
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue