Skip to content

Commit

Permalink
Support token verification with the inlined certificate chain
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Nov 27, 2023
1 parent 670b43c commit 57c341b
Show file tree
Hide file tree
Showing 15 changed files with 486 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
.setPassword(oidcConfig.tls.getTrustStorePassword().orElse("password"))
.setAlias(oidcConfig.tls.getTrustStoreCertAlias().orElse(null))
.setValue(io.vertx.core.buffer.Buffer.buffer(trustStoreData))
.setType(getStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get()))
.setType(getKeyStoreType(oidcConfig.tls.trustStoreFileType, oidcConfig.tls.trustStoreFile.get()))
.setProvider(oidcConfig.tls.trustStoreProvider.orElse(null));
options.setTrustOptions(trustStoreOptions);
if (Verification.CERTIFICATE_VALIDATION == oidcConfig.tls.verification.orElse(Verification.REQUIRED)) {
Expand All @@ -156,7 +156,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
.setAlias(oidcConfig.tls.keyStoreKeyAlias.orElse(null))
.setAliasPassword(oidcConfig.tls.keyStoreKeyPassword.orElse(null))
.setValue(io.vertx.core.buffer.Buffer.buffer(keyStoreData))
.setType(getStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get()))
.setType(getKeyStoreType(oidcConfig.tls.keyStoreFileType, oidcConfig.tls.keyStoreFile.get()))
.setProvider(oidcConfig.tls.keyStoreProvider.orElse(null));

if (oidcConfig.tls.keyStorePassword.isPresent()) {
Expand Down Expand Up @@ -184,7 +184,7 @@ public static void setHttpClientOptions(OidcCommonConfig oidcConfig, TlsConfig t
options.setConnectTimeout((int) oidcConfig.getConnectionTimeout().toMillis());
}

private static String getStoreType(Optional<String> fileType, Path storePath) {
public static String getKeyStoreType(Optional<String> fileType, Path storePath) {
if (fileType.isPresent()) {
return fileType.get().toUpperCase();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.oidc;

import java.nio.file.Path;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -170,6 +171,93 @@ public void setIncludeClientId(boolean includeClientId) {
@ConfigItem
public Logout logout = new Logout();

public static enum VerificationMode {
/**
* Indicates that the token are verified using JSON Web Keys fetched from the OIDC provider or
* introspected remotely with the OIDC provider introspection endpoint.
*/
SERVER,

/**
* Indicates that the token signatures must be verified with the certificate chain inlined in the Base64 encoded format
* as the token `x5c` header
*/
CERTIFICATE_CHAIN
}

/**
* Verification mode indicating how the tokens must be verified.
* <p/>
* By default, the server controls the verification process: it will verify tokens with JSON Web Keys
* acquired from the OIDC provider or it will request the OIDC provider to introspect tokens.
* <p/>
* If the mode is set to `certificate-chain` then the tokens must be verified using the certificate
* chain inlined in the Base64-encoded format as an `x5c` header in the token itself.
* Effectively, the verification material is provided by the client, therefore you should only
* enable this mode when a certification chain can be restricted to known root authorities.
*/
@ConfigItem(defaultValue = "server")
public VerificationMode verificationMode = VerificationMode.SERVER;

/**
* Configuration of the certificate chain which can be used to verify tokens
* if the {@link #verificationMode} is set to `certificate-chain`.
*/
@ConfigItem
public CertificateChain certificateChain = new CertificateChain();

@ConfigGroup
public static class CertificateChain {
/**
* Truststore file which keeps thumbprints of the trusted certificates
*/
@ConfigItem
public Optional<Path> trustStoreFile = Optional.empty();

/**
* A parameter to specify the password of the truststore file if it is configured with {@link #trustStoreFile}.
*/
@ConfigItem
public Optional<String> trustStorePassword;

/**
* A parameter to specify the alias of the truststore certificate.
*/
@ConfigItem
public Optional<String> trustStoreCertAlias = Optional.empty();

/**
* An optional parameter to specify type of the truststore file. If not given, the type is automatically detected
* based on the file name.
*/
@ConfigItem
public Optional<String> trustStoreFileType = Optional.empty();

public Optional<Path> getTrustStoreFile() {
return trustStoreFile;
}

public void setTrustStoreFile(Path trustStoreFile) {
this.trustStoreFile = Optional.of(trustStoreFile);
}

public Optional<String> getTrustStoreCertAlias() {
return trustStoreCertAlias;
}

public void setTrustStoreCertAlias(String trustStoreCertAlias) {
this.trustStoreCertAlias = Optional.of(trustStoreCertAlias);
}

public Optional<String> getTrustStoreFileType() {
return trustStoreFileType;
}

public void setTrustStoreFileType(Optional<String> trustStoreFileType) {
this.trustStoreFileType = trustStoreFileType;
}
}

/**
* Different options to configure authorization requests
*/
Expand Down Expand Up @@ -1811,4 +1899,20 @@ public CodeGrant getCodeGrant() {
public void setCodeGrant(CodeGrant codeGrant) {
this.codeGrant = codeGrant;
}

public VerificationMode getVerificationMode() {
return verificationMode;
}

public void setVerificationMode(VerificationMode verificationMode) {
this.verificationMode = verificationMode;
}

public CertificateChain getCertificateChain() {
return certificateChain;
}

public void setCertificateChain(CertificateChain certificateChain) {
this.certificateChain = certificateChain;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.quarkus.oidc.IdTokenCredential;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.OidcTenantConfig.VerificationMode;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.TokenIntrospectionCache;
import io.quarkus.oidc.UserInfo;
Expand Down Expand Up @@ -102,6 +103,9 @@ private Uni<SecurityIdentity> authenticate(TokenAuthenticationRequest request, M
if (resolvedContext.oidcConfig.publicKey.isPresent()) {
LOG.debug("Performing token verification with a configured public key");
return validateTokenWithoutOidcServer(request, resolvedContext);
} else if (resolvedContext.oidcConfig.getVerificationMode() == VerificationMode.CERTIFICATE_CHAIN) {
LOG.debug("Performing token verification with a public key inlined in the certificate chain");
return validateTokenWithoutOidcServer(request, resolvedContext);
} else {
return validateAllTokensWithOidcServer(requestData, request, resolvedContext);
}
Expand Down Expand Up @@ -557,7 +561,8 @@ private static Uni<SecurityIdentity> validateTokenWithoutOidcServer(TokenAuthent
TenantConfigContext resolvedContext) {

try {
TokenVerificationResult result = resolvedContext.provider.verifyJwtToken(request.getToken().getToken(), false,
TokenVerificationResult result = resolvedContext.provider.verifyJwtToken(request.getToken().getToken(),
resolvedContext.oidcConfig.token.subjectRequired,
false, null);
return Uni.createFrom()
.item(validateAndCreateIdentity(Map.of(), request.getToken(), resolvedContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import java.io.Closeable;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;

Expand Down Expand Up @@ -35,6 +37,8 @@
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.CertificateChain;
import io.quarkus.oidc.OidcTenantConfig.VerificationMode;
import io.quarkus.oidc.TokenCustomizer;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
Expand All @@ -44,6 +48,7 @@
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.impl.CertificateHelper;

public class OidcProvider implements Closeable {

Expand Down Expand Up @@ -103,7 +108,13 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD
this.client = null;
this.oidcConfig = oidcConfig;
this.tokenCustomizer = TokenCustomizerFinder.find(oidcConfig);
this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc);
if (publicKeyEnc != null) {
this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc);
} else if (oidcConfig.getVerificationMode() == VerificationMode.CERTIFICATE_CHAIN) {
this.asymmetricKeyResolver = new CertChainPublicKeyResolver(oidcConfig.certificateChain);
} else {
throw new IllegalStateException("Neither public key nor certificate chain verification modes are enabled");
}
this.keyResolverProvider = null;
this.issuer = checkIssuerProp();
this.audience = checkAudienceProp();
Expand Down Expand Up @@ -529,6 +540,42 @@ public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContex

}

private static class CertChainPublicKeyResolver implements RefreshableVerificationKeyResolver {
final Set<String> thumbprints;

public CertChainPublicKeyResolver(CertificateChain chain) {
thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints(chain.trustStoreFile.get(),
chain.trustStorePassword.get(), chain.trustStoreCertAlias, chain.getTrustStoreFileType());
}

@Override
public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext)
throws UnresolvableKeyException {

try {
List<X509Certificate> chain = jws.getCertificateChainHeaderValue();
if (chain == null) {
throw new UnresolvableKeyException("Token does not have an 'x5c' certificate chain header");
}
String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0));
if (!thumbprints.contains(thumbprint)) {
throw new UnresolvableKeyException("Certificate chain thumprint is invalid");
}
//TODO: support revocation lists
CertificateHelper.checkValidity(chain, null);
if (chain.size() == 1) {
// CertificateHelper.checkValidity does not currently
// verify the certificate signature if it is a single certificate chain
final X509Certificate root = chain.get(0);
root.verify(root.getPublicKey());
}
return chain.get(0).getPublicKey();
} catch (Exception ex) {
throw new UnresolvableKeyException("Invalid certificate chain", ex);
}
}
}

private class SymmetricKeyResolver implements VerificationKeyResolver {
@Override
public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.CertificateChain;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy;
import io.quarkus.oidc.OidcTenantConfig.VerificationMode;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.oidc.common.OidcRequestFilter;
Expand Down Expand Up @@ -180,10 +182,18 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf
return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig));
}

if (oidcConfig.getPublicKey().isPresent() && oidcConfig.getVerificationMode() == VerificationMode.CERTIFICATE_CHAIN) {
throw new ConfigurationException("Both public key and certificate chain verification modes are enabled");
}

if (oidcConfig.getPublicKey().isPresent()) {
return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig));
}

if (oidcConfig.getVerificationMode() == VerificationMode.CERTIFICATE_CHAIN) {
return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig));
}

try {
if (!oidcConfig.getAuthServerUrl().isPresent()) {
if (DEFAULT_TENANT_ID.equals(oidcConfig.tenantId.get())) {
Expand Down Expand Up @@ -332,6 +342,22 @@ private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantCo
new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
}

private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) {
if (!OidcUtils.isServiceApp(oidcConfig)) {
throw new ConfigurationException(
"Currently only 'service' applications can be user to verify tokens with inlined certificate chains");
}

CertificateChain chain = oidcConfig.certificateChain;
if (chain.trustStoreFile.isEmpty() || chain.trustStorePassword.isEmpty()) {
throw new ConfigurationException(
"Truststore with configured password which keeps thumbprints of the trusted certificates must be present");
}

return new TenantConfigContext(
new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
}

public void setSecurityEventObserved(boolean isSecurityEventObserved) {
DefaultTenantConfigResolver bean = Arc.container().instance(DefaultTenantConfigResolver.class).get();
bean.setSecurityEventObserved(isSecurityEventObserved);
Expand Down
Loading

0 comments on commit 57c341b

Please sign in to comment.