diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index 12cc7b961b0dc..577ca17069967 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -1297,7 +1297,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import io.quarkus.oidc.AccessTokenCredential; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.ConsumeEvent; @@ -1306,7 +1306,7 @@ import io.smallrye.common.annotation.Blocking; @ApplicationScoped public class OrderService { - @TenantFeature("tenantId") + @Tenant("tenantId") @Inject TenantIdentityProvider identityProvider; @@ -1323,14 +1323,14 @@ public class OrderService { } ---- -<1> For the default tenant, the `TenantFeature` qualifier is optional. +<1> For the default tenant, the `Tenant` qualifier is optional. <2> Executes token verification and converts the token to a `SecurityIdentity`. [NOTE] ==== When the provider is used during an HTTP request, the tenant configuration can be resolved as described in the xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide. -However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.TenantFeature` qualifier. +However, when there is no active HTTP request, you must select the tenant explicitly with the `io.quarkus.oidc.Tenant` qualifier. ==== [WARNING] diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index a7e4e41dfbd2f..08dd65840379e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -2,10 +2,12 @@ import static io.quarkus.arc.processor.BuiltinScope.APPLICATION; import static io.quarkus.arc.processor.DotNames.DEFAULT; +import static io.quarkus.arc.processor.DotNames.NAMED; import static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME; import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE; import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID; import static org.jboss.jandex.AnnotationTarget.Kind.CLASS; +import static org.jboss.jandex.AnnotationTarget.Kind.METHOD; import java.util.List; import java.util.Map; @@ -16,16 +18,23 @@ import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; +import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem; import io.quarkus.arc.deployment.QualifierRegistrarBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; +import io.quarkus.arc.processor.InjectionPointsTransformer; import io.quarkus.arc.processor.QualifierRegistrar; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -151,6 +160,7 @@ ExtensionSslNativeSupportBuildItem enableSslInNative() { QualifierRegistrarBuildItem addQualifiers() { // this seems to be necessary; I think it's because sometimes we only access beans // annotated with @TenantFeature programmatically and no injection point is annotated with it + // TODO: drop @TenantFeature qualifier when 'TenantFeatureFinder' stop using this annotation as a qualifier return new QualifierRegistrarBuildItem(new QualifierRegistrar() { @Override public Map> getAdditionalQualifiers() { @@ -159,52 +169,94 @@ public Map> getAdditionalQualifiers() { }); } + @BuildStep + InjectionPointTransformerBuildItem makeTenantIdentityProviderInjectionPointsNamed() { + // @Tenant annotation cannot be a qualifier as it is used on resource methods and lead to illegal states + return new InjectionPointTransformerBuildItem(new InjectionPointsTransformer() { + @Override + public boolean appliesTo(Type requiredType) { + return requiredType.name().equals(TENANT_IDENTITY_PROVIDER_NAME); + } + + @Override + public void transform(TransformationContext ctx) { + if (ctx.getTarget().kind() == METHOD) { + ctx + .getAllAnnotations() + .stream() + .filter(a -> TENANT_NAME.equals(a.name())) + .forEach(a -> { + var annotationValue = new AnnotationValue[] { + AnnotationValue.createStringValue("value", a.value().asString()) }; + ctx + .transform() + .add(AnnotationInstance.create(NAMED, a.target(), annotationValue)) + .done(); + }); + } else { + // field + var tenantAnnotation = Annotations.find(ctx.getAllAnnotations(), TENANT_NAME); + if (tenantAnnotation != null && tenantAnnotation.value() != null) { + ctx + .transform() + .add(NAMED, AnnotationValue.createStringValue("value", tenantAnnotation.value().asString())) + .done(); + } + } + } + }); + } + /** - * Produce {@link OidcIdentityProvider} with already selected tenant for each {@link OidcIdentityProvider} - * injection point annotated with {@link TenantFeature} annotation. - * For example, we produce {@link OidcIdentityProvider} with pre-selected tenant 'my-tenant' for injection point: + * Produce {@link TenantIdentityProvider} with already selected tenant for each {@link TenantIdentityProvider} + * injection point annotated with {@link Tenant} annotation. + * For example, we produce {@link TenantIdentityProvider} with pre-selected tenant 'my-tenant' for injection point: * * * @Inject - * @TenantFeature("my-tenant") - * OidcIdentityProvider identityProvider; + * @Tenant("my-tenant") + * TenantIdentityProvider identityProvider; * */ @Record(ExecutionTime.STATIC_INIT) @BuildStep void produceTenantIdentityProviders(BuildProducer syntheticBeanProducer, OidcRecorder recorder, BeanDiscoveryFinishedBuildItem beans, CombinedIndexBuildItem combinedIndex) { - // create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant") - if (!combinedIndex.getIndex().getAnnotations(TENANT_FEATURE_NAME).isEmpty()) { - // create TenantIdentityProviders for tenants selected with @TenantFeature like: @TenantFeature("my-tenant") + if (!combinedIndex.getIndex().getAnnotations(TENANT_NAME).isEmpty()) { + // create TenantIdentityProviders for tenants selected with @Tenant like: @Tenant("my-tenant") beans .getInjectionPoints() .stream() - .filter(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME) != null) .filter(OidcBuildStep::isTenantIdentityProviderType) - .map(ip -> ip.getRequiredQualifier(TENANT_FEATURE_NAME).value().asString()) + .filter(ip -> ip.getRequiredQualifier(NAMED) != null) + .map(ip -> ip.getRequiredQualifier(NAMED).value().asString()) .distinct() .forEach(tenantName -> syntheticBeanProducer.produce( SyntheticBeanBuildItem .configure(TenantIdentityProvider.class) - .addQualifier().annotation(TENANT_FEATURE_NAME).addValue("value", tenantName).done() + .named(tenantName) .scope(APPLICATION.getInfo()) .supplier(recorder.createTenantIdentityProvider(tenantName)) .unremovable() .done())); } - // create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @TenantFeature + // create TenantIdentityProvider for default tenant when tenant is not explicitly selected via @Tenant boolean createTenantIdentityProviderForDefaultTenant = beans .getInjectionPoints() .stream() - .filter(InjectionPointInfo::hasDefaultedQualifier) + .filter(ip -> ip.getRequiredQualifier(NAMED) == null) .anyMatch(OidcBuildStep::isTenantIdentityProviderType); if (createTenantIdentityProviderForDefaultTenant) { syntheticBeanProducer.produce( SyntheticBeanBuildItem .configure(TenantIdentityProvider.class) - .addQualifier(DEFAULT) .scope(APPLICATION.getInfo()) + .addQualifier(DEFAULT) + // named beans are implicitly default according to the specs + // when no other qualifiers are present other than @Named and @Any + // which means we need to handle ambiguous resolution + .alternative(true) + .priority(1) .supplier(recorder.createTenantIdentityProvider(DEFAULT_TENANT_ID)) .unremovable() .done()); @@ -243,8 +295,17 @@ public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRec BuildProducer systemPropertyProducer) { if (!buildTimeConfig.auth.proactive && (capabilities.isPresent(Capability.RESTEASY_REACTIVE) || capabilities.isPresent(Capability.RESTEASY))) { - var annotationInstances = combinedIndexBuildItem.getIndex().getAnnotations(TENANT_NAME); - if (!annotationInstances.isEmpty()) { + boolean foundTenantResolver = combinedIndexBuildItem + .getIndex() + .getAnnotations(TENANT_NAME) + .stream() + .map(AnnotationInstance::target) + // ignored field injection points and injection setters + // as we don't want to count in the TenantIdentityProvider injection point + .filter(t -> t.kind() == METHOD) + .map(AnnotationTarget::asMethod) + .anyMatch(m -> !m.isConstructor() && !m.hasAnnotation(DotNames.INJECT)); + if (foundTenantResolver) { // register method interceptor that will be run before security checks bindingProducer.produce( new EagerSecurityInterceptorBindingBuildItem(recorder.tenantResolverInterceptorCreator(), TENANT_NAME)); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java index e882dfc299864..97cbab1568177 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Tenant.java @@ -1,6 +1,8 @@ package io.quarkus.oidc; +import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Retention; @@ -9,8 +11,10 @@ /** * Annotation which can be used to associate OIDC tenant configurations with Jakarta REST resources and resource methods. + * When placed on injection points, this annotation can be used to select a tenant associated + * with the {@link TenantIdentityProvider}. */ -@Target({ TYPE, METHOD }) +@Target({ TYPE, METHOD, FIELD, PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface Tenant { /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java index fd37e50e8c4a8..377a4a7185564 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantIdentityProvider.java @@ -5,7 +5,7 @@ /** * Tenant-specific {@link SecurityIdentity} provider. Associated tenant configuration needs to be selected - * with the {@link TenantFeature} qualifier. When injection point is not annotated with the {@link TenantFeature} + * with the {@link Tenant} qualifier. When injection point is not annotated with the {@link Tenant} * qualifier, default tenant is selected. */ public interface TenantIdentityProvider { diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java index 36d98d57b01d9..95c6a31e7443d 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OrderService.java @@ -7,7 +7,7 @@ import jakarta.inject.Inject; import io.quarkus.oidc.AccessTokenCredential; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.ConsumeEvent; @@ -21,7 +21,7 @@ public class OrderService { @Inject SecurityIdentity identity; - @TenantFeature("bearer") + @Tenant("bearer") @Inject TenantIdentityProvider identityProvider; diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java index 911c4e55291a3..38ddd5b7e75ec 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/StartupService.java @@ -9,11 +9,12 @@ import java.util.concurrent.ConcurrentHashMap; import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OIDCException; -import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantIdentityProvider; import io.quarkus.runtime.StartupEvent; import io.quarkus.security.AuthenticationFailedException; @@ -27,16 +28,18 @@ public class StartupService { private static final String ISSUER = "https://server.example.com"; - @TenantFeature("bearer") + @Inject + @Tenant("bearer") TenantIdentityProvider identityProviderBearer; - @TenantFeature("bearer-role-claim-path") + @Inject + @Tenant("bearer-role-claim-path") TenantIdentityProvider identityProviderBearerRoleClaimPath; private final Map>> tenantToIdentityWithRole = new ConcurrentHashMap<>(); void onStartup(@Observes StartupEvent event, - @TenantFeature(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider, + @Tenant(DEFAULT_TENANT_ID) TenantIdentityProvider defaultTenantProvider, TenantIdentityProvider defaultTenantProviderDefaultQualifier) { assertDefaultTenantProviderInjection(defaultTenantProvider); assertDefaultTenantProviderInjection(defaultTenantProviderDefaultQualifier);