Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quarkus OIDC CredentialsProvider integration resolves secrets during BuildStep #41223

Closed
ryandens opened this issue Jun 15, 2024 · 6 comments · Fixed by #41571
Closed

Quarkus OIDC CredentialsProvider integration resolves secrets during BuildStep #41223

ryandens opened this issue Jun 15, 2024 · 6 comments · Fixed by #41571
Assignees
Labels
area/oidc kind/bug Something isn't working
Milestone

Comments

@ryandens
Copy link
Contributor

ryandens commented Jun 15, 2024

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 java.base@21.0.3/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 ryandens added the kind/bug Something isn't working label Jun 15, 2024
Copy link

quarkus-bot bot commented Jun 15, 2024

/cc @pedroigor (oidc), @sberyozkin (oidc)

@ryandens
Copy link
Contributor Author

Adding some more context here, I tried out a local build of quarkus that added the @Consume(SyntheticBeansRuntimeInitBuildItem.class) to the BuildStep that causes this error when using a synthetic bean in the CredentialsProvider used by the OIDC extension to see if that would be a quick way to resolve this/support this use case. It didn't work, failing with io.quarkus.builder.ChainBuildException: Cycle detected:. The build scan is available here: https://scans.gradle.com/s/gdodpfqklxwis/tests/task/:test/details/org.acme.GreetingResourceTest/testHelloEndpoint()?top-execution=1

I think this means that in order for the Quarkus OIDC extension to support CredentialsProviders that use beans like the SecretsManagerClient provided by the Quarkus Amazon Services extension, we would need to move the resolution of the secret from a BuildStep to the runtime itself (not RUNTIME_INIT)

@ryandens
Copy link
Contributor Author

I was able to demonstrate how one might workaround this issue by deferring secret access to outside of the build step.I think this deferral is ideal for long-running applications that might have short-lived credentials accessed via a CredentialsProvider, though I'm not sure if this is the spirit of that abstraction.

Basically, finding usages of OidcCommonUtils.clientSecret in the scope of a constructor and make sure it isn't resolved until outside of a BuildStep using the Jakarta inject Provider API - this probably isn't ideal/consistent with the rest of Quarkus, but it demonstrates how one could solve this issue (perhaps using patterns that are more consistent with Quarkus).

I was able to confirm that a local build of my branch linked above will pass the initialization test of the reproducer repository https://github.com/ryandens/quarkus-oidc-secrets-manager by using the maven local quarkus release 999-SNAPSHOT.

@sberyozkin, do you think a change that looks something like this might be one you'd be willing to accept? If not, do you have suggestions for users who want to have a Quarkus CredentialsProvider for OIDC that uses other injected beans?

@michalvavrik michalvavrik self-assigned this Jun 30, 2024
@michalvavrik
Copy link
Member

michalvavrik commented Jun 30, 2024

I am going to have a look. I cannot tell whether main...ryandens:quarkus:ryandens/41223/oidc-client-secret is a good solution without trying to figure it out myself. Sure, it might end-up same + tests. Let's see.

@sberyozkin
Copy link
Member

Thanks @michalvavrik, @ryandens

@ryandens
Copy link
Contributor Author

Thanks for investigating and fixing this! I definitely agree the solution merged makes a lot more sense than the hack/workaround provided by me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/oidc kind/bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants