8000 Allow JWS header with algorithms not in the Algorithm enum by dmitrygx · Pull Request #53415 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/src/main/java/org/keycloak/TokenVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.exceptions.TokenSignatureInvalidException;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
Expand Down Expand Up @@ -440,7 +441,8 @@ public void verifySignature() throws VerificationException {
throw new VerificationException(e);
}
} else {
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
Algorithm algorithm = getHeader().getAlgorithm();
AlgorithmType algorithmType = algorithm == null ? null : algorithm.getType();
if (null == algorithmType) {
throw new VerificationException("Unknown or unsupported token algorithm");
}
Expand Down
24 changes: 18 additions & 6 deletions core/src/main/java/org/keycloak/jose/jws/JWSHeader.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public class JWSHeader implements JOSEHeader {
@JsonProperty("alg")
private Algorithm algorithm;
private String alg;

@JsonProperty("typ")
private String type;
Expand All @@ -66,30 +66,42 @@ public JWSHeader() {
}

public JWSHeader(Algorithm algorithm, String type, String contentType) {
this.algorithm = algorithm;
setAlgorithm(algorithm);
this.type = type;
this.contentType = contentType;
}

public JWSHeader(Algorithm algorithm, String type, String keyId, JWK key) {
this.algorithm = algorithm;
setAlgorithm(algorithm);
this.type = type;
this.keyId = keyId;
this.key = key;
}

/**
* @return the algorithm, or {@code null} if the {@code alg} header is not one of {@link Algorithm} values
* (e.g. an algorithm implemented by a custom {@code SignatureProvider}). Use {@link #getRawAlgorithm()} to get it.
*/
@JsonIgnore
public Algorithm getAlgorithm() {
return algorithm;
if (alg == null) {
return null;
}
try {
return Algorithm.valueOf(alg);
} catch (IllegalArgumentException e) {
return null;
Comment thread
dmitrygx marked this conversation as resolved.
Resolved
}
}

public void setAlgorithm(Algorithm algorithm) {
this.algorithm = algorithm;
this.alg = algorithm == null ? null : algorithm.name();
}

@JsonIgnore
@Override
public String getRawAlgorithm() {
return getAlgorithm().name();
return alg;
}

public String getType() {
Expand Down
4 changes: 1 addition & 3 deletions core/src/main/java/org/keycloak/sdjwt/JwsToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ public String sign(SignatureSignerContext signerContext) {

public void verifySignature(SignatureVerifierContext verifier) throws VerificationException {
Objects.requireNonNull(verifier, "verifier must not be null");
String headerAlgorithm = jwsHeader == null || jwsHeader.getAlgorithm() == null
? null
: jwsHeader.getRawAlgorithm();
String headerAlgorithm = jwsHeader == null ? null : jwsHeader.getRawAlgorithm();
String verifierAlgorithm = verifier.getAlgorithm();
if (headerAlgorithm == null || verifierAlgorithm == null || !headerAlgorithm.equals(verifierAlgorithm)) {
throw new VerificationException(String.format(
Expand Down
109 changes: 109 additions & 0 deletions core/src/test/java/org/keycloak/jose/jws/JWSHeaderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.jose.jws;

import java.nio.charset.StandardCharsets;
import java.security.KeyPairGenerator;
import java.util.Arrays;
import java.util.Base64;

import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.JsonSerialization;

import org.junit.Assert;
import org.junit.Test;

public class JWSHeaderTest {

@Test
public void knownAlgorithm() throws Exception {
JWSHeader header = JsonSerialization.readValue("{\"alg\":\"ES256\",\"typ\":\"JWT\"}", JWSHeader.class);
Assert.assertEquals(Algorithm.ES256, header.getAlgorithm());
Assert.assertEquals("ES256", header.getRawAlgorithm());
Assert.assertEquals("{\"alg\":\"ES256\",\"typ\":\"JWT\"}", JsonSerialization.writeValueAsString(header));
}

@Test
public void customAlgorithm() throws Exception {
JWSHeader header = JsonSerialization.readValue("{\"alg\":\"CUSTOM256\",\"typ\":\"JWT\",\"kid\":\"k1\"}", JWSHeader.class);
Assert.assertNull(header.getAlgorithm());
Assert.assertEquals("CUSTOM256", header.getRawAlgorithm());
Assert.assertEquals("k1", header.getKeyId());
Assert.assertEquals("{\"alg\":\"CUSTOM256\",\"typ\":\"JWT\",\"kid\":\"k1\"}", JsonSerialization.writeValueAsString(header));
}

@Test
public void customAlgorithmInToken() throws Exception {
String token = encode("{\"alg\":\"CUSTOM256\",\"typ\":\"JWT\"}") + "." + encode("{\"sub\":\"user\"}") + "." + encode("signature");

JWSInput input = new JWSInput(token);
Assert.assertEquals("CUSTOM256", input.getHeader().getRawAlgorithm());

TokenVerifier<AccessToken> verifier = TokenVerifier.create(token, AccessToken.class)
.publicKey(KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic());
Assert.assertEquals("user", verifier.getToken().getSubject());
VerificationException e = Assert.assertThrows(VerificationException.class, verifier::verifySignature);
Assert.assertEquals("Unknown or unsupported token algorithm", e.getMessage());
Comment thread
dmitrygx marked this conversation as resolved.
Resolved
}

@Test
public void customAlgorithmWithVerifierContext() throws Exception {
String token = encode("{\"alg\":\"CUSTOM256\",\"typ\":\"JWT\",\"kid\":\"k1\"}") + "." + encode("{\"sub\":\"user\"}") + "." + encode("signature");

AccessToken accessToken = TokenVerifier.create(token, AccessToken.class)
.verifierContext(new CustomVerifier("signature"))
.verify()
.getToken();
Assert.assertEquals("user", accessToken.getSubject());

TokenVerifier<AccessToken> invalid = TokenVerifier.create(token, AccessToken.class)
.verifierContext(new CustomVerifier("other"));
Assert.assertThrows(VerificationException.class, invalid::verify);
}

private static String encode(String s) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(s.getBytes(StandardCharsets.UTF_8));
}

private static class CustomVerifier implements SignatureVerifierContext {

private final byte[] expectedSignature;

CustomVerifier(String expectedSignature) {
this.expectedSignature = expectedSignature.getBytes(StandardCharsets.UTF_8);
}

@Override
public String getKid() {
return "k1";
}

@Override
public String getAlgorithm() {
return "CUSTOM256";
}

@Override
public boolean verify(byte[] data, byte[] signature) {
return Arrays.equals(expectedSignature, signature);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ protected boolean isSymmetricAlgorithmAllowed() {
public boolean validateSignatureAlgorithm(String expectedSignatureAlg) {
JWSInput jws = clientAssertionState.getJws();

if (jws.getHeader().getAlgorithm() == null) {
String algorithmName = jws.getHeader().getRawAlgorithm();

if (algorithmName == null) {
return failure("Invalid signature algorithm");
}

String algorithmName = jws.getHeader().getAlgorithm().name();

if ("none".equalsIgnoreCase(algorithmName)) {
return failure("Invalid signature algorithm");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import org.keycloak.exceptions.TokenVerificationException;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
Expand Down Expand Up @@ -403,8 +402,8 @@ private void validateClientAttestationJwt(ClientAuthenticationFlowContext contex

// Client Attestation JWT signature check
//
Algorithm algorithm = jws.getHeader().getAlgorithm();
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm.name());
String algorithm = jws.getHeader().getRawAlgorithm();
SignatureProvider signatureProvider = algorithm == null ? null : session.getProvider(SignatureProvider.class, algorithm);
if (signatureProvider == null) {
throw new TokenVerificationException(attestationJwt, "Signature provider not found for algorithm: " + algorithm);
}
Expand Down Expand Up @@ -480,8 +479,8 @@ private void validateClientAttestationPoPJwt(ClientAuthenticationFlowContext con

// Client Attestation PoP JWT signature check
//
Algorithm algorithm = jws.getHeader().getAlgorithm();
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, algorithm.name());
String algorithm = jws.getHeader().getRawAlgorithm();
SignatureProvider signatureProvider = algorithm == null ? null : session.getProvider(SignatureProvider.class, algorithm);
if (signatureProvider == null) {
throw new TokenVerificationException(attestationPoPJwt, "Signature provider not found for algorithm: " + algorithm);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ public <T extends Token> T decode(String token, Class<T> clazz) {
try {
JWSInput jws = new JWSInput(token);

String signatureAlgorithm = jws.getHeader().getAlgorithm().name();
String signatureAlgorithm = jws.getHeader().getRawAlgorithm();

SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, signatureAlgorithm);
SignatureProvider signatureProvider = signatureAlgorithm == null ? null : session.getProvider(SignatureProvider.class, signatureAlgorithm);
if (signatureProvider == null) {
return null;
}
Expand Down Expand Up @@ -177,11 +177,14 @@ public <T> T decodeClientJWT(String jwt, ClientModel client, BiConsumer<JOSE, Cl

private <T> T verifyJWS(ClientModel client, Class<T> clazz, JWSInput jws, boolean allowNoneAlgorithm) {
try {
String signatureAlgorithm = jws.getHeader().getAlgorithm().name();
String signatureAlgorithm = jws.getHeader().getRawAlgorithm();
if (signatureAlgorithm == null) {
return null;
}
ClientSignatureVerifierProvider signatureProvider = session.getProvider(ClientSignatureVerifierProvider.class, signatureAlgorithm);

if (signatureProvider == null) {
if (allowNoneAlgorithm && jws.getHeader().getAlgorithm().equals(org.keycloak.jose.jws.Algorithm.none)) {
if (allowNoneAlgorithm && org.keycloak.jose.jws.Algorithm.none.name().equals(signatureAlgorithm)) {
return jws.readJsonContent(clazz);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ public static InviteOrgActionToken parseInvitationToken(KeycloakSession session,
.withChecks(TokenVerifier.IS_ACTIVE,
new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(context.getUri().getBaseUri(), realm.getName())));

SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getRawAlgorithm()).verifier(verifier.getHeader().getKeyId());
verifier.verifierContext(verifierContext);

return verifier.verify().getToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
Expand Down Expand Up @@ -113,13 +112,13 @@ public static KeyAttestationJwtBody validateAttestationJwt(
throw new VCIssuerException(ErrorType.INVALID_PROOF,
"The x5c certificate chain is not trusted by a configured attester trust provider");
}
verifier = verifierFromResolvedJWK(resolvedJwk, header.getAlgorithm().name(), keycloakSession);
verifier = verifierFromResolvedJWK(resolvedJwk, header.getRawAlgorithm(), keycloakSession);
} else if (header.getKeyId() != null) {
JWK resolvedJwk = keyResolver.resolveKey(header.getKeyId(), rawHeader, rawPayload);
if (resolvedJwk == null) {
throw new VCIssuerException(ErrorType.INVALID_PROOF, "Key with kid '" + header.getKeyId() + "' not found in trusted key registry");
}
verifier = verifierFromResolvedJWK(resolvedJwk, header.getAlgorithm().name(), keycloakSession);
verifier = verifierFromResolvedJWK(resolvedJwk, header.getRawAlgorithm(), keycloakSession);
} else {
throw new VCIssuerException(ErrorType.INVALID_PROOF, "Neither x5c nor kid present in attestation JWT header");
}
Expand Down Expand Up @@ -293,8 +292,7 @@ private static Optional<List<String>> resolveProofSigningAlgorithms(

private static void validateJwsHeader(KeycloakSession session, JWSHeader header, VCIssuanceContext vcIssuanceContext,
String proofTypeKeyForSigningAlgPolicy) {
String alg = Optional.ofNullable(header.getAlgorithm())
.map(Algorithm::name)
String alg = Optional.ofNullable(header.getRawAlgorithm())
.orElseThrow(() -> new VCIssuerException(ErrorType.INVALID_PROOF, "Missing algorithm in JWS header"));

List<String> supportedAsymmetricAlgs = CryptoUtils.getSupportedAsymmetricSignatureAlgorithms(session);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private JWK validateSingleJwtProof(VCIssuanceContext vcIssuanceContext, String j
Map<String, Object> headerClaims = JsonSerialization.mapper.convertValue(jwsHeader,
new TypeReference<>() {
});
String algorithm = jwsHeader.getAlgorithm().name();
String algorithm = jwsHeader.getRawAlgorithm();
validateNoPrivateKeyInHeaderClaims(algorithm, headerClaims);
KeyAttestationInfo attestationInfo = resolveHeaderAttestation(vcIssuanceContext, headerClaims);

Expand All @@ -179,7 +179,7 @@ private JWK validateSingleJwtProof(VCIssuanceContext vcIssuanceContext, String j
}
}
} else if (jwsHeader.getX5c() != null && !jwsHeader.getX5c().isEmpty()) {
jwk = AttestationValidatorUtil.resolveJwkFromProofX5c(jwsHeader.getX5c(), jwsHeader.getAlgorithm().name());
jwk = AttestationValidatorUtil.resolveJwkFromProofX5c(jwsHeader.getX5c(), algorithm);
} else {
throw new VCIssuerException(ErrorType.INVALID_PROOF, "Missing binding key. JWT must contain either jwk, kid, or x5c in header.");
}
Expand All @@ -198,9 +198,9 @@ private JWK validateSingleJwtProof(VCIssuanceContext vcIssuanceContext, String j
AccessToken proofPayload = JsonSerialization.readValue(jwsInput.getContent(), AccessToken.class);
validateProofPayload(vcIssuanceContext, proofPayload);

SignatureVerifierContext signatureVerifierContext = getVerifier(jwk, jwsHeader.getAlgorithm().name());
SignatureVerifierContext signatureVerifierContext = getVerifier(jwk, algorithm);
if (signatureVerifierContext == null) {
throw new VCIssuerException(ErrorType.INVALID_PROOF, "No verifier configured for " + jwsHeader.getAlgorithm());
throw new VCIssuerException(ErrorType.INVALID_PROOF, "No verifier configured for " + algorithm);
}
if (!signatureVerifierContext.verify(jwsInput.getEncodedSignatureInput().getBytes(StandardCharsets.UTF_8),
jwsInput.getSignature())) {
Expand Down Expand Up @@ -289,8 +289,7 @@ private JWSInput getJwsInput(String jwt) throws JWSInputException {
* @throws VCIssuerException
*/
private void validateJwsHeader(VCIssuanceContext vcIssuanceContext, JWSHeader jwsHeader) throws VCIssuerException {
String alg = Optional.ofNullable(jwsHeader.getAlgorithm())
.map(Enum::name)
String alg = Optional.ofNullable(jwsHeader.getRawAlgorithm())
.orElseThrow(() -> new VCIssuerException(ErrorType.INVALID_PROOF, "Missing jwsHeader claim alg"));
if (!CryptoUtils.getSupportedAsymmetricSignatureAlgorithms(keycloakSession).contains(alg)) {
throw new VCIssuerException(ErrorType.INVALID_PROOF, "Proof signature algorithm not supported: " + alg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ protected boolean verifyToken(String tokenStr) {
TokenVerifier<T> verifier = TokenVerifier.create(tokenStr, getTokenClass())
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));

SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, verifier.getHeader().getRawAlgorithm()).verifier(verifier.getHeader().getKeyId());
verifier.verifierContext(verifierContext);

this.token = verifier.verify().getToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.keys.loader.PublicKeyStorageManager;
Expand Down Expand Up @@ -199,11 +198,7 @@ private Response issueUserInfo() {
verifier = DPoPUtil.withDPoPVerifier(verifier, realm, new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).accessToken(tokenForUserInfo.getToken()));

JWSHeader header = verifier.getHeader();
Algorithm algorithm = header.getAlgorithm();
if (algorithm == null) {
throw new VerificationException("Missing token algorithm");
}
SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, algorithm.name()).verifier(header.getKeyId());
SignatureVerifierContext verifierContext = CryptoUtils.getSignatureProvider(session, header.getRawAlgorithm()).verifier(header.getKeyId());
verifier.verifierContext(verifierContext);

token = verifier.verify().getToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private void validateSigningAlgorithm() throws ClientPolicyException {
} catch (JWSInputException e) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not allowed input format.");
}
verifySecureSigningAlgorithm(jws.getHeader().getAlgorithm().name());
verifySecureSigningAlgorithm(jws.getHeader().getRawAlgorithm());
}

private boolean supportsClientAssertion(String clientAuthenticatorType) {
Expand Down
Loading
0