Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.oidc.runtime;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -58,6 +59,12 @@ public class OidcConfig {
@ConfigItem
Roles roles;

/**
* Configuration how to validate the token claims.
*/
@ConfigItem
Token token;

/**
* Credentials which the OIDC adapter will use to authenticate to the OIDC server.
*/
Expand Down Expand Up @@ -160,4 +167,34 @@ public static class Authentication {
@ConfigItem
public Optional<List<String>> scopes;
}

@ConfigGroup
public static class Token {

/**
* Expected issuer 'iss' claim value
*/
@ConfigItem
public Optional<String> issuer;

/**
* Expected audience `aud` claim value which may be a string or an array of strings
*/
@ConfigItem
public Optional<List<String>> audience;

public static Token fromIssuer(String issuer) {
Token tokenClaims = new Token();
tokenClaims.issuer = Optional.of(issuer);
tokenClaims.audience = Optional.ofNullable(null);
return tokenClaims;
}

public static Token fromAudience(String... audience) {
Token tokenClaims = new Token();
tokenClaims.issuer = Optional.ofNullable(null);
tokenClaims.audience = Optional.of(Arrays.asList(audience));
return tokenClaims;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;

import io.quarkus.oidc.OIDCException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.ForbiddenException;
import io.quarkus.security.identity.AuthenticationRequestContext;
Expand Down Expand Up @@ -59,6 +60,13 @@ public void handle(AsyncResult<AccessToken> event) {
return;
}
AccessToken token = event.result();
try {
OidcUtils.validateClaims(config.token, token.accessToken());
} catch (OIDCException e) {
result.completeExceptionally(new AuthenticationFailedException(e));
return;
}

QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder();

JsonWebToken jwtPrincipal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public void setup(OidcConfig config, OidcBuildTimeConfig btConfig, RuntimeValue<
.setAlgorithm("RS256")
.setPublicKey(config.publicKey.get()));
}
if (config.token.issuer.isPresent()) {
options.setValidateIssuer(false);
}

final long connectionDelayInSecs = config.connectionDelay.isPresent() ? config.connectionDelay.get().toMillis() / 1000
: 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.quarkus.oidc.runtime;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.eclipse.microprofile.jwt.Claims;

import io.quarkus.oidc.OIDCException;
import io.vertx.core.json.JsonArray;
Expand All @@ -16,16 +18,38 @@ private OidcUtils() {

}

public static boolean validateClaims(OidcConfig.Token tokenConfig, JsonObject json) {
if (tokenConfig.issuer.isPresent()) {
String issuer = json.getString(Claims.iss.name());
if (!tokenConfig.issuer.get().equals(issuer)) {
throw new OIDCException("Invalid issuer");
}
}
if (tokenConfig.audience.isPresent()) {
Object claimValue = json.getValue(Claims.aud.name());
List<String> audience = Collections.emptyList();
if (claimValue instanceof JsonArray) {
audience = convertJsonArrayToList((JsonArray) claimValue);
} else if (claimValue != null) {
audience = Arrays.asList((String) claimValue);
}
if (!audience.containsAll(tokenConfig.audience.get())) {
throw new OIDCException("Invalid audience");
}
}
return true;
}

public static List<String> findRoles(String clientId, OidcConfig.Roles rolesConfig, JsonObject json) {
// If the user configured a specific path - check and enforce a claim at this path exists
if (rolesConfig.getRoleClaimPath().isPresent()) {
return findClaimWithRoles(rolesConfig, rolesConfig.getRoleClaimPath().get(), json, true);
}

// Check 'groups' next
List<String> groups = findClaimWithRoles(rolesConfig, "groups", json, false);
List<String> groups = findClaimWithRoles(rolesConfig, Claims.groups.name(), json, false);
if (!groups.isEmpty()) {
return groups.stream().map(v -> v.toString()).collect(Collectors.toList());
return groups;
} else {
// Finally, check if this token has been issued by Keycloak.
// Return an empty or populated list of realm and resource access roles
Expand All @@ -45,7 +69,7 @@ private static List<String> findClaimWithRoles(OidcConfig.Roles rolesConfig, Str
Object claimValue = findClaimValue(claimPath, json, claimPath.split("/"), 0, mustExist);

if (claimValue instanceof JsonArray) {
return ((JsonArray) claimValue).stream().map(v -> v.toString()).collect(Collectors.toList());
return convertJsonArrayToList((JsonArray) claimValue);
} else if (claimValue != null) {
String sep = rolesConfig.getRoleClaimSeparator().isPresent() ? rolesConfig.getRoleClaimSeparator().get() : " ";
return Arrays.asList(claimValue.toString().split(sep));
Expand All @@ -71,4 +95,12 @@ private static Object findClaimValue(String claimPath, JsonObject json, String[]

return claimValue;
}

private static List<String> convertJsonArrayToList(JsonArray claimValue) {
List<String> list = new ArrayList<>(claimValue.size());
for (int i = 0; i < claimValue.size(); i++) {
list.add(claimValue.getString(i));
}
return list;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,68 @@

import org.junit.jupiter.api.Test;

import io.quarkus.oidc.OIDCException;
import io.vertx.core.json.JsonObject;

public class OidcUtilsTest {

@Test
public void testTokenWithCorrectIssuer() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromIssuer("https://server.example.com");
InputStream is = getClass().getResourceAsStream("/tokenIssuer.json");
assertTrue(OidcUtils.validateClaims(tokenClaims, read(is)));
}

@Test
public void testTokenWithWrongIssuer() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromIssuer("https://servers.example.com");
InputStream is = getClass().getResourceAsStream("/tokenIssuer.json");
try {
OidcUtils.validateClaims(tokenClaims, read(is));
fail("Exception expected: wrong issuer");
} catch (OIDCException ex) {
// expected
}
}

@Test
public void testTokenWithCorrectStringAudience() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromAudience("https://quarkus.example.com");
InputStream is = getClass().getResourceAsStream("/tokenStringAudience.json");
assertTrue(OidcUtils.validateClaims(tokenClaims, read(is)));
}

@Test
public void testTokenWithWrongStringAudience() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromIssuer("https://quarkus.examples.com");
InputStream is = getClass().getResourceAsStream("/tokenStringAudience.json");
try {
OidcUtils.validateClaims(tokenClaims, read(is));
fail("Exception expected: wrong audience");
} catch (OIDCException ex) {
// expected
}
}

@Test
public void testTokenWithCorrectArrayAudience() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromAudience("https://quarkus.example.com", "frontend_client_id");
InputStream is = getClass().getResourceAsStream("/tokenArrayAudience.json");
assertTrue(OidcUtils.validateClaims(tokenClaims, read(is)));
}

@Test
public void testTokenWithWrongArrayAudience() throws Exception {
OidcConfig.Token tokenClaims = OidcConfig.Token.fromAudience("service_client_id");
InputStream is = getClass().getResourceAsStream("/tokenArrayAudience.json");
try {
OidcUtils.validateClaims(tokenClaims, read(is));
fail("Exception expected: wrong array audience");
} catch (OIDCException ex) {
// expected
}
}

@Test
public void testKeycloakRealmAccessToken() throws Exception {
OidcConfig.Roles rolesCfg = OidcConfig.Roles.fromClaimPath(null);
Expand Down
11 changes: 11 additions & 0 deletions extensions/oidc/runtime/src/test/resources/tokenArrayAudience.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"iss": "https://server.example.com",
"aud": ["https://quarkus.example.com", "frontend_client_id"],
"jti": "a-123",
"sub": "24400320",
"upn": "[email protected]",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969
}
11 changes: 11 additions & 0 deletions extensions/oidc/runtime/src/test/resources/tokenIssuer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"iss": "https://server.example.com",
"jti": "a-123",
"sub": "24400320",
"upn": "[email protected]",
"preferred_username": "jdoe",
"aud": "s6BhdRkqt3",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"iss": "https://server.example.com",
"aud": "https://quarkus.example.com",
"jti": "a-123",
"sub": "24400320",
"upn": "[email protected]",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"auth_time": 1311280969
}