quarkus icon indicating copy to clipboard operation
quarkus copied to clipboard

Quarkus OIDC CredentialsProvider integration resolves secrets during BuildStep

Open ryandens opened this issue 8 months ago • 4 comments

Describe the bug

The Quarkus OIDC Extension appears to deviate from norms that I've observed in other extensions that have support for CredentialsProviders in that it attempts to resolve the secret value from the configured CredentialsProvider during STATIC_INIT.

I successfully use the CredentialsProvider to provide credentials to various Quarkus extensions (such as the data source extension and the Quarkiverse GitHub App Extension). I store secrets for these extensions in AWS Secrets Manager and use CredentialsProvider as a bridge to provide these extensions with values from AWS Secrets Manager. To do this, I leverage the Quarkiverse Amazon Services Secrets Manager Client extension. This provides my application with a SecretsManagerClient.

I recently tried to adopt the Quarkus OIDC extension and use the same pattern for safely storing and accessing secrets that this OIDC extension needs at runtime in order to validate users from OIDC providers.

I configured by application with the my desired OIDC provider, client ID, and client secret provider/key.

quarkus.oidc.provider=github
quarkus.oidc.client-id=foo
quarkus.oidc.credentials.client-secret.provider.name=aws-secrets-manager
quarkus.oidc.credentials.client-secret.provider.key=bar

I then leveraged by previous pattern of using a CredenentialsProvider to resolve secrets from AWS SecretsManager

@ApplicationScoped
@Unremovable
@Named("aws-secrets-manager")
public class SecretsManagerCredentialsProvider implements CredentialsProvider {

    private final ObjectMapper mapper;
    private final SecretsManagerClient secrets;

    @Inject
    public SecretsManagerCredentialsProvider(final ObjectMapper mapper, final SecretsManagerClient secrets) {
        this.mapper = mapper;
        this.secrets = secrets;
    }

    /**
     * @param credentialsProviderName in this context, this is the name of the secret in AWS Secrets
     *     Manager. The Secret value is expected to be a JSON object (which is typical for AWS Secrets
     *     Manager).
     * @return the secret value as a map of key-value pairs
     */
    @Override
    public Map<String, String> getCredentials(final String credentialsProviderName) {
 logger.debug("Getting credentials from secret: {}", credentialsProviderName);
        final GetSecretValueResponse response;
        try {
            response = secrets.getSecretValue(request -> request.secretId(credentialsProviderName));
        } catch (final ResourceNotFoundException e) {
            throw new IllegalArgumentException("Secret not found: " + credentialsProviderName, e);
        }
        // ..  
}

However, as a result, my application fails to initialize because the SecretsManagerCredentialsProvider is now required during the Quarkus OIDC OidcBuildStep and fails with the following error:

java.lang.RuntimeException: java.lang.RuntimeException: Failed to start quarkus
	at io.quarkus.test.junit.QuarkusTestExtension.throwBootFailureException(QuarkusTestExtension.java:642)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptTestClassConstructor(QuarkusTestExtension.java:726)
	at java.base/java.util.Optional.orElseGet(Optional.java:364)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.RuntimeException: Failed to start quarkus
	at io.quarkus.runner.ApplicationImpl.doStart(Unknown Source)
	at io.quarkus.runtime.Application.start(Application.java:101)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at io.quarkus.runner.bootstrap.StartupActionImpl.run(StartupActionImpl.java:285)
	at io.quarkus.test.junit.QuarkusTestExtension.doJavaStart(QuarkusTestExtension.java:251)
	at io.quarkus.test.junit.QuarkusTestExtension.ensureStarted(QuarkusTestExtension.java:609)
	at io.quarkus.test.junit.QuarkusTestExtension.beforeAll(QuarkusTestExtension.java:659)
	... 1 more
Caused by: org.gradle.internal.exceptions.DefaultMultiCauseException: Multiple exceptions caught:
	[Exception 0] jakarta.enterprise.inject.CreationException: Error creating synthetic bean [oRj83jIPijUzp7JkawwrGWJy-G8]: jakarta.enterprise.inject.CreationException: Synthetic bean instance for software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder not initialized yet: software_amazon_awssdk_services_secretsmanager_SecretsManagerClientBuilder_35edabeaf18440581dc996704efe9686730c9848
	- a synthetic bean initialized during RUNTIME_INIT must not be accessed during STATIC_INIT
	- RUNTIME_INIT build steps that require access to synthetic beans initialized during RUNTIME_INIT should consume the SyntheticBeansRuntimeInitBuildItem
	[Exception 1] io.quarkus.oidc.OIDCException
	at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap$UniOnFailureFlatMapProcessor.performInnerSubscription(UniOnFailureFlatMap.java:94)
	at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap$UniOnFailureFlatMapProcessor.dispatch(UniOnFailureFlatMap.java:83)
	at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap$UniOnFailureFlatMapProcessor.onFailure(UniOnFailureFlatMap.java:60)
	at io.smallrye.mutiny.operators.uni.UniOperatorProcessor.onFailure(UniOperatorProcessor.java:55)
	at io.smallrye.mutiny.operators.uni.UniOperatorProcessor.onFailure(UniOperatorProcessor.java:55)
	at io.smallrye.mutiny.operators.uni.UniOnItemOrFailureFlatMap$UniOnItemOrFailureFlatMapProcessor.performInnerSubscription(UniOnItemOrFailureFlatMap.java:91)
	at io.smallrye.mutiny.operators.uni.UniOnItemOrFailureFlatMap$UniOnItemOrFailureFlatMapProcessor.onItem(UniOnItemOrFailureFlatMap.java:54)
	at io.smallrye.mutiny.operators.uni.builders.UniCreateFromKnownItem$KnownItemSubscription.forward(UniCreateFromKnownItem.java:38)
	at io.smallrye.mutiny.operators.uni.builders.UniCreateFromKnownItem.subscribe(UniCreateFromKnownItem.java:23)
	at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
	at io.smallrye.mutiny.operators.uni.UniOnItemOrFailureFlatMap.subscribe(UniOnItemOrFailureFlatMap.java:27)
	at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
	at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni.subscribe(UniOnItemTransformToUni.java:25)
	at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
	at io.smallrye.mutiny.operators.uni.UniOnItemTransform.subscribe(UniOnItemTransform.java:22)
	at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
	at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap.subscribe(UniOnFailureFlatMap.java:31)
	at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:36)
	at io.smallrye.mutiny.operators.uni.UniBlockingAwait.await(UniBlockingAwait.java:60)
	at io.smallrye.mutiny.groups.UniAwait.atMost(UniAwait.java:65)
	at io.quarkus.oidc.runtime.OidcRecorder.createStaticTenantContext(OidcRecorder.java:166)
	at io.quarkus.oidc.runtime.OidcRecorder.setup(OidcRecorder.java:88)
	at io.quarkus.deployment.steps.OidcBuildStep$setup1008959783.deploy_0(Unknown Source)
	at io.quarkus.deployment.steps.OidcBuildStep$setup1008959783.deploy(Unknown Source)
	... 8 more
	Suppressed: io.quarkus.oidc.OIDCException
		at io.quarkus.oidc.runtime.OidcRecorder$5.apply(OidcRecorder.java:163)
		at io.quarkus.oidc.runtime.OidcRecorder$5.apply(OidcRecorder.java:145)
		at io.smallrye.context.impl.wrappers.SlowContextualFunction.apply(SlowContextualFunction.java:21)
		at io.smallrye.mutiny.groups.UniOnFailure.lambda$recoverWithItem$8(UniOnFailure.java:190)
		at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap$UniOnFailureFlatMapProcessor.performInnerSubscription(UniOnFailureFlatMap.java:92)
		... 31 more
Caused by: jakarta.enterprise.inject.CreationException: Error creating synthetic bean [oRj83jIPijUzp7JkawwrGWJy-G8]: jakarta.enterprise.inject.CreationException: Synthetic bean instance for software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder not initialized yet: software_amazon_awssdk_services_secretsmanager_SecretsManagerClientBuilder_35edabeaf18440581dc996704efe9686730c9848
	- a synthetic bean initialized during RUNTIME_INIT must not be accessed during STATIC_INIT
	- RUNTIME_INIT build steps that require access to synthetic beans initialized during RUNTIME_INIT should consume the SyntheticBeansRuntimeInitBuildItem
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_Bean.doCreate(Unknown Source)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_Bean.create(Unknown Source)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_Bean.create(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.c20(Unknown Source)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.computeIfAbsent(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.impl.ClientProxies.getApplicationScopedDelegate(ClientProxies.java:21)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_ClientProxy.arc$delegate(Unknown Source)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_ClientProxy.build(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer.<init>(SecretsManagerClientProducer.java:21)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_Bean.doCreate(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_Bean.create(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_Bean.create(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.c11(Unknown Source)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.computeIfAbsent(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.impl.ClientProxies.getApplicationScopedDelegate(ClientProxies.java:21)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_ClientProxy.arc$delegate(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_ClientProxy.arc_contextualInstance(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_ProducerMethod_client_o2u827DJgDuZ1zCfbjV6TqIsxgk_Bean.doCreate(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_ProducerMethod_client_o2u827DJgDuZ1zCfbjV6TqIsxgk_Bean.create(Unknown Source)
	at io.quarkus.amazon.secretsmanager.runtime.SecretsManagerClientProducer_ProducerMethod_client_o2u827DJgDuZ1zCfbjV6TqIsxgk_Bean.create(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:119)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:38)
	at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.c13(Unknown Source)
	at io.quarkus.arc.generator.Default_jakarta_enterprise_context_ApplicationScoped_ContextInstances.computeIfAbsent(Unknown Source)
	at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:35)
	at io.quarkus.arc.impl.ClientProxies.getApplicationScopedDelegate(ClientProxies.java:21)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientProducer_ProducerMethod_client_o2u827DJgDuZ1zCfbjV6TqIsxgk_ClientProxy.arc$delegate(Unknown Source)
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientProducer_ProducerMethod_client_o2u827DJgDuZ1zCfbjV6TqIsxgk_ClientProxy.getSecretValue(Unknown Source)
	at org.acme.SecretsManagerCredentialsProvider.getCredentials(SecretsManagerCredentialsProvider.java:45)
	at org.acme.SecretsManagerCredentialsProvider_ClientProxy.getCredentials(Unknown Source)
	at io.quarkus.oidc.common.runtime.OidcCommonUtils$1.get(OidcCommonUtils.java:317)
	at io.quarkus.oidc.common.runtime.OidcCommonUtils$1.get(OidcCommonUtils.java:309)
	at [email protected]/java.util.Optional.orElseGet(Optional.java:364)
	at io.quarkus.oidc.common.runtime.OidcCommonUtils.clientSecret(OidcCommonUtils.java:297)
	at io.quarkus.oidc.common.runtime.OidcCommonUtils.initClientSecretBasicAuth(OidcCommonUtils.java:421)
	at io.quarkus.oidc.runtime.OidcProviderClient.<init>(OidcProviderClient.java:66)
	at io.quarkus.oidc.runtime.OidcRecorder$11.apply(OidcRecorder.java:556)
	at io.quarkus.oidc.runtime.OidcRecorder$11.apply(OidcRecorder.java:523)
	at io.smallrye.context.impl.wrappers.SlowContextualBiFunction.apply(SlowContextualBiFunction.java:21)
	at io.smallrye.mutiny.operators.uni.UniOnItemOrFailureFlatMap$UniOnItemOrFailureFlatMapProcessor.performInnerSubscription(UniOnItemOrFailureFlatMap.java:86)
	... 26 more
Caused by: jakarta.enterprise.inject.CreationException: Synthetic bean instance for software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder not initialized yet: software_amazon_awssdk_services_secretsmanager_SecretsManagerClientBuilder_35edabeaf18440581dc996704efe9686730c9848
	- a synthetic bean initialized during RUNTIME_INIT must not be accessed during STATIC_INIT
	- RUNTIME_INIT build steps that require access to synthetic beans initialized during RUNTIME_INIT should consume the SyntheticBeansRuntimeInitBuildItem
	at software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder_oRj83jIPijUzp7JkawwrGWJy-G8_Synthetic_Bean.createSynthetic(Unknown Source)
	... 75 more

Expected behavior

CredentialsProvider implementations can leverage beans produced by other Quarkus extensions, especially the SecretsManagerClient, which is a pretty ideal bean to use in a CredentialsProvider.

It might be possible to resolve this issue using the SyntheticBeansRuntimeInitBuildItem, but I think in practice the config validation that happens during this build step should not resolve the value from the CredentialsProvider in the scope of a build step (even a RUNTIME_INIT one). More similar to how gsmet added support for the CredentialsProvider interface in the Quarkiverse Quarkus GitHub App Extension.

Actual behavior

The application fails to initialize.

How to Reproduce?

  1. Clone this reproducer repository: https://github.com/ryandens/quarkus-oidc-secrets-manager
  2. Run the build: ./gradlew build or run dev mode ./gradlew quarkusDev
  3. Observe the failure: https://scans.gradle.com/s/5vyysmt4kfphi/tests/task/:test/details/org.acme.GreetingResourceTest/testHelloEndpoint()?expanded-stacktrace=WyIwLTEtMiIsIjAtMSIsIjAtMS0yLTMiLCIwLTEtMi00IiwiMC0xLTItMy01IiwiMC0xLTItNC02Il0&top-execution=1

Output of uname -a or ver

Darwin 23.4.0 Darwin Kernel Version 23.4.0: Fri Mar 15 00:12:49 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6020 arm64

Output of java -version

OpenJDK Runtime Environment Temurin-21.0.3+9 (build 21.0.3+9-LTS)

Quarkus version or git rev

3.11.2

Build tool (ie. output of mvnw --version or gradlew --version)

Gradle 8.7

Additional information

https://scans.gradle.com/s/5vyysmt4kfphi

If anyone else encounters this issue and is interested in a workaround, you can simply not use the SecretsManagerClient provided by the Quarkus Amazon Services extension and write a CredentialsProvider implementation like this:

    @Inject
    public SecretsManagerCredentialsProvider(final ObjectMapper mapper) {
        this.mapper = mapper;
        this.secrets = SecretsManagerClient.create();
    }

However, you then lose the ability to easily test these resources with @QuarkusTest and localstack without having to manually configure the SecretsManagerClient to point to localstack

ryandens avatar Jun 15 '24 00:06 ryandens