Skip to content

Commit

Permalink
Better support for OIDC JWK keys without kid
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Aug 24, 2023
1 parent 56188a6 commit e28b959
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package io.quarkus.oidc.runtime;

import java.security.Key;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.lang.InvalidAlgorithmException;
import org.jose4j.lang.JoseException;

import io.quarkus.logging.Log;
import io.quarkus.oidc.OIDCException;

public class JsonWebKeySet {
Expand All @@ -23,7 +28,7 @@ public class JsonWebKeySet {

private Map<String, Key> keysWithKeyId = new HashMap<>();
private Map<String, Key> keysWithThumbprints = new HashMap<>();
private Key keyWithoutKeyIdAndThumbprint;
private Map<String, List<Key>> keysWithoutKeyIdAndThumbprint = new HashMap<>();

public JsonWebKeySet(String json) {
initKeys(json);
Expand All @@ -43,12 +48,16 @@ private void initKeys(String json) {
if (x5t != null && jwkKey.getKey() != null) {
keysWithThumbprints.put(x5t, jwkKey.getKey());
}
if (jwkKey.getKeyId() == null && x5t == null && jwkKey.getKeyType() != null) {
List<Key> keys = keysWithoutKeyIdAndThumbprint.get(jwkKey.getKeyType());
if (keys == null) {
keys = new ArrayList<>();
keysWithoutKeyIdAndThumbprint.put(jwkKey.getKeyType(), keys);
}
keys.add(jwkKey.getKey());
}
}
}
if (keysWithKeyId.isEmpty() && keysWithThumbprints.isEmpty() && jwkSet.getJsonWebKeys().size() == 1
&& isSupportedJwkKey(jwkSet.getJsonWebKeys().get(0))) {
keyWithoutKeyIdAndThumbprint = jwkSet.getJsonWebKeys().get(0).getKey();
}
} catch (JoseException ex) {
throw new OIDCException(ex);
}
Expand All @@ -67,7 +76,13 @@ public Key getKeyWithThumbprint(String x5t) {
return keysWithThumbprints.get(x5t);
}

public Key getKeyWithoutKeyIdAndThumbprint() {
return keyWithoutKeyIdAndThumbprint;
public Key getKeyWithoutKeyIdAndThumbprint(JsonWebSignature jws) {
try {
List<Key> keys = keysWithoutKeyIdAndThumbprint.get(jws.getKeyType());
return keys == null || keys.size() != 1 ? null : keys.get(0);
} catch (InvalidAlgorithmException ex) {
Log.debug("Token 'alg'(algorithm) header value is invalid");
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContex
}

if (key == null && kid == null && thumbprint == null) {
key = jwks.getKeyWithoutKeyIdAndThumbprint();
key = jwks.getKeyWithoutKeyIdAndThumbprint(jws);
}

if (key == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
import jakarta.json.JsonObject;

import org.eclipse.microprofile.jwt.Claims;
import org.jose4j.jwk.EcJwkGenerator;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.keys.EllipticCurves;
import org.jose4j.lang.UnresolvableKeyException;
import org.junit.jupiter.api.Test;

import io.quarkus.oidc.OidcTenantConfig;
Expand All @@ -22,36 +26,72 @@

public class OidcProviderTest {

@SuppressWarnings("resource")
@Test
public void testAlgorithmCustomizer() throws Exception {

RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("k1");

final String token = Jwt.issuer("http://keycloak/ream").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey());
final String token = Jwt.issuer("http://keycloak/realm").jws().keyId("k1").sign(rsaJsonWebKey.getPrivateKey());
final String newToken = replaceAlgorithm(token, "ES256");
JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}");
OidcTenantConfig oidcConfig = new OidcTenantConfig();

OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null);
try {
provider.verifyJwtToken(newToken, false, false, null);
fail("InvalidJwtException expected");
} catch (InvalidJwtException ex) {
// continue
try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) {
try {
provider.verifyJwtToken(newToken, false, false, null);
fail("InvalidJwtException expected");
} catch (InvalidJwtException ex) {
// continue
}
}

provider = new OidcProvider(null, oidcConfig, jwkSet, new TokenCustomizer() {
try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, new TokenCustomizer() {

@Override
public JsonObject customizeHeaders(JsonObject headers) {
return Json.createObjectBuilder(headers).add("alg", "RS256").build();
}

}, null);
TokenVerificationResult result = provider.verifyJwtToken(newToken, false, false, null);
assertEquals("http://keycloak/ream", result.localVerificationResult.getString("iss"));
}, null)) {
TokenVerificationResult result = provider.verifyJwtToken(newToken, false, false, null);
assertEquals("http://keycloak/realm", result.localVerificationResult.getString("iss"));
}
}

@Test
public void testTokenWithoutKidSingleRsaJwkWithoutKid() throws Exception {
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
EllipticCurveJsonWebKey ecJsonWebKey = EcJwkGenerator.generateJwk(EllipticCurves.P256);

JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "," + ecJsonWebKey.toJson() + "]}");

final String token = Jwt.issuer("http://keycloak/realm").sign(rsaJsonWebKey.getPrivateKey());

try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null, null)) {
TokenVerificationResult result = provider.verifyJwtToken(token, false, false, null);
assertEquals("http://keycloak/realm", result.localVerificationResult.getString("iss"));
}
}

@Test
public void testTokenWithoutKidMultipleRSAJwkWithoutKid() throws Exception {
RsaJsonWebKey rsaJsonWebKey1 = RsaJwkGenerator.generateJwk(2048);
RsaJsonWebKey rsaJsonWebKey2 = RsaJwkGenerator.generateJwk(2048);
JsonWebKeySet jwkSet = new JsonWebKeySet(
"{\"keys\": [" + rsaJsonWebKey1.toJson() + "," + rsaJsonWebKey2.toJson() + "]}");

final String token = Jwt.issuer("http://keycloak/realm").sign(rsaJsonWebKey1.getPrivateKey());

try (OidcProvider provider = new OidcProvider(null, new OidcTenantConfig(), jwkSet, null, null)) {
try {
provider.verifyJwtToken(token, false, false, null);
fail("InvalidJwtException expected");
} catch (InvalidJwtException ex) {
assertTrue(ex.getCause() instanceof UnresolvableKeyException);
}

}
}

private static String replaceAlgorithm(String token, String algorithm) {
Expand Down

0 comments on commit e28b959

Please sign in to comment.