io.helidon.microprofile.security.JerseySecurityFeature Maven / Gradle / Ivy
/*
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.helidon.microprofile.security;
import java.util.LinkedList;
import java.util.List;
import io.helidon.common.config.Config;
import io.helidon.security.Security;
import io.helidon.security.SecurityContext;
import io.helidon.security.annotations.Authorized;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
import jakarta.ws.rs.ConstrainedTo;
import jakarta.ws.rs.RuntimeType;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.FeatureContext;
import jakarta.ws.rs.core.GenericType;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.internal.inject.ReferencingFactory;
import org.glassfish.jersey.internal.util.collection.Ref;
import org.glassfish.jersey.process.internal.RequestScoped;
import static java.lang.System.Logger.Level.TRACE;
/**
* Integration of Security module with Jersey.
*
* Register this as you would any other feature, e.g.:
*
* ResourceConfig resourceConfig = new ResourceConfig();
* // register JAX-RS resource
* resourceConfig.register(MyResource.class);
* // integrate security
* resourceConfig.register(new SecurityFeature(buildSecurity()));
*
*/
@ConstrainedTo(RuntimeType.SERVER)
public final class JerseySecurityFeature implements Feature {
private static final System.Logger LOGGER = System.getLogger(JerseySecurityFeature.class.getName());
private final Security security;
private final FeatureConfig featureConfig;
/**
* Create a new instance of security feature for a security component.
*
*
* This constructor is workaround solution for Jersey instantiation problem.
*/
public JerseySecurityFeature() {
this.security = null;
this.featureConfig = null;
}
/**
* Create a new instance of security feature for a security component.
*
* @param security Fully configured security component to integrate with Jersey
*/
public JerseySecurityFeature(Security security) {
this.security = security;
this.featureConfig = new FeatureConfig(builder(security).config(security.configFor("jersey")));
}
private JerseySecurityFeature(Builder builder) {
this.security = builder.security;
this.featureConfig = new FeatureConfig(builder);
}
/**
* Builder for {@link JerseySecurityFeature}.
*
* @param security Security instance to create this feature for (cannot build a feature without security instance)
* @return Builder to configure feature
*/
public static Builder builder(Security security) {
return new Builder(security);
}
@Override
public boolean configure(FeatureContext context) {
RuntimeType runtimeType = context.getConfiguration().getRuntimeType();
//register server
if (runtimeType != RuntimeType.SERVER) {
return false;
}
if (LOGGER.isLoggable(TRACE)) {
LOGGER.log(TRACE, "Configuring Security feature in Jersey server runtime");
}
context.register(SecurityPreMatchingFilter.class);
context.register(SecurityFilter.class);
//allow injection of security context (our, not Jersey)
context.register(new AbstractBinder() {
@Override
protected void configure() {
bindFactory(SecurityContextRefFactory.class)
.to(SecurityContext.class)
.proxy(true)
.proxyForSameScope(false)
.in(RequestScoped.class);
bindFactory(ReferencingFactory.referenceFactory())
.to(new GenericType>() { })
.in(RequestScoped.class);
bind(security).to(Security.class);
bind(featureConfig).to(FeatureConfig.class);
}
});
return true;
}
FeatureConfig featureConfig() {
return featureConfig;
}
/**
* {@link JerseySecurityFeature} fluent API builder.
*/
@SuppressWarnings("UnusedReturnValue")
public static final class Builder implements io.helidon.common.Builder {
private final Security security;
private final List queryParamHandlers = new LinkedList<>();
private boolean authorizeAnnotatedOnly = FeatureConfig.DEFAULT_ATZ_ANNOTATED_ONLY;
private boolean authenticateAnnotatedOnly = FeatureConfig.DEFAULT_ATN_ANNOTATED_ONLY;
private boolean debug = FeatureConfig.DEFAULT_DEBUG;
private boolean prematchingAuthorization = FeatureConfig.DEFAULT_PREMATCHING_ATZ;
private boolean prematchingAuthentication = FeatureConfig.DEFAULT_PREMATCHING_ATN;
private boolean useAbortWith = FeatureConfig.DEFAULT_USE_ABORT_WITH;
private boolean failOnFailureIfOptional = FeatureConfig.DEFAULT_ATN_FAIL_ON_FAILURE_IF_OPT;
private Builder(Security security) {
this.security = security;
}
/**
* Whether to authorize only annotated methods (with {@link Authorized} annotation) or all.
* When using {@link #usePrematchingAuthorization(boolean)}
* this method is ignored.
*
* @param authzOnly if set to true, authorization will be performed on annotated methods only, defaults to false
* @return updated builder instance
*/
public Builder authorizeAnnotatedOnly(boolean authzOnly) {
this.authorizeAnnotatedOnly = authzOnly;
return this;
}
/**
* Whether to authorize only annotated methods (with {@link io.helidon.security.annotations.Authenticated} annotation or all.
* When using {@link #usePrematchingAuthentication(boolean)} this method is ignored.
* By default only annotated methods (annotation may be also on Application class or resource class) are authenticated.
*
* @param authnOnly if set to false, authentication will be performed for all requests, defaults to true
* @return updated builder instance
*/
public Builder authenticateAnnotatedOnly(boolean authnOnly) {
this.authenticateAnnotatedOnly = authnOnly;
return this;
}
/**
* Add a new handler to extract query parameter and store it in security request header.
*
* @param handler handler to extract data
* @return updated builder instance
*/
public Builder addQueryParamHandler(QueryParamHandler handler) {
this.queryParamHandlers.add(handler);
return this;
}
/**
* Add handlers to extract query parameters and store them in security request header.
*
* @param handlers handlers to extract data
* @return updated builder instance
*/
public Builder addQueryParamHandlers(Iterable handlers) {
handlers.forEach(this::addQueryParamHandler);
return this;
}
/**
* Configure whether pre-matching or post-matching filter is used to authenticate requests.
* Defaults to post-matching, as we have access to information about resource class and method that is
* invoked, allowing us to use annotations defined on these.
* When switched to prematching, the security is an on/off switch - all resources are protected the
* same way.
*
* @param usePrematching whether to use pre-matching filter instead of post-matching
* @return updated builder instance
*/
public Builder usePrematchingAuthentication(boolean usePrematching) {
this.prematchingAuthentication = usePrematching;
return this;
}
/**
* Configure whether pre-matching or post-matching filter is used to authorize requests.
* Defaults to post-matching, as we have access to information about resource class and method that is
* invoked, allowing us to use annotations defined on these.
* When switched to prematching, the security is an on/off switch - all resources are protected the
* same way.
*
*
* When set to true, authentication will be prematching as well.
*
* @param usePrematching whether to use pre-matching filter instead of post-matching
* @return updated builder instance
*/
public Builder usePrematchingAuthorization(boolean usePrematching) {
this.prematchingAuthorization = usePrematching;
return this;
}
/**
* Whether to fail in case of authentication failure if authentication is optional.
* When set to {@code true}, authentication will fail in case of failure even when it is set
* as an optional.
*
*
* Default value is {@code false}.
*
* @param failOnFailureIfOptional fail on authentication failure if optional
* @return updated builder instance
*/
private Builder failOnFailureIfOptional(Boolean failOnFailureIfOptional) {
this.failOnFailureIfOptional = failOnFailureIfOptional;
return this;
}
/**
* Set debugging on.
* Will return description from response in entity.
*
* @return updated builder instance
*/
public Builder debug() {
this.debug = true;
return this;
}
/**
* When set to {@code true} (which is the default behavior, the security filter
* would use {@link org.glassfish.jersey.server.ContainerRequest#abortWith(jakarta.ws.rs.core.Response)} to
* abort request and configure a security response.
*
* When set to {@code false}, the security filter would throw an {@link jakarta.ws.rs.WebApplicationException} instead.
* Such an exception can be handled by a custom error handler.
*
* @param useAbortWith set to {@code false} to use exceptions, by default uses abortWith on request
* @return updated builder instance
*/
public Builder useAbortWith(boolean useAbortWith) {
this.useAbortWith = useAbortWith;
return this;
}
/**
* Update this builder from configuration.
* Expects:
*
* - authorize-annotated-only: see {@link #authorizeAnnotatedOnly(boolean)}
* - query-params: see {@link #addQueryParamHandler(QueryParamHandler)}
*
* Example:
*
* security:
* jersey:
* defaults:
* # If set to true, only annotated (@Authenticated) resources will be authorized
* # By default, every request is sent to authorization provider
* authorize-annotated-only: false
* # query parameters will be extracted from request
* # and sent to authentication and authorization providers
* # as headers. These will NOT be available to application
* # as headers.
* query-params:
* - name: "basicAuth"
* header: "Authorization"
*
*
* @param config configuration set to key "jersey" (see example above)
* @return updated builder instance
*/
public Builder config(Config config) {
config.get("prematching-authentication").asBoolean().ifPresent(this::usePrematchingAuthentication);
config.get("prematching-authorization").asBoolean().ifPresent(this::usePrematchingAuthorization);
config.get("use-abort-with").asBoolean().ifPresent(this::useAbortWith);
config.get("fail-on-failure-if-optional").asBoolean().ifPresent(this::failOnFailureIfOptional);
Config myConfig = config.get("defaults");
myConfig.get("authorize-annotated-only").asBoolean().ifPresent(this::authorizeAnnotatedOnly);
myConfig.get("authenticate-annotated-only").asBoolean().ifPresent(this::authenticateAnnotatedOnly);
myConfig.get("query-params").mapList(QueryParamHandler::create).ifPresent(this::addQueryParamHandlers);
myConfig.get("debug").asBoolean().filter(bool -> bool).ifPresent(bool -> this.debug());
return this;
}
/**
* Build this configuration into an instance.
*
* @return feature to register with Jersey
*/
@Override
public JerseySecurityFeature build() {
return new JerseySecurityFeature(this);
}
Security security() {
return security;
}
List queryParamHandlers() {
return queryParamHandlers;
}
boolean isAuthorizeAnnotatedOnly() {
return authorizeAnnotatedOnly;
}
boolean isAuthenticateAnnotatedOnly() {
return authenticateAnnotatedOnly;
}
boolean isDebug() {
return debug;
}
boolean isPrematchingAuthorization() {
return prematchingAuthorization;
}
boolean isPrematchingAuthentication() {
return prematchingAuthentication;
}
boolean useAbortWith() {
return useAbortWith;
}
boolean failOnFailureIfOptional() {
return failOnFailureIfOptional;
}
}
private static class SecurityContextRefFactory extends ReferencingFactory {
@Inject
SecurityContextRefFactory(Provider> referenceFactory) {
super(referenceFactory);
}
}
}