Add a bit more in terms of wiring - still designing and working on it
Project: http://git-wip-us.apache.org/repos/asf/tomee/repo Commit: http://git-wip-us.apache.org/repos/asf/tomee/commit/d987d3ae Tree: http://git-wip-us.apache.org/repos/asf/tomee/tree/d987d3ae Diff: http://git-wip-us.apache.org/repos/asf/tomee/diff/d987d3ae Branch: refs/heads/master Commit: d987d3aedf89abf68e2975cd3c2be4274616a2fc Parents: 672b422 Author: Jean-Louis Monteiro <jeano...@gmail.com> Authored: Thu Feb 22 22:00:27 2018 +0100 Committer: Jean-Louis Monteiro <jeano...@gmail.com> Committed: Thu Feb 22 22:00:27 2018 +0100 ---------------------------------------------------------------------- .../server/cxf/rs/MPJWTSecurityContextTest.java | 183 ++++++++++++++----- .../tomee/microprofile/jwt/MPJWTContext.java | 134 ++++++++++++++ .../tomee/microprofile/jwt/MPJWTFilter.java | 22 ++- .../microprofile/jwt/MPJWTInitializer.java | 19 +- .../src/main/resources/META-INF/beans.xml | 1 + 5 files changed, 305 insertions(+), 54 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tomee/blob/d987d3ae/server/openejb-cxf-rs/src/test/java/org/apache/openejb/server/cxf/rs/MPJWTSecurityContextTest.java ---------------------------------------------------------------------- diff --git a/server/openejb-cxf-rs/src/test/java/org/apache/openejb/server/cxf/rs/MPJWTSecurityContextTest.java b/server/openejb-cxf-rs/src/test/java/org/apache/openejb/server/cxf/rs/MPJWTSecurityContextTest.java index 62565b4..10a1305 100644 --- a/server/openejb-cxf-rs/src/test/java/org/apache/openejb/server/cxf/rs/MPJWTSecurityContextTest.java +++ b/server/openejb-cxf-rs/src/test/java/org/apache/openejb/server/cxf/rs/MPJWTSecurityContextTest.java @@ -21,71 +21,81 @@ import org.apache.cxf.endpoint.Server; import org.apache.cxf.jaxrs.model.ApplicationInfo; import org.apache.openejb.jee.WebApp; import org.apache.openejb.junit.ApplicationComposer; -import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.observer.Observes; import org.apache.openejb.server.cxf.rs.event.ServerCreated; import org.apache.openejb.server.rest.InternalApplication; -import org.apache.openejb.spi.SecurityService; import org.apache.openejb.testing.Classes; import org.apache.openejb.testing.Configuration; import org.apache.openejb.testing.EnableServices; import org.apache.openejb.testing.Module; +import org.apache.openejb.testing.RandomPort; import org.apache.openejb.testng.PropertiesBuilder; -import org.apache.openejb.util.NetworkUtil; import org.eclipse.microprofile.auth.LoginConfig; -import org.junit.BeforeClass; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.junit.Test; import org.junit.runner.RunWith; +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.ws.rs.ApplicationPath; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.core.Application; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.ext.Provider; import java.io.IOException; +import java.lang.annotation.Annotation; import java.security.Principal; -import java.util.Objects; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import java.util.Properties; +import java.util.Set; +import java.util.function.Predicate; import static org.junit.Assert.assertEquals; @EnableServices("jax-rs") @RunWith(ApplicationComposer.class) public class MPJWTSecurityContextTest { - - private static int port = -1; - - @BeforeClass - public static void beforeClass() { - port = NetworkUtil.getNextAvailablePort(); - } + @RandomPort("http") + private int port; @Configuration public Properties props() { return new PropertiesBuilder() - .p("httpejbd.port", Integer.toString(port)) .p("observer", "new://Service?class-name=" + Observer.class.getName()) // properly packaged and auto registered .build(); } @Module - @Classes(cdi = true, value = {Res.class, RestApplication.class}) + @Classes(value = {Res.class, RestApplication.class, MPFilter.class, MPContext.class}, cdi = true) public WebApp war() { return new WebApp() .contextRoot("foo"); } @Test - public void check() throws IOException { + public void check() { + // todo: close the client (just to stay clean even in tests and avoid to potentially leak) assertEquals("true", ClientBuilder.newClient() .target("http://127.0.0.1:" + port) - .path("api/foo/sc") + .path("foo/api/sc") .queryParam("role", "therole") .request() .accept(MediaType.TEXT_PLAIN_TYPE) @@ -93,7 +103,7 @@ public class MPJWTSecurityContextTest { assertEquals("false", ClientBuilder.newClient() .target("http://127.0.0.1:" + port) - .path("api/foo/sc") + .path("foo/api/sc") .queryParam("role", "another") .request() .accept(MediaType.TEXT_PLAIN_TYPE) @@ -107,13 +117,12 @@ public class MPJWTSecurityContextTest { } @Path("sc") + @ApplicationScoped public static class Res { - @Context - private SecurityContext sc; - @GET - public boolean f() { - return sc.isUserInRole("therole"); + @Produces(MediaType.TEXT_PLAIN) + public boolean f(@Context final SecurityContext ctx, @QueryParam("role") final String role) { + return ctx.isUserInRole(role); } } @@ -132,46 +141,120 @@ public class MPJWTSecurityContextTest { final LoginConfig annotation = application.getClass().getAnnotation(LoginConfig.class); if (annotation != null && "MP-JWT".equals(annotation.authMethod())) { // add the ContainerRequestFilter on the fly - // todo how to add this on the fly } } } - // this should also be packaged into the same module and delegate to the security service - @Provider - public static class MPJWTSecurityContext implements ContainerRequestFilter { - - private final SecurityService securityService; - - public MPJWTSecurityContext() { - securityService = SystemInstance.get().getComponent(SecurityService.class); - Objects.requireNonNull(securityService, "A security context needs to be properly configured to enforce security in REST services"); - } + // todo: industrialize that but idea is to fill that during startup to let it be usable at runtime + // note: the bean must be added through an extension in a real impl + @ApplicationScoped + public static class MPContext { + // todo: login config model, not the raw annot + private Map<String, LoginConfig> configs = new HashMap<>(); - @Override - public void filter(final ContainerRequestContext containerRequestContext) throws IOException { - containerRequestContext.setSecurityContext(new SecurityContext() { - @Override - public Principal getUserPrincipal() { - return securityService.getCallerPrincipal(); - } + @PostConstruct + private void init() { + // todo: drop and replace by actual init + configs.put("/api", new LoginConfig() { @Override - public boolean isUserInRole(final String s) { - return securityService.isCallerInRole(s); + public Class<? extends Annotation> annotationType() { + return LoginConfig.class; } @Override - public boolean isSecure() { - return false; + public String authMethod() { + return "MP-JWT"; } @Override - public String getAuthenticationScheme() { - return "MP-JWT"; + public String realmName() { + return ""; } }); } + + public Map<String, LoginConfig> getConfigs() { + return configs; + } } -} + @WebFilter(asyncSupported = true, urlPatterns = "/*") // addbefore from an initializer + public static class MPFilter implements Filter { + @Inject + private MPContext context; + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // no-op + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + final HttpServletRequest httpServletRequest = HttpServletRequest.class.cast(request); + final String uri = httpServletRequest.getRequestURI().substring(httpServletRequest.getContextPath().length()); + + // todo: better handling of conflicting app paths? + final Optional<Map.Entry<String, LoginConfig>> first = context.getConfigs() + .entrySet() + .stream() + .filter(new Predicate<Map.Entry<String, LoginConfig>>() { + @Override + public boolean test(final Map.Entry<String, LoginConfig> e) { + return uri.startsWith(e.getKey()); + } + }) + .findFirst(); + + if (first.isPresent()) { + chain.doFilter(new HttpServletRequestWrapper(httpServletRequest) { + private final MPPcp pcp = new MPPcp(); + + @Override + public Principal getUserPrincipal() { + return pcp; + } + + @Override + public boolean isUserInRole(final String role) { + return pcp.getGroups().contains(role); + } + }, response); + + } else { + chain.doFilter(request, response); + + } + } + + @Override + public void destroy() { + // no-op + } + } + + // todo + public static class MPPcp implements JsonWebToken { + + @Override + public String getName() { + return "mp"; + } + + @Override + public Set<String> getClaimNames() { + return Collections.singleton("test"); + } + + @Override + public <T> T getClaim(String claimName) { + return (T) "foo"; + } + + @Override + public Set<String> getGroups() { + return Collections.singleton("therole"); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/d987d3ae/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 new file mode 100644 index 0000000..2197745 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTContext.java @@ -0,0 +1,134 @@ +/* + * 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 org.eclipse.microprofile.auth.LoginConfig; + +import javax.enterprise.context.ApplicationScoped; +import java.net.URI; +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 + */ +@ApplicationScoped +public class MPJWTContext { + + private final ConcurrentMap<MPJWTConfigKey, MPJWTConfigValue> configuration = new ConcurrentHashMap<>(); + + public 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 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 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/d987d3ae/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 9b57b51..cb235fa 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,6 +16,9 @@ */ package org.apache.tomee.microprofile.jwt; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import javax.inject.Inject; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -27,11 +30,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.IOException; import java.security.Principal; +import java.util.Map; +import java.util.Optional; // async is supported because we only need to do work on the way in @WebFilter(asyncSupported = true, urlPatterns = "/*") public class MPJWTFilter implements Filter { + @Inject + private MPJWTContext context; @Override public void init(final FilterConfig filterConfig) throws ServletException { @@ -42,21 +49,30 @@ public class MPJWTFilter implements Filter { @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { - final HttpServletRequest httpServletRequest = (HttpServletRequest)request; + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + final Optional<Map.Entry<MPJWTContext.MPJWTConfigKey, MPJWTContext.MPJWTConfigValue>> first = + context.findFirst(httpServletRequest.getRequestURI()); + + if (first.isPresent()) { // nothing found in the context + chain.doFilter(request, response); + } // todo get JWT and do validation + // todo not sure what to do with the realm + + final JsonWebToken jsonWebToken = new DefaultJWTCallerPrincipal("bla"); // will be build during validation // now wrap the httpServletRequest and override the principal so CXF can propagate into the SecurityContext chain.doFilter(new HttpServletRequestWrapper(httpServletRequest) { @Override public Principal getUserPrincipal() { - return null; // todo, during parsing and validation, we need to convert into the JWT Principal as specified by the spec + return jsonWebToken; } @Override public boolean isUserInRole(String role) { - return true; // replace with a check based on the claims content + return jsonWebToken.getGroups().contains(role); } @Override http://git-wip-us.apache.org/repos/asf/tomee/blob/d987d3ae/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 e91047d..dc3d7ba 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 @@ -18,11 +18,13 @@ package org.apache.tomee.microprofile.jwt; import org.eclipse.microprofile.auth.LoginConfig; +import javax.inject.Inject; 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; @@ -32,6 +34,9 @@ import java.util.Set; @HandlesTypes(LoginConfig.class) public class MPJWTInitializer implements ServletContainerInitializer { + @Inject + private MPJWTContext context; + @Override public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException { @@ -46,12 +51,24 @@ public class MPJWTInitializer implements ServletContainerInitializer { continue; } - if (!Application.class.isInstance(clazz)) { + if (!Application.class.isAssignableFrom(clazz)) { 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); + + context.addMapping( + new MPJWTContext.MPJWTConfigKey( + ctx.getContextPath(), + applicationPath == null ? "" : applicationPath.value()), + new MPJWTContext.MPJWTConfigValue( + loginConfig.authMethod(), + loginConfig.realmName()) + ); + } } http://git-wip-us.apache.org/repos/asf/tomee/blob/d987d3ae/tck/mp-jwt-embedded/src/main/resources/META-INF/beans.xml ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/resources/META-INF/beans.xml b/tck/mp-jwt-embedded/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..330c7f6 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/resources/META-INF/beans.xml @@ -0,0 +1 @@ +<beans/> \ No newline at end of file