quarkus
quarkus copied to clipboard
Quarkus OIDC CredentialsProvider integration resolves secrets during BuildStep
Describe the bug
The Quarkus OIDC Extension appears to deviate from norms that I've observed in other extensions that have support for CredentialsProvider
s 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?
- Clone this reproducer repository: https://github.com/ryandens/quarkus-oidc-secrets-manager
- Run the build:
./gradlew build
or run dev mode./gradlew quarkusDev
- 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