cleanup and implement EJB integration
Project: http://git-wip-us.apache.org/repos/asf/tomee/repo Commit: http://git-wip-us.apache.org/repos/asf/tomee/commit/893525f8 Tree: http://git-wip-us.apache.org/repos/asf/tomee/tree/893525f8 Diff: http://git-wip-us.apache.org/repos/asf/tomee/diff/893525f8 Branch: refs/heads/master Commit: 893525f860c485b5c5d0a79cb1621de3f56e8013 Parents: 7b0c434 Author: Jean-Louis Monteiro <jeano...@gmail.com> Authored: Tue Mar 6 10:13:04 2018 +0100 Committer: Jean-Louis Monteiro <jeano...@gmail.com> Committed: Tue Mar 6 10:13:04 2018 +0100 ---------------------------------------------------------------------- .../core/security/AbstractSecurityService.java | 9 +- tck/mp-jwt-embedded/pom.xml | 14 +- .../tomee/microprofile/jwt/MPJWTContext.java | 129 ----------- .../tomee/microprofile/jwt/MPJWTFilter.java | 230 +++++++++++++++---- .../microprofile/jwt/MPJWTInitializer.java | 15 +- .../tomee/microprofile/jwt/cdi/ClaimBean.java | 46 ++-- .../microprofile/jwt/cdi/ClaimValueWrapper.java | 10 +- .../microprofile/jwt/cdi/JsonbProducer.java | 1 + .../microprofile/jwt/cdi/MPJWTProducer.java | 28 ++- .../jwt/jaxrs/MPJWPProviderRegistration.java | 46 ++++ .../MPJWTSecurityAnnotationsInterceptor.java | 54 +++++ ...TSecurityAnnotationsInterceptorsFeature.java | 144 ++++++++++++ .../META-INF/org.apache.openejb.extension | 1 + tck/mp-jwt-embedded/src/test/resources/dev.xml | 13 +- .../tomee/catalina/TomcatSecurityService.java | 20 ++ 15 files changed, 532 insertions(+), 228 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/container/openejb-core/src/main/java/org/apache/openejb/core/security/AbstractSecurityService.java ---------------------------------------------------------------------- diff --git a/container/openejb-core/src/main/java/org/apache/openejb/core/security/AbstractSecurityService.java b/container/openejb-core/src/main/java/org/apache/openejb/core/security/AbstractSecurityService.java index 57e2c9c..514847b 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/core/security/AbstractSecurityService.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/core/security/AbstractSecurityService.java @@ -53,6 +53,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; /** @@ -153,13 +154,13 @@ public abstract class AbstractSecurityService implements DestroyableResource, Se final ProvidedSecurityContext providedSecurityContext = newContext.get(ProvidedSecurityContext.class); SecurityContext securityContext = oldContext != null ? oldContext.get(SecurityContext.class) : - (providedSecurityContext != null ? providedSecurityContext.context : null); + (providedSecurityContext != null ? providedSecurityContext.context : null); if (providedSecurityContext == null && (securityContext == null || securityContext == defaultContext)) { final Identity identity = clientIdentity.get(); if (identity != null) { securityContext = new SecurityContext(identity.subject); } else { - securityContext = defaultContext; + securityContext = getDefaultContext(); } } @@ -398,6 +399,10 @@ public abstract class AbstractSecurityService implements DestroyableResource, Se } } + protected SecurityContext getDefaultContext() { + return defaultContext; + } + public static final class ProvidedSecurityContext { public final SecurityContext context; http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/pom.xml ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/pom.xml b/tck/mp-jwt-embedded/pom.xml index 5055ac4..2af8f28 100644 --- a/tck/mp-jwt-embedded/pom.xml +++ b/tck/mp-jwt-embedded/pom.xml @@ -40,7 +40,13 @@ <groupId>${project.groupId}</groupId> <artifactId>openejb-core</artifactId> <version>${project.version}</version> - <scope>test</scope> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>openejb-cxf-rs</artifactId> + <version>${project.version}</version> + <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> @@ -112,6 +118,12 @@ <artifactId>johnzon-jsonb</artifactId> <version>1.1.2</version> </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-catalina</artifactId> + <version>${tomcat.version}</version> + <scope>provided</scope> + </dependency> </dependencies> <build> http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTContext.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTContext.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTContext.java deleted file mode 100644 index 50b7d1e..0000000 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTContext.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.tomee.microprofile.jwt; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Predicate; - -/** - * Responsible for holding the runtime model - */ -public class MPJWTContext { - - private static final ConcurrentMap<MPJWTConfigKey, MPJWTConfigValue> configuration = new ConcurrentHashMap<>(); - - public static MPJWTConfigValue addMapping(final MPJWTConfigKey key, final MPJWTConfigValue value) { - Objects.requireNonNull(key, "MP JWT Key is required"); - Objects.requireNonNull(value, "MP JWT Value is required"); - - final MPJWTConfigValue oldValue = configuration.putIfAbsent(key, value); - if (oldValue != null) { - throw new IllegalStateException("A mapping has already been defined for the key " + key); - } - - return value; - } - - public static Optional<MPJWTConfigValue> get(final MPJWTConfigKey key) { - Objects.requireNonNull(key, "MP JWT Key is required to retrieve the configuration"); - return Optional.ofNullable(configuration.get(key)); - } - - public static Optional<Map.Entry<MPJWTConfigKey, MPJWTConfigValue>> findFirst(final String path) { - return configuration.entrySet() - .stream() - .filter(new Predicate<ConcurrentMap.Entry<MPJWTConfigKey, MPJWTConfigValue>>() { - @Override - public boolean test(final ConcurrentMap.Entry<MPJWTConfigKey, MPJWTConfigValue> e) { - return path.startsWith(e.getKey().toURI()); - } - }) - .findFirst(); - } - - - public static class MPJWTConfigValue { - private final String authMethod; - private final String realm; - - public MPJWTConfigValue(final String authMethod, final String realm) { - this.authMethod = authMethod; - this.realm = realm; - } - - public String getAuthMethod() { - return authMethod; - } - - public String getRealm() { - return realm; - } - } - - public static class MPJWTConfigKey { - private final String contextPath; - private final String applicationPath; - - public MPJWTConfigKey(final String contextPath, final String applicationPath) { - this.contextPath = contextPath; - this.applicationPath = applicationPath; - } - - public String getApplicationPath() { - return applicationPath; - } - - public String getContextPath() { - return contextPath; - } - - public String toURI() { - return ("/" + contextPath + "/" + applicationPath).replaceAll("//", "/"); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final MPJWTConfigKey that = (MPJWTConfigKey) o; - - if (contextPath != null ? !contextPath.equals(that.contextPath) : that.contextPath != null) return false; - return !(applicationPath != null ? !applicationPath.equals(that.applicationPath) : that.applicationPath != null); - - } - - @Override - public int hashCode() { - int result = contextPath != null ? contextPath.hashCode() : 0; - result = 31 * result + (applicationPath != null ? applicationPath.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "MPJWTConfigKey{" + - "applicationPath='" + applicationPath + '\'' + - ", contextPath='" + contextPath + '\'' + - '}'; - } - } -} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java index 36e53cf..87ab714 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java @@ -16,13 +16,12 @@ */ package org.apache.tomee.microprofile.jwt; -import org.apache.tomee.microprofile.jwt.cdi.MPJWTProducer; import org.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo; -import org.apache.tomee.microprofile.jwt.principal.DefaultJWTCallerPrincipalFactory; import org.apache.tomee.microprofile.jwt.principal.JWTCallerPrincipalFactory; import org.eclipse.microprofile.jwt.JsonWebToken; import javax.inject.Inject; +import javax.security.auth.Subject; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -33,11 +32,18 @@ import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; import java.io.IOException; import java.security.Principal; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Locale; -import java.util.Map; -import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.stream.Collectors; // async is supported because we only need to do work on the way in @WebFilter(asyncSupported = true, urlPatterns = "/*") @@ -56,74 +62,200 @@ public class MPJWTFilter implements Filter { public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { final HttpServletRequest httpServletRequest = (HttpServletRequest) request; - final Optional<Map.Entry<MPJWTContext.MPJWTConfigKey, MPJWTContext.MPJWTConfigValue>> first = - MPJWTContext.findFirst(httpServletRequest.getRequestURI()); - if (!first.isPresent()) { // nothing found in the context - chain.doFilter(request, response); - return; - } + // now wrap the httpServletRequest and override the principal so CXF can propagate into the SecurityContext + try { + chain.doFilter(new MPJWTServletRequestWrapper(httpServletRequest, authContextInfo), response); - // todo get JWT and do validation - // todo not sure what to do with the realm + } catch (final Exception e) { + // this is an alternative to the @Provider bellow which requires registration on the fly + // or users to add it into their webapp for scanning or into the Application itself + if (MPJWTException.class.isInstance(e)) { + final MPJWTException jwtException = MPJWTException.class.cast(e); + HttpServletResponse.class.cast(response).sendError(jwtException.getStatus(), jwtException.getMessage()); + } - final String authorizationHeader = ((HttpServletRequest) request).getHeader("Authorization"); - if (authorizationHeader == null || authorizationHeader.isEmpty()) { - HttpServletResponse.class.cast(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); - return; - } + if (MPJWTException.class.isInstance(e.getCause())) { + final MPJWTException jwtException = MPJWTException.class.cast(e.getCause()); + HttpServletResponse.class.cast(response).sendError(jwtException.getStatus(), jwtException.getMessage()); + } - if (!authorizationHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer ")) { - HttpServletResponse.class.cast(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); - return; } - final String token = authorizationHeader.substring("bearer ".length()); - final JsonWebToken jsonWebToken; - try { - jsonWebToken = validate(token); + } - } catch (final ParseException e) { - // todo is this enough? - HttpServletResponse.class.cast(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); - return; - } + @Override + public void destroy() { - // associate with the producer. Should not be needed. - // todo We should be able to retrieve it based on the HTTP Servlet Request in the producer - MPJWTProducer.setJWTPrincipal(jsonWebToken); + } - // now wrap the httpServletRequest and override the principal so CXF can propagate into the SecurityContext - chain.doFilter(new HttpServletRequestWrapper(httpServletRequest) { + private static Function<HttpServletRequest, JsonWebToken> token(final HttpServletRequest httpServletRequest, final JWTAuthContextInfo authContextInfo) { + + return new Function<HttpServletRequest, JsonWebToken>() { + + private JsonWebToken jsonWebToken; @Override - public Principal getUserPrincipal() { + public JsonWebToken apply(final HttpServletRequest request) { + + // not sure it's worth having synchronization inside a single request + // worth case, we would parse and validate the token twice + if (jsonWebToken != null) { + return jsonWebToken; + } + + final String authorizationHeader = httpServletRequest.getHeader("Authorization"); + if (authorizationHeader == null || authorizationHeader.isEmpty()) { + throw new MissingAuthorizationHeaderException(); + } + + if (!authorizationHeader.toLowerCase(Locale.ENGLISH).startsWith("bearer ")) { + throw new BadAuthorizationPrefixException(authorizationHeader); + } + + final String token = authorizationHeader.substring("bearer ".length()); + try { + jsonWebToken = validate(token, authContextInfo); + + } catch (final ParseException e) { + throw new InvalidTokenException(token, e); + } + return jsonWebToken; - } - @Override - public boolean isUserInRole(String role) { - return jsonWebToken.getGroups().contains(role); } + }; - @Override - public String getAuthType() { - return "MP-JWT"; - } + } + + private static JsonWebToken validate(final String bearerToken, final JWTAuthContextInfo authContextInfo) throws ParseException { + JWTCallerPrincipalFactory factory = JWTCallerPrincipalFactory.instance(); + return factory.parse(bearerToken, authContextInfo); + } + + public static class MPJWTServletRequestWrapper extends HttpServletRequestWrapper { + + private final Function<HttpServletRequest, JsonWebToken> tokenFunction; + private final HttpServletRequest request; + + /** + * Constructs a request object wrapping the given request. + * + * @param request The request to wrap + * @param authContextInfo the context configuration to validate the token + * @throws IllegalArgumentException if the request is null + */ + public MPJWTServletRequestWrapper(final HttpServletRequest request, final JWTAuthContextInfo authContextInfo) { + super(request); + this.request = request; + tokenFunction = token(request, authContextInfo); + + // this is so that the MPJWTProducer can find the function and apply it if necessary + request.setAttribute(JsonWebToken.class.getName(), tokenFunction); + request.setAttribute("javax.security.auth.subject.callable", new Callable<Subject>() { + @Override + public Subject call() throws Exception { + final Set<Principal> principals = new LinkedHashSet<Principal>(); + final JsonWebToken namePrincipal = tokenFunction.apply(request); + principals.add(namePrincipal); + principals.addAll(namePrincipal.getGroups().stream().map(role -> (Principal) () -> role).collect(Collectors.toList())); + return new Subject(true, principals, Collections.emptySet(), Collections.emptySet()); + } + }); + } + + @Override + public Principal getUserPrincipal() { + return tokenFunction.apply(request); + } - }, response); + @Override + public boolean isUserInRole(String role) { + final JsonWebToken jsonWebToken = tokenFunction.apply(request); + return jsonWebToken.getGroups().contains(role); + } + @Override + public String getAuthType() { + return "MP-JWT"; + } } - @Override - public void destroy() { + private static abstract class MPJWTException extends RuntimeException { + + public MPJWTException() { + super(); + } + + public MPJWTException(final Throwable cause) { + super(cause); + } + public abstract int getStatus(); + + public abstract String getMessage(); } - protected JsonWebToken validate(String bearerToken) throws ParseException { - JWTCallerPrincipalFactory factory = JWTCallerPrincipalFactory.instance(); - return factory.parse(bearerToken, authContextInfo); + private static class MissingAuthorizationHeaderException extends MPJWTException { + + @Override + public int getStatus() { + return HttpServletResponse.SC_UNAUTHORIZED; + } + + @Override + public String getMessage() { + return "No authorization header provided. Can't validate the JWT."; + } } + private static class BadAuthorizationPrefixException extends MPJWTException { + + private String authorizationHeader; + + public BadAuthorizationPrefixException(final String authorizationHeader) { + this.authorizationHeader = authorizationHeader; + } + + @Override + public int getStatus() { + return HttpServletResponse.SC_UNAUTHORIZED; + } + + @Override + public String getMessage() { + return "Authorization header does not use the Bearer prefix. Can't validate header " + authorizationHeader; + } + } + + private static class InvalidTokenException extends MPJWTException { + + private final String token; + + public InvalidTokenException(final String token, final Throwable cause) { + super(cause); + this.token = token; + } + + @Override + public int getStatus() { + return HttpServletResponse.SC_UNAUTHORIZED; + } + + @Override + public String getMessage() { + return "Invalid or not parsable JWT " + token; // we might want to break down the exceptions so we can have more messages. + } + } + + @Provider // would be the ideal but not automatically registered + public static class MPJWTExceptionMapper implements ExceptionMapper<MPJWTException> { + + @Override + public Response toResponse(final MPJWTException exception) { + return Response.status(exception.getStatus()).entity(exception.getMessage()).build(); + } + + } } http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java index 28a1735..bf64601 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java @@ -23,7 +23,6 @@ import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.annotation.HandlesTypes; -import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; import java.util.Set; @@ -51,23 +50,11 @@ public class MPJWTInitializer implements ServletContainerInitializer { continue; // do we really want Application? } - final ApplicationPath applicationPath = clazz.getAnnotation(ApplicationPath.class); - final FilterRegistration.Dynamic mpJwtFilter = ctx.addFilter("mp-jwt-filter", MPJWTFilter.class); mpJwtFilter.setAsyncSupported(true); mpJwtFilter.addMappingForUrlPatterns(null, false, "/*"); - MPJWTContext.addMapping( - new MPJWTContext.MPJWTConfigKey( - ctx.getContextPath(), - // todo instead of empty path, we need to look for default value - applicationPath == null ? "" : applicationPath.value()), - - new MPJWTContext.MPJWTConfigValue( - loginConfig.authMethod(), - loginConfig.realmName()) - ); - + break; // no need to add it more than once } } http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java index 0d3488a..513e271 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java @@ -22,8 +22,10 @@ import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import javax.enterprise.context.Dependent; +import javax.enterprise.context.RequestScoped; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Vetoed; import javax.enterprise.inject.spi.Annotated; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; @@ -49,6 +51,7 @@ import java.util.Optional; import java.util.Set; import java.util.logging.Logger; +@Vetoed public class ClaimBean<T> implements Bean<T>, PassivationCapable { private static Logger logger = Logger.getLogger(MPJWTCDIExtension.class.getName()); @@ -62,10 +65,14 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { @Inject private Jsonb jsonb; + @Inject + private JsonWebToken jsonWebToken; + private final BeanManager bm; private final Class rawType; private final Set<Type> types; private final String id; + private final Class<? extends Annotation> scope; public ClaimBean(final BeanManager bm, final Type type) { this.bm = bm; @@ -73,6 +80,7 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { types.add(type); rawType = getRawType(type); this.id = "ClaimBean_" + types; + scope = Dependent.class; } private Class getRawType(final Type type) { @@ -84,8 +92,6 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { return (Class) paramType.getRawType(); } - // todo deal with Optional here? aka check type again - throw new UnsupportedOperationException("Unsupported type " + type); } @@ -122,7 +128,7 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { @Override public Class<? extends Annotation> getScope() { - return Dependent.class; + return scope; } @Override @@ -156,7 +162,6 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { final Claim claim = annotated.getAnnotation(Claim.class); final String key = getClaimKey(claim); - System.out.println(String.format("Found Claim injection with name=%s and for InjectionPoint=%s", key, ip.toString())); logger.finest(String.format("Found Claim injection with name=%s and for %s", key, ip.toString())); if (annotated.getBaseType() instanceof ParameterizedType) { @@ -190,20 +195,28 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { final ClaimValueWrapper claimValueWrapper = new ClaimValueWrapper(key); if (claimValueType instanceof ParameterizedType && isOptional((ParameterizedType) claimValueType)) { - final T claimValue = getClaimValue(key); - claimValueWrapper.setValue(Optional.ofNullable(claimValue)); + claimValueWrapper.setValue(() -> { + final T claimValue = getClaimValue(key); + return Optional.ofNullable(claimValue); + }); } else if (claimValueType instanceof ParameterizedType && isSet((ParameterizedType) claimValueType)) { - final T claimValue = getClaimValue(key); - claimValueWrapper.setValue(claimValue); // todo convert to set + claimValueWrapper.setValue(() -> { + final T claimValue = getClaimValue(key); + return claimValue; + }); } else if (claimValueType instanceof ParameterizedType && isList((ParameterizedType) claimValueType)) { - final T claimValue = getClaimValue(key); - claimValueWrapper.setValue(claimValue); // // todo convert to list + claimValueWrapper.setValue(() -> { + final T claimValue = getClaimValue(key); + return claimValue; + }); } else if (claimValueType instanceof Class) { - final T claimValue = getClaimValue(key); - claimValueWrapper.setValue(claimValue); + claimValueWrapper.setValue(() -> { + final T claimValue = getClaimValue(key); + return claimValue; + }); } else { throw new IllegalArgumentException("Unsupported ClaimValue type " + claimValueType.toString()); @@ -245,13 +258,16 @@ public class ClaimBean<T> implements Bean<T>, PassivationCapable { } private T getClaimValue(final String name) { - final JsonWebToken jwt = MPJWTProducer.getJWTPrincipal(); - if (jwt == null) { + final Bean<?> bean = bm.resolve(bm.getBeans(JsonWebToken.class)); + if (RequestScoped.class.equals(bean.getScope())) { + jsonWebToken = JsonWebToken.class.cast(bm.getReference(bean, JsonWebToken.class, null)); + } + if (jsonWebToken == null || !bean.getScope().equals(RequestScoped.class)) { logger.warning(String.format("Can't retrieve claim %s. No active principal.", name)); return null; } - final Optional<T> claimValue = jwt.claim(name); + final Optional<T> claimValue = jsonWebToken.claim(name); logger.finest(String.format("Found ClaimValue=%s for name=%s", claimValue, name)); return claimValue.orElse(null); } http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java index e0aa68f..a5a4bb5 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java @@ -18,12 +18,14 @@ package org.apache.tomee.microprofile.jwt.cdi; import org.eclipse.microprofile.jwt.ClaimValue; +import java.util.function.Supplier; + public class ClaimValueWrapper<T> implements ClaimValue<T> { private final String name; - private T value; + private Supplier<T> value; - public ClaimValueWrapper(String name) { + public ClaimValueWrapper(final String name) { this.name = name; } @@ -34,10 +36,10 @@ public class ClaimValueWrapper<T> implements ClaimValue<T> { @Override public T getValue() { - return value; + return value.get(); } - public void setValue(T value) { + void setValue(final Supplier<T> value) { this.value = value; } http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java index 297dfb3..a0434ef 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java @@ -25,6 +25,7 @@ import java.util.logging.Level; import java.util.logging.Logger; @ApplicationScoped +// todo add a qualifier here so we isolate our instance from what applications would do public class JsonbProducer { private static Logger log = Logger.getLogger(MPJWTCDIExtension.class.getName()); http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java index 21196a8..453dcff 100644 --- a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java @@ -21,23 +21,29 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Produces; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; +import java.util.function.Function; @ApplicationScoped public class MPJWTProducer { - private static ThreadLocal<JsonWebToken> currentPrincipal = new ThreadLocal<>(); - - public static void setJWTPrincipal(final JsonWebToken principal) { - currentPrincipal.set(principal); - } - - public static JsonWebToken getJWTPrincipal() { - return currentPrincipal.get(); - } + @Inject + private HttpServletRequest httpServletRequest; @Produces @RequestScoped - JsonWebToken currentPrincipal() { - return currentPrincipal.get(); + public JsonWebToken currentPrincipal() { + Objects.requireNonNull(httpServletRequest, "HTTP Servlet Request is required to produce a JSonWebToken principal."); + + // not very beatiful, but avoids having the MPJWTFilter setting the request or the principal in a thread local + // CDI integration already has one - dunno which approach is the best for now + final Object tokenAttribute = httpServletRequest.getAttribute(JsonWebToken.class.getName()); + if (tokenAttribute != null && Function.class.isInstance(tokenAttribute)) { + return (JsonWebToken) Function.class.cast(tokenAttribute).apply(httpServletRequest); + } + + return null; } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java new file mode 100644 index 0000000..29c146e --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomee.microprofile.jwt.jaxrs; + +import org.apache.openejb.observer.Observes; +import org.apache.openejb.server.cxf.rs.event.ExtensionProviderRegistration; +import org.apache.tomee.microprofile.jwt.MPJWTFilter; +import org.eclipse.microprofile.auth.LoginConfig; + +import javax.servlet.FilterRegistration; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.HandlesTypes; +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; +import java.util.Set; + +/** + * OpenEJB/TomEE hack to register a new provider on the fly + * Could be package in tomee only or done in another way + * + * As soon as Roberto is done with the packaging, we can remove all this + */ +public class MPJWPProviderRegistration { + + public void registerProvider(@Observes final ExtensionProviderRegistration event) { // openejb hack to register the provider + event.getProviders().add(new MPJWTFilter.MPJWTExceptionMapper()); + event.getProviders().add(new MPJWTSecurityAnnotationsInterceptorsFeature()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java new file mode 100644 index 0000000..60c2599 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java @@ -0,0 +1,54 @@ +package org.apache.tomee.microprofile.jwt.jaxrs; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +public class MPJWTSecurityAnnotationsInterceptor implements ContainerRequestFilter { + + private final javax.ws.rs.container.ResourceInfo resourceInfo; + private final ConcurrentMap<Method, Set<String>> rolesAllowed; + private final Set<Method> denyAll; + private final Set<Method> permitAll; + + public MPJWTSecurityAnnotationsInterceptor(final javax.ws.rs.container.ResourceInfo resourceInfo, final ConcurrentMap<Method, Set<String>> rolesAllowed, final Set<Method> denyAll, final Set<Method> permitAll) { + this.resourceInfo = resourceInfo; + this.rolesAllowed = rolesAllowed; + this.denyAll = denyAll; + this.permitAll = permitAll; + } + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + if (permitAll.contains(resourceInfo.getResourceMethod())) { + return; + } + + if (denyAll.contains(resourceInfo.getResourceMethod())) { + forbidden(requestContext); + return; + } + + final Set<String> roles = rolesAllowed.get(resourceInfo.getResourceMethod()); + if (roles != null && !roles.isEmpty()) { + final SecurityContext securityContext = requestContext.getSecurityContext(); + for (String role : roles) { + if (!securityContext.isUserInRole(role)) { + forbidden(requestContext); + break; + } + } + } + + } + + private void forbidden(final ContainerRequestContext requestContext) { + requestContext.abortWith(Response.status(HttpURLConnection.HTTP_FORBIDDEN).build()); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java new file mode 100644 index 0000000..5a0a00a --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomee.microprofile.jwt.jaxrs; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +@Provider +public class MPJWTSecurityAnnotationsInterceptorsFeature implements DynamicFeature { + + private final ConcurrentMap<Method, Set<String>> rolesAllowed = new ConcurrentHashMap<>(); + private final Set<Method> denyAll = new HashSet<>(); + private final Set<Method> permitAll = new HashSet<>(); + + @Override + public void configure(final ResourceInfo resourceInfo, final FeatureContext context) { + + final boolean hasSecurity = processSecurityAnnotations(resourceInfo.getResourceClass(), resourceInfo.getResourceMethod()); + + if (hasSecurity) { + context.register(new MPJWTSecurityAnnotationsInterceptor(resourceInfo, rolesAllowed, denyAll, permitAll)); + } + + } + + private boolean processSecurityAnnotations(final Class clazz, final Method method) { + + final List<Class<? extends Annotation>[]> classSecurityAnnotations = hasClassLevelAnnotations(clazz, + RolesAllowed.class, PermitAll.class, DenyAll.class); + + final List<Class<? extends Annotation>[]> methodSecurityAnnotations = hasMethodLevelAnnotations(method, + RolesAllowed.class, PermitAll.class, DenyAll.class); + + if (classSecurityAnnotations.size() == 0 && methodSecurityAnnotations.size() == 0) { + return false; // nothing to do + } + + /* + * Process annotations at the class level + */ + if (classSecurityAnnotations.size() > 1) { + // todo error to properly handle + } + + if (methodSecurityAnnotations.size() > 1) { + // todo proper error handling + } + + if (methodSecurityAnnotations.size() == 0) { // no need to deal with class level annotations if the method has some + final RolesAllowed classRolesAllowed = (RolesAllowed) clazz.getAnnotation(RolesAllowed.class); + final PermitAll classPermitAll = (PermitAll) clazz.getAnnotation(PermitAll.class); + final DenyAll classDenyAll = (DenyAll) clazz.getAnnotation(DenyAll.class); + + if (classRolesAllowed != null) { + Set<String> roles = new HashSet<String>(); + final Set<String> previous = rolesAllowed.putIfAbsent(method, roles); + if (previous != null) { + roles = previous; + } + roles.addAll(Arrays.asList(classRolesAllowed.value())); + } + + if (classPermitAll != null) { + permitAll.add(method); + } + + if (classDenyAll != null) { + denyAll.add(method); + } + } + + final RolesAllowed mthdRolesAllowed = (RolesAllowed) method.getAnnotation(RolesAllowed.class); + final PermitAll mthdPermitAll = (PermitAll) method.getAnnotation(PermitAll.class); + final DenyAll mthdDenyAll = (DenyAll) method.getAnnotation(DenyAll.class); + + if (mthdRolesAllowed != null) { + Set<String> roles = new HashSet<String>(); + final Set<String> previous = rolesAllowed.putIfAbsent(method, roles); + if (previous != null) { + roles = previous; + } + roles.addAll(Arrays.asList(mthdRolesAllowed.value())); + } + + if (mthdPermitAll != null) { + permitAll.add(method); + } + + if (mthdDenyAll != null) { + denyAll.add(method); + } + + return true; + } + + private List<Class<? extends Annotation>[]> hasClassLevelAnnotations(final Class clazz, final Class<? extends Annotation>... annotationsToCheck) { + final List<Class<? extends Annotation>[]> list = new ArrayList<>(); + for (Class<? extends Annotation> annotationToCheck : annotationsToCheck) { + if (clazz.isAnnotationPresent(annotationToCheck)) { + list.add(annotationsToCheck); + } + } + return list; + } + + private List<Class<? extends Annotation>[]> hasMethodLevelAnnotations(final Method method, final Class<? extends Annotation>... annotationsToCheck) { + final List<Class<? extends Annotation>[]> list = new ArrayList<>(); + for (Class<? extends Annotation> annotationToCheck : annotationsToCheck) { + if (method.isAnnotationPresent(annotationToCheck)) { + list.add(annotationsToCheck); + } + } + return list; + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/main/resources/META-INF/org.apache.openejb.extension ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/resources/META-INF/org.apache.openejb.extension b/tck/mp-jwt-embedded/src/main/resources/META-INF/org.apache.openejb.extension new file mode 100644 index 0000000..9734019 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/resources/META-INF/org.apache.openejb.extension @@ -0,0 +1 @@ +org.apache.tomee.microprofile.jwt.jaxrs.MPJWPProviderRegistration \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tck/mp-jwt-embedded/src/test/resources/dev.xml ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/test/resources/dev.xml b/tck/mp-jwt-embedded/src/test/resources/dev.xml index 3814456..3700a3f 100644 --- a/tck/mp-jwt-embedded/src/test/resources/dev.xml +++ b/tck/mp-jwt-embedded/src/test/resources/dev.xml @@ -21,6 +21,7 @@ <!-- The required base JAX-RS and CDI based tests that all MP-JWT implementations must pass. --> + <test name="base-tests" verbose="10"> <groups> <define name="base-groups"> @@ -50,8 +51,9 @@ <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.ClaimValueInjectionTest" /> <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.JsonValueInjectionTest" /> <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.ProviderInjectionTest" /> - <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.RolesAllowedTest" /> <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.InvalidTokenTest" /> + <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.RolesAllowedTest" /> + <class name="org.eclipse.microprofile.jwt.tck.container.jaxrs.RolesAllowedTest" /> </classes> </test> @@ -73,10 +75,15 @@ </run> </groups> <classes> - <class name="org.eclipse.microprofile.jwt.tck.container.ejb.EjbTest" /> + <class name="org.eclipse.microprofile.jwt.tck.container.ejb.EjbTest" > + <methods> + <!-- Excluded cause we never really enforce ACC context for EJB Calls in TomEE --> + <exclude name="getSubjectClass"/> + </methods> + </class> + <class name="org.eclipse.microprofile.jwt.tck.container.servlet.ServletTest" /> <class name="org.eclipse.microprofile.jwt.tck.container.jacc.SubjectTest" /> </classes> </test> - </suite> http://git-wip-us.apache.org/repos/asf/tomee/blob/893525f8/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/TomcatSecurityService.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/TomcatSecurityService.java b/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/TomcatSecurityService.java index 81bffd4..4076ab5 100644 --- a/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/TomcatSecurityService.java +++ b/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/TomcatSecurityService.java @@ -39,6 +39,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.Set; import java.util.UUID; +import java.util.concurrent.Callable; public class TomcatSecurityService extends AbstractSecurityService { private static final boolean ONLY_DEFAULT_REALM = "true".equals(SystemInstance.get().getProperty("tomee.realm.only-default", "false")); @@ -330,4 +331,23 @@ public class TomcatSecurityService extends AbstractSecurityService { } } + @Override + protected SecurityContext getDefaultContext() { + final Request request = OpenEJBSecurityListener.requests.get(); + if (request != null) { + final Object subjectCallable = request.getAttribute("javax.security.auth.subject.callable"); + if (subjectCallable != null && Callable.class.isInstance(subjectCallable)) { + // maybe we should check, but it's so specific ... + try { + final Subject subject = (Subject) Callable.class.cast(subjectCallable).call(); + return new SecurityContext(subject); + + } catch (final Exception e) { + // ignore and let it go to the default implementation + } + } + } + + return super.getDefaultContext(); + } }