From 96f2c4490c63372debd0eae2c636574f33f44e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Thu, 7 May 2026 15:09:48 +0200 Subject: [PATCH 1/2] Unhandled NPE with alg:none JWT in Bearer Authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #48744 Signed-off-by: Martin Bartoš --- .../java/org/keycloak/crypto/CryptoUtils.java | 15 ++++++ .../organization/utils/Organizations.java | 4 +- .../AccessTokenIntrospectionProvider.java | 4 +- .../oidc/endpoints/UserInfoEndpoint.java | 3 +- .../ClientRegistrationTokenUtils.java | 3 +- .../managers/AuthenticationManager.java | 3 +- .../org/keycloak/services/util/DPoPUtil.java | 3 +- .../org/keycloak/crypto/CryptoUtilsTest.java | 40 ++++++++++++++++ .../keycloak/services/util/DPoPUtilTests.java | 21 +------- .../utils/AbstractUtilSessionTest.java | 27 +++++++++++ .../tests/oauth/TokenInputValidationTest.java | 48 +++++++++++++++++++ 11 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 services/src/test/java/org/keycloak/crypto/CryptoUtilsTest.java create mode 100644 services/src/test/java/org/keycloak/utils/AbstractUtilSessionTest.java create mode 100644 tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java diff --git a/services/src/main/java/org/keycloak/crypto/CryptoUtils.java b/services/src/main/java/org/keycloak/crypto/CryptoUtils.java index 72e72263e579..2dd0abc3ba89 100644 --- a/services/src/main/java/org/keycloak/crypto/CryptoUtils.java +++ b/services/src/main/java/org/keycloak/crypto/CryptoUtils.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.stream.Collectors; +import org.keycloak.common.VerificationException; import org.keycloak.models.KeycloakSession; import org.keycloak.provider.ProviderFactory; import org.keycloak.util.Strings; @@ -36,6 +37,20 @@ */ public class CryptoUtils { + /** + * Looks up a {@link SignatureProvider} for the given algorithm, throwing if none is registered. + */ + public static SignatureProvider getSignatureProvider(KeycloakSession session, String algorithm) throws VerificationException { + if (algorithm == null) { + throw new VerificationException("Missing token algorithm"); + } + SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm); + if (signatureProvider == null) { + throw new VerificationException("Unsupported token algorithm: " + algorithm); + } + return signatureProvider; + } + /** * Returns the supported asymmetric signature algorithms. * This method discovers all available SignatureProvider implementations and filters diff --git a/services/src/main/java/org/keycloak/organization/utils/Organizations.java b/services/src/main/java/org/keycloak/organization/utils/Organizations.java index 7c372671c899..ff1c3eaf2b35 100644 --- a/services/src/main/java/org/keycloak/organization/utils/Organizations.java +++ b/services/src/main/java/org/keycloak/organization/utils/Organizations.java @@ -33,7 +33,7 @@ import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.common.VerificationException; -import org.keycloak.crypto.SignatureProvider; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.http.HttpRequest; import org.keycloak.models.Constants; @@ -181,7 +181,7 @@ public static InviteOrgActionToken parseInvitationToken(KeycloakSession session, .withChecks(TokenVerifier.IS_ACTIVE, new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(context.getUri().getBaseUri(), realm.getName()))); - SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); + SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); verifier.verifierContext(verifierContext); return verifier.verify().getToken(); 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 8775f7b6c2b9..c14bcbb9400d 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java @@ -24,7 +24,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; -import org.keycloak.crypto.SignatureProvider; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -226,7 +226,7 @@ protected boolean verifyToken(String tokenStr) { TokenVerifier verifier = TokenVerifier.create(tokenStr, getTokenClass()) .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); - SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); + SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); verifier.verifierContext(verifierContext); this.token = verifier.verify().getToken(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 73ad7867d15e..7fe739cca206 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -37,6 +37,7 @@ import org.keycloak.common.VerificationException; import org.keycloak.crypto.CekManagementProvider; import org.keycloak.crypto.ContentEncryptionProvider; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureSignerContext; @@ -191,7 +192,7 @@ private Response issueUserInfo() { verifier = DPoPUtil.withDPoPVerifier(verifier, realm, new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).accessToken(tokenForUserInfo.getToken())); - SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); + SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); verifier.verifierContext(verifierContext); token = verifier.verify().getToken(); diff --git a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java index fcee6176299c..787ed7989926 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/ClientRegistrationTokenUtils.java @@ -25,6 +25,7 @@ import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.SecretGenerator; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.crypto.SignatureVerifierContext; @@ -99,7 +100,7 @@ public static TokenVerification verifyToken(KeycloakSession session, RealmModel TokenVerifier verifier = TokenVerifier.create(token, AccessToken.class) .withChecks(new TokenVerifier.RealmUrlCheck(getIssuer(session, realm)), TokenVerifier.IS_ACTIVE, new TokenRevocationCheck(session)); - SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); + SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId()); verifier.verifierContext(verifierContext); kid = verifierContext.getKid(); 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 464c8dcbb6f1..20f1c021cfb6 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -66,6 +66,7 @@ import org.keycloak.common.util.Time; import org.keycloak.cookie.CookieProvider; import org.keycloak.cookie.CookieType; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.events.Details; @@ -262,7 +263,7 @@ public static boolean expireUserSessionCookie(KeycloakSession session, UserSessi String kid = verifier.getHeader().getKeyId(); String algorithm = verifier.getHeader().getAlgorithm().name(); - SignatureVerifierContext signatureVerifier = session.getProvider(SignatureProvider.class, algorithm).verifier(kid); + SignatureVerifierContext signatureVerifier = CryptoUtils.getSignatureProvider(session, algorithm).verifier(kid); verifier.verifierContext(signatureVerifier); AccessToken token = verifier.verify().getToken(); diff --git a/services/src/main/java/org/keycloak/services/util/DPoPUtil.java b/services/src/main/java/org/keycloak/services/util/DPoPUtil.java index b810f9402694..f9589d0a7453 100644 --- a/services/src/main/java/org/keycloak/services/util/DPoPUtil.java +++ b/services/src/main/java/org/keycloak/services/util/DPoPUtil.java @@ -39,6 +39,7 @@ import org.keycloak.common.Profile; import org.keycloak.common.VerificationException; import org.keycloak.common.util.Time; +import org.keycloak.crypto.CryptoUtils; import org.keycloak.crypto.KeyWrapper; import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureVerifierContext; @@ -209,7 +210,7 @@ private static DPoP validateDPoP(KeycloakSession session, URI uri, String method key.setAlgorithm(header.getAlgorithm().name()); - SignatureVerifierContext signatureVerifier = session.getProvider(SignatureProvider.class, algorithm).verifier(key); + SignatureVerifierContext signatureVerifier = CryptoUtils.getSignatureProvider(session, algorithm).verifier(key); verifier.verifierContext(signatureVerifier); verifier.withChecks( DPoPClaimsCheck.INSTANCE, diff --git a/services/src/test/java/org/keycloak/crypto/CryptoUtilsTest.java b/services/src/test/java/org/keycloak/crypto/CryptoUtilsTest.java new file mode 100644 index 000000000000..54c61b1b5c1d --- /dev/null +++ b/services/src/test/java/org/keycloak/crypto/CryptoUtilsTest.java @@ -0,0 +1,40 @@ +package org.keycloak.crypto; + +import org.keycloak.common.VerificationException; +import org.keycloak.utils.AbstractUtilSessionTest; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CryptoUtilsTest extends AbstractUtilSessionTest { + + @Test + public void getSignatureProviderReturnsProviderForSupportedAlgorithm() throws VerificationException { + assertNotNull(CryptoUtils.getSignatureProvider(session, "RS256")); + } + + @Test + public void getSignatureProviderThrowsForNoneAlgorithm() { + VerificationException thrown = assertThrows(VerificationException.class, + () -> CryptoUtils.getSignatureProvider(session, "none")); + assertThat(thrown.getMessage(), containsString("none")); + } + + @Test + public void getSignatureProviderThrowsForUnknownAlgorithm() { + VerificationException thrown = assertThrows(VerificationException.class, + () -> CryptoUtils.getSignatureProvider(session, "FAKE256")); + assertThat(thrown.getMessage(), containsString("FAKE256")); + } + + @Test + public void getSignatureProviderThrowsForNullAlgorithm() { + assertThrows(VerificationException.class, + () -> CryptoUtils.getSignatureProvider(session, null)); + } + +} diff --git a/services/src/test/java/org/keycloak/services/util/DPoPUtilTests.java b/services/src/test/java/org/keycloak/services/util/DPoPUtilTests.java index 46f8c28dd495..6e9364be0e7e 100644 --- a/services/src/test/java/org/keycloak/services/util/DPoPUtilTests.java +++ b/services/src/test/java/org/keycloak/services/util/DPoPUtilTests.java @@ -4,39 +4,22 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; -import org.keycloak.common.Profile; import org.keycloak.common.VerificationException; -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.crypto.CryptoProvider; import org.keycloak.http.HttpRequest; -import org.keycloak.models.KeycloakSession; import org.keycloak.services.resteasy.HttpRequestImpl; -import org.keycloak.services.resteasy.ResteasyKeycloakSession; -import org.keycloak.services.resteasy.ResteasyKeycloakSessionFactory; +import org.keycloak.utils.AbstractUtilSessionTest; import org.jboss.resteasy.mock.MockHttpRequest; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -public class DPoPUtilTests { +public class DPoPUtilTests extends AbstractUtilSessionTest { private static final String URL = "http://localhost/test"; private static final long IAT = 1_000_000_000L; - private static KeycloakSession session; - - @BeforeAll - public static void beforeAll() { - Profile.defaults(); - CryptoIntegration.init(CryptoProvider.class.getClassLoader()); - ResteasyKeycloakSessionFactory sessionFactory = new ResteasyKeycloakSessionFactory(); - sessionFactory.init(); - session = new ResteasyKeycloakSession(sessionFactory); - } - @Test public void testRejectsUnsupportedJwkKeyType() { String jwkJson = "{\"kty\":\"foo\",\"kid\":\"invalid-kid\",\"alg\":\"RS256\"}"; diff --git a/services/src/test/java/org/keycloak/utils/AbstractUtilSessionTest.java b/services/src/test/java/org/keycloak/utils/AbstractUtilSessionTest.java new file mode 100644 index 000000000000..2c67edad936d --- /dev/null +++ b/services/src/test/java/org/keycloak/utils/AbstractUtilSessionTest.java @@ -0,0 +1,27 @@ +package org.keycloak.utils; + +import org.keycloak.common.Profile; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.crypto.CryptoProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.resteasy.ResteasyKeycloakSession; +import org.keycloak.services.resteasy.ResteasyKeycloakSessionFactory; + +import org.junit.jupiter.api.BeforeAll; + +/** + * Abstract class for tests using some test session + */ +public abstract class AbstractUtilSessionTest { + + protected static KeycloakSession session; + + @BeforeAll + public static void beforeAll() { + Profile.defaults(); + CryptoIntegration.init(CryptoProvider.class.getClassLoader()); + ResteasyKeycloakSessionFactory sessionFactory = new ResteasyKeycloakSessionFactory(); + sessionFactory.init(); + session = new ResteasyKeycloakSession(sessionFactory); + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java new file mode 100644 index 000000000000..8c698433c532 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java @@ -0,0 +1,48 @@ +package org.keycloak.tests.oauth; + +import java.io.IOException; + +import org.keycloak.OAuthErrorException; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.oauth.OAuthClient; +import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testsuite.util.oauth.IntrospectionResponse; +import org.keycloak.testsuite.util.oauth.UserInfoResponse; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@KeycloakIntegrationTest +public class TokenInputValidationTest { + + // {"alg":"none","typ":"JWT"}.{"sub":"attacker","exp":9999999999} + private static final String ALG_NONE_TOKEN = + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsImV4cCI6OTk5OTk5OTk5OX0."; + + @InjectRealm + ManagedRealm realm; + + @InjectOAuthClient + OAuthClient oauth; + + @Test + public void userInfoRejectsAlgNoneToken() { + UserInfoResponse response = oauth.doUserInfoRequest(ALG_NONE_TOKEN); + + assertEquals(401, response.getStatusCode()); + assertFalse(response.isSuccess()); + assertEquals(OAuthErrorException.INVALID_TOKEN, response.getError()); + } + + @Test + public void introspectionRejectsAlgNoneToken() throws IOException { + IntrospectionResponse response = oauth.doIntrospectionAccessTokenRequest(ALG_NONE_TOKEN); + + assertFalse(response.asJsonNode().get("active").asBoolean()); + } + +} From c420dc981dc33dcb934488667d8af569c15b1a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Mon, 11 May 2026 12:57:44 +0200 Subject: [PATCH 2/2] Update test to create programatically token with alg:none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Bartoš Co-authored-by: Ricardo Martin --- .../tests/oauth/TokenInputValidationTest.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java index 8c698433c532..610e890cfdb6 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/TokenInputValidationTest.java @@ -3,11 +3,13 @@ import java.io.IOException; import org.keycloak.OAuthErrorException; -import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.common.util.SecretGenerator; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.representations.JsonWebToken; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.oauth.OAuthClient; import org.keycloak.testframework.oauth.annotations.InjectOAuthClient; -import org.keycloak.testframework.realm.ManagedRealm; import org.keycloak.testsuite.util.oauth.IntrospectionResponse; import org.keycloak.testsuite.util.oauth.UserInfoResponse; @@ -19,19 +21,13 @@ @KeycloakIntegrationTest public class TokenInputValidationTest { - // {"alg":"none","typ":"JWT"}.{"sub":"attacker","exp":9999999999} - private static final String ALG_NONE_TOKEN = - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhdHRhY2tlciIsImV4cCI6OTk5OTk5OTk5OX0."; - - @InjectRealm - ManagedRealm realm; - @InjectOAuthClient OAuthClient oauth; @Test public void userInfoRejectsAlgNoneToken() { - UserInfoResponse response = oauth.doUserInfoRequest(ALG_NONE_TOKEN); + String noneToken = new JWSBuilder().jsonContent(createDefaultToken()).none(); + UserInfoResponse response = oauth.doUserInfoRequest(noneToken); assertEquals(401, response.getStatusCode()); assertFalse(response.isSuccess()); @@ -40,9 +36,21 @@ public void userInfoRejectsAlgNoneToken() { @Test public void introspectionRejectsAlgNoneToken() throws IOException { - IntrospectionResponse response = oauth.doIntrospectionAccessTokenRequest(ALG_NONE_TOKEN); + String noneToken = new JWSBuilder().jsonContent(createDefaultToken()).none(); + IntrospectionResponse response = oauth.doIntrospectionAccessTokenRequest(noneToken); assertFalse(response.asJsonNode().get("active").asBoolean()); } + private JsonWebToken createDefaultToken() { + JsonWebToken token = new JsonWebToken(); + token.id(SecretGenerator.getInstance().generateSecureID()); + token.issuer("http://127.0.0.1:8500"); + token.audience(oauth.getEndpoints().getIssuer()); + token.iat((long) Time.currentTime()); + token.exp((long) (Time.currentTime() + 300)); + token.subject("attacker"); + return token; + } + }