Skip to content

Commit

Permalink
Allow to customize OIDC verification
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed May 11, 2023
1 parent c59afb6 commit 12a961f
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,16 @@ public static Token fromAudience(String... audience) {
@ConfigItem(defaultValue = "true")
public boolean allowOpaqueTokenIntrospection = true;

/**
* Token customizer name.
*
* Allows to select a tenant specific token customizer.
* If this property is not set then a default `io.quarkus.oidc.TokenCustomizer` bean
* will be used if registered.
*/
@ConfigItem
public Optional<String> tokenCustomizer = Optional.empty();

/**
* Indirectly verify that the opaque (binary) access token is valid by using it to request UserInfo.
* Opaque access token is considered valid if the provider accepted this token and returned a valid UserInfo.
Expand Down Expand Up @@ -1386,6 +1396,14 @@ public boolean isRequireJwtIntrospectionOnly() {
public void setRequireJwtIntrospectionOnly(boolean requireJwtIntrospectionOnly) {
this.requireJwtIntrospectionOnly = requireJwtIntrospectionOnly;
}

public Optional<String> getTokenCustomizer() {
return tokenCustomizer;
}

public void setTokenCustomizer(String tokenCustomizer) {
this.tokenCustomizer = Optional.of(tokenCustomizer);
}
}

public static enum ApplicationType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.oidc;

import jakarta.json.JsonObject;

/**
* TokenCustomizer can be used to change token headers for the token verification to succeed
*/
public interface TokenCustomizer {
/**
* Customize token headers
*
* @param headers the token headers
* @return modified headers, null can be returned to indicate no modification has taken place
*/
JsonObject customizeHeaders(JsonObject headers);
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ protected String getNonNullJsonString() {
return jsonString == null ? json.toString() : jsonString;
}

private static JsonObject toJsonObject(String userInfoJson) {
try (JsonReader jsonReader = Json.createReader(new StringReader(userInfoJson))) {
static JsonObject toJsonObject(String json) {
try (JsonReader jsonReader = Json.createReader(new StringReader(json))) {
return jsonReader.readObject();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package io.quarkus.oidc.runtime;

import java.io.Closeable;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;

import jakarta.json.JsonObject;

import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logging.Logger;
import org.jose4j.jwa.AlgorithmConstraints;
Expand All @@ -29,6 +33,7 @@
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCustomizer;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcConstants;
Expand All @@ -55,14 +60,21 @@ public class OidcProvider implements Closeable {
final OidcProviderClient client;
final RefreshableVerificationKeyResolver asymmetricKeyResolver;
final OidcTenantConfig oidcConfig;
final TokenCustomizer tokenCustomizer;
final String issuer;
final String[] audience;
final Map<String, String> requiredClaims;
final Key tokenDecryptionKey;

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) {
this(client, oidcConfig, jwks, findTokenCustomizer(oidcConfig), tokenDecryptionKey);
}

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks,
TokenCustomizer tokenCustomizer, Key tokenDecryptionKey) {
this.client = client;
this.oidcConfig = oidcConfig;
this.tokenCustomizer = tokenCustomizer;
this.asymmetricKeyResolver = jwks == null ? null
: new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval);

Expand All @@ -75,13 +87,19 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json
public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) {
this.client = null;
this.oidcConfig = oidcConfig;
this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig.token.getTokenCustomizer().orElse(null));
this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc);
this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
this.requiredClaims = checkRequiredClaimsProp();
this.tokenDecryptionKey = tokenDecryptionKey;
}

private static TokenCustomizer findTokenCustomizer(OidcTenantConfig config) {

return config == null ? null : TokenCustomizerFinder.find(config.token.getTokenCustomizer().orElse(null));
}

private String checkIssuerProp() {
String issuerProp = null;
if (oidcConfig != null) {
Expand All @@ -107,7 +125,7 @@ public TokenVerificationResult verifySelfSignedJwtToken(String token) throws Inv
}

public TokenVerificationResult verifyJwtToken(String token) throws InvalidJwtException {
return verifyJwtTokenInternal(token, ASYMMETRIC_ALGORITHM_CONSTRAINTS, asymmetricKeyResolver, true);
return verifyJwtTokenInternal(customizeJwtToken(token), ASYMMETRIC_ALGORITHM_CONSTRAINTS, asymmetricKeyResolver, true);
}

public TokenVerificationResult verifyLogoutJwtToken(String token) throws InvalidJwtException {
Expand Down Expand Up @@ -180,6 +198,22 @@ private TokenVerificationResult verifyJwtTokenInternal(String token, AlgorithmCo
return result;
}

private String customizeJwtToken(String token) {
if (tokenCustomizer != null) {
JsonObject headers = AbstractJsonObjectResponse.toJsonObject(
OidcUtils.decodeJwtHeadersAsString(token));
headers = tokenCustomizer.customizeHeaders(headers);
if (headers != null) {
String newHeaders = new String(
Base64.getUrlEncoder().withoutPadding().encode(headers.toString().getBytes()),
StandardCharsets.UTF_8);
int dotIndex = token.indexOf('.');
return newHeaders + token.substring(dotIndex);
}
}
return token;
}

private void verifyTokenAge(Long iat) throws InvalidJwtException {
if (oidcConfig.token.age.isPresent() && iat != null) {
final long now = now() / 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ public static JsonObject decodeJwtHeaders(String jwt) {
return decodeAsJsonObject(tokens.nextToken());
}

public static String decodeJwtHeadersAsString(String jwt) {
StringTokenizer tokens = new StringTokenizer(jwt, ".");
return base64UrlDecode(tokens.nextToken());
}

public static List<String> findRoles(String clientId, OidcTenantConfig.Roles rolesConfig, JsonObject json) {
// If the user configured specific paths - check and enforce the claims at these paths exist
if (rolesConfig.getRoleClaimPath().isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.oidc.runtime;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.TokenCustomizer;

public class TokenCustomizerFinder {

public static TokenCustomizer find(String name) {
ArcContainer container = Arc.container();
TokenCustomizer tokenCustomizer = null;
if (container != null) {
tokenCustomizer = name != null
? (TokenCustomizer) container.instance(name).get()
: container.instance(TokenCustomizer.class).get();
}
if (tokenCustomizer == null && name != null) {
throw new OIDCException("Unable to find TokenCustomizer " + name);
}

return tokenCustomizer;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.quarkus.oidc.runtime;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import jakarta.json.Json;
import jakarta.json.JsonObject;

import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.junit.jupiter.api.Test;

import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCustomizer;
import io.smallrye.jwt.build.Jwt;

public class OidcProviderTest {

@SuppressWarnings("resource")
@Test
public void testCustomizer() 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 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);
fail("InvalidJwtException expected");
} catch (InvalidJwtException ex) {
// continue
}

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);
assertEquals("http://keycloak/ream", result.localVerificationResult.getString("iss"));
}

private static String replaceAlgorithm(String token, String algorithm) {
io.vertx.core.json.JsonObject headers = OidcUtils.decodeJwtHeaders(token);
headers.put("alg", algorithm);
String newHeaders = new String(
Base64.getUrlEncoder().withoutPadding().encode(headers.toString().getBytes()),
StandardCharsets.UTF_8);
int dotIndex = token.indexOf('.');
return newHeaders + token.substring(dotIndex);
}
}

0 comments on commit 12a961f

Please sign in to comment.