Skip to content

Commit

Permalink
Use single key JWK Set if neither kid nor x5t header is set
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Mar 31, 2022
1 parent 6346b35 commit d4bea95
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class JsonWebKeySet {

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

public JsonWebKeySet(String json) {
initKeys(json);
Expand All @@ -26,9 +27,7 @@ private void initKeys(String json) {
try {
org.jose4j.jwk.JsonWebKeySet jwkSet = new org.jose4j.jwk.JsonWebKeySet(json);
for (JsonWebKey jwkKey : jwkSet.getJsonWebKeys()) {
if ((RSA_KEY_TYPE.equals(jwkKey.getKeyType()) || EC_KEY_TYPE.equals(jwkKey.getKeyType())
|| jwkKey.getKeyType() == null)
&& (SIGNATURE_USE.equals(jwkKey.getUse()) || jwkKey.getUse() == null)) {
if (isSupportedJwkKey(jwkKey)) {
if (jwkKey.getKeyId() != null) {
keysWithKeyId.put(jwkKey.getKeyId(), jwkKey.getKey());
}
Expand All @@ -40,16 +39,30 @@ private void initKeys(String json) {
}
}
}
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);
}
}

private static boolean isSupportedJwkKey(JsonWebKey jwkKey) {
return (RSA_KEY_TYPE.equals(jwkKey.getKeyType()) || EC_KEY_TYPE.equals(jwkKey.getKeyType())
|| jwkKey.getKeyType() == null)
&& (SIGNATURE_USE.equals(jwkKey.getUse()) || jwkKey.getUse() == null);
}

public Key getKeyWithId(String kid) {
return keysWithKeyId.get(kid);
}

public Key getKeyWithThumbprint(String x5t) {
return keysWithThumbprints.get(x5t);
}

public Key getKeyWithoutKeyIdAndThumbprint() {
return keyWithoutKeyIdAndThumbprint;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContex
}
}

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

if (key == null) {
throw new UnresolvableKeyException(
String.format("JWK is not available, neither 'kid' nor 'x5t' token headers are set", kid));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public String adminNoIntrospection() {
return "granted:" + identity.getRoles();
}

@Path("bearer-key-without-kid-thumbprint")
@GET
@RolesAllowed("admin")
@Produces(MediaType.APPLICATION_JSON)
public String adminNoKidandThumprint() {
return "granted:" + identity.getRoles();
}

@Path("bearer-wrong-role-path")
@GET
@RolesAllowed("admin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public String resolve(RoutingContext context) {
if (path.endsWith("bearer-no-introspection")) {
return "bearer-no-introspection";
}
if (path.endsWith("bearer-key-without-kid-thumbprint")) {
return "bearer-key-without-kid-thumbprint";
}
if (path.endsWith("bearer-wrong-role-path")) {
return "bearer-wrong-role-path";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ quarkus.oidc.bearer-no-introspection.authentication.scopes=profile,email,phone
quarkus.oidc.bearer-no-introspection.token.audience=https://service.example.com
quarkus.oidc.bearer-no-introspection.token.allow-jwt-introspection=false

quarkus.oidc.bearer-key-without-kid-thumbprint.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.bearer-key-without-kid-thumbprint.discovery-enabled=false
quarkus.oidc.bearer-key-without-kid-thumbprint.jwks-path=single-key-without-kid-thumbprint
quarkus.oidc.bearer-key-without-kid-thumbprint.client-id=quarkus-app
quarkus.oidc.bearer-key-without-kid-thumbprint.credentials.secret=secret
quarkus.oidc.bearer-key-without-kid-thumbprint.authentication.scopes=profile,email,phone
quarkus.oidc.bearer-key-without-kid-thumbprint.token.audience=https://service.example.com
quarkus.oidc.bearer-key-without-kid-thumbprint.token.allow-jwt-introspection=false

quarkus.oidc.bearer-wrong-role-path.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.bearer-wrong-role-path.client-id=quarkus-app
quarkus.oidc.bearer-wrong-role-path.credentials.secret=secret
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ public void testAccessAdminResourceWithCertThumbprint() {
.body(Matchers.containsString("admin"));
}

@Test
public void testAccessAdminResourceWithoutKidAndThumbprint() {
RestAssured.given().auth().oauth2(getAccessTokenWithoutKidAndThumbprint("admin", Set.of("admin")))
.when().get("/api/admin/bearer-no-introspection")
.then()
.statusCode(401);
}

@Test
public void testTokenAndKeyWithoutKidAndThumbprint() {
RestAssured.given().auth().oauth2(getAccessTokenWithoutKidAndThumbprint("admin", Set.of("admin")))
.when().get("/api/admin/bearer-key-without-kid-thumbprint")
.then()
.statusCode(200)
.body(Matchers.containsString("admin"));
}

@Test
public void testTokenWithKidAndKeyWithoutKidAndThumbprint() {
RestAssured.given().auth().oauth2(getAccessToken("admin", Set.of("admin")))
.when().get("/api/admin/bearer-key-without-kid-thumbprint")
.then()
.statusCode(401);
}

@Test
public void testSecureAccessSuccessPreferredUsernameWrongRolePath() {
for (String username : Arrays.asList("alice", "admin")) {
Expand Down Expand Up @@ -163,6 +188,14 @@ private String getAccessTokenWithThumbprint(String userName, Set<String> groups)
.sign("privateKeyWithoutKid.jwk");
}

private String getAccessTokenWithoutKidAndThumbprint(String userName, Set<String> groups) {
return Jwt.preferredUserName(userName)
.groups(groups)
.issuer("https://server.example.com")
.audience("https://service.example.com")
.sign("privateKeyWithoutKid.jwk");
}

private String getAccessTokenWrongAudience(String userName, Set<String> groups) {
return Jwt.preferredUserName(userName)
.groups(groups)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@ public Map<String, String> start() {
" ]\n" +
"}")));

server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/single-key-without-kid-thumbprint"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"keys\" : [\n" +
" {\n" +
" \"kty\":\"RSA\",\n" +
" \"n\":\"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ\",\n"
+
" \"e\":\"AQAB\"\n" +
" }" +
" ]\n" +
"}")));

server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo"))
.willReturn(aResponse()
Expand Down

0 comments on commit d4bea95

Please sign in to comment.