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

Select OIDC TenantIdentityProvider with the @Tenant annotation instead of the @TenantFeature #40843

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1297,7 +1297,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;
Expand All @@ -1306,7 +1306,7 @@
@ApplicationScoped
public class OrderService {

@TenantFeature("tenantId")
@Tenant("tenantId")
@Inject
TenantIdentityProvider identityProvider;

Expand All @@ -1323,14 +1323,14 @@

}
----
<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

Check warning on line 1331 in docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc", "range": {"start": {"line": 1331, "column": 92}}}, "severity": "INFO"}
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<DotName, Set<String>> getAdditionalQualifiers() {
Expand All @@ -159,52 +169,94 @@ public Map<DotName, Set<String>> 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:
*
* <code>
* &#064;Inject
* &#064;TenantFeature("my-tenant")
* OidcIdentityProvider identityProvider;
* &#064;Tenant("my-tenant")
* TenantIdentityProvider identityProvider;
* </code>
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void produceTenantIdentityProviders(BuildProducer<SyntheticBeanBuildItem> 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());
Expand Down Expand Up @@ -243,8 +295,17 @@ public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRec
BuildProducer<SystemPropertyBuildItem> 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)
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
.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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,7 +21,7 @@ public class OrderService {
@Inject
SecurityIdentity identity;

@TenantFeature("bearer")
@Tenant("bearer")
@Inject
TenantIdentityProvider identityProvider;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, Map<String, Set<String>>> 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);
Expand Down