Some refactoring to remove Java 8 lambdas
Project: http://git-wip-us.apache.org/repos/asf/tomee/repo Commit: http://git-wip-us.apache.org/repos/asf/tomee/commit/72c08321 Tree: http://git-wip-us.apache.org/repos/asf/tomee/tree/72c08321 Diff: http://git-wip-us.apache.org/repos/asf/tomee/diff/72c08321 Branch: refs/heads/master Commit: 72c0832184c4354627cb97a52f557e6d0ffb4aa2 Parents: 09ca434 Author: Jean-Louis Monteiro <jeano...@gmail.com> Authored: Tue Apr 17 01:27:50 2018 +0200 Committer: Jean-Louis Monteiro <jeano...@gmail.com> Committed: Tue Apr 17 01:27:50 2018 +0200 ---------------------------------------------------------------------- mp-jwt/pom.xml | 93 +++++ .../tomee/microprofile/jwt/MPJWTFilter.java | 271 ++++++++++++++ .../microprofile/jwt/MPJWTInitializer.java | 64 ++++ .../tomee/microprofile/jwt/ParseException.java | 32 ++ .../tomee/microprofile/jwt/cdi/ClaimBean.java | 373 +++++++++++++++++++ .../jwt/cdi/ClaimInjectionPoint.java | 70 ++++ .../microprofile/jwt/cdi/ClaimValueWrapper.java | 53 +++ .../microprofile/jwt/cdi/DefaultLiteral.java | 24 ++ .../microprofile/jwt/cdi/JsonbProducer.java | 46 +++ .../microprofile/jwt/cdi/MPJWTCDIExtension.java | 136 +++++++ .../microprofile/jwt/cdi/MPJWTProducer.java | 49 +++ .../jwt/config/JWTAuthContextInfo.java | 67 ++++ .../jwt/config/JWTAuthContextInfoProvider.java | 61 +++ .../jwt/jaxrs/MPJWPProviderRegistration.java | 36 ++ .../MPJWTSecurityAnnotationsInterceptor.java | 57 +++ ...TSecurityAnnotationsInterceptorsFeature.java | 144 +++++++ .../principal/DefaultJWTCallerPrincipal.java | 360 ++++++++++++++++++ .../DefaultJWTCallerPrincipalFactory.java | 92 +++++ .../jwt/principal/JWTCallerPrincipal.java | 59 +++ .../principal/JWTCallerPrincipalFactory.java | 129 +++++++ mp-jwt/src/main/resources/META-INF/beans.xml | 1 + .../META-INF/org.apache.openejb.extension | 1 + .../javax.enterprise.inject.spi.Extension | 1 + .../javax.servlet.ServletContainerInitializer | 1 + ...file.jwt.principal.JWTCallerPrincipalFactory | 1 + pom.xml | 1 + tck/mp-jwt-embedded/pom.xml | 44 +-- .../tomee/microprofile/jwt/MPJWTFilter.java | 258 ------------- .../microprofile/jwt/MPJWTInitializer.java | 64 ---- .../tomee/microprofile/jwt/ParseException.java | 32 -- .../tomee/microprofile/jwt/cdi/ClaimBean.java | 360 ------------------ .../jwt/cdi/ClaimInjectionPoint.java | 70 ---- .../microprofile/jwt/cdi/ClaimValueWrapper.java | 53 --- .../microprofile/jwt/cdi/DefaultLiteral.java | 24 -- .../microprofile/jwt/cdi/JsonbProducer.java | 46 --- .../microprofile/jwt/cdi/MPJWTCDIExtension.java | 100 ----- .../microprofile/jwt/cdi/MPJWTProducer.java | 49 --- .../jwt/config/JWTAuthContextInfo.java | 67 ---- .../jwt/config/JWTAuthContextInfoProvider.java | 61 --- .../jwt/jaxrs/MPJWPProviderRegistration.java | 36 -- .../MPJWTSecurityAnnotationsInterceptor.java | 57 --- ...TSecurityAnnotationsInterceptorsFeature.java | 144 ------- .../principal/DefaultJWTCallerPrincipal.java | 360 ------------------ .../DefaultJWTCallerPrincipalFactory.java | 92 ----- .../jwt/principal/JWTCallerPrincipal.java | 59 --- .../principal/JWTCallerPrincipalFactory.java | 129 ------- .../src/main/resources/META-INF/beans.xml | 1 - .../META-INF/org.apache.openejb.extension | 1 - .../javax.enterprise.inject.spi.Extension | 1 - .../javax.servlet.ServletContainerInitializer | 1 - ...file.jwt.principal.JWTCallerPrincipalFactory | 1 - 51 files changed, 2225 insertions(+), 2107 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/pom.xml ---------------------------------------------------------------------- diff --git a/mp-jwt/pom.xml b/mp-jwt/pom.xml new file mode 100644 index 0000000..adc79c6 --- /dev/null +++ b/mp-jwt/pom.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <parent> + <artifactId>tomee-project</artifactId> + <groupId>org.apache.tomee</groupId> + <version>7.0.5-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <artifactId>mp-jwt</artifactId> + <packaging>jar</packaging> + <name>OpenEJB :: Microprofile JWT</name> + + <properties> + <mp-jwt.version>1.1-SNAPSHOT</mp-jwt.version> + </properties> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>javaee-api</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>openejb-core</artifactId> + <version>${project.version}</version> + <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> + <artifactId>slf4j-jdk14</artifactId> + <version>${slf4j.version}</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.bitbucket.b_c</groupId> + <artifactId>jose4j</artifactId> + <version>0.6.0</version> + </dependency> + + <dependency> + <groupId>org.eclipse.microprofile.jwt</groupId> + <artifactId>microprofile-jwt-auth-tck</artifactId> + <version>${mp-jwt.version}</version> + </dependency> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-json_1.1_spec</artifactId> + <version>1.0</version> + </dependency> + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jsonb_1.0_spec</artifactId> + <version>1.0</version> + </dependency> + <dependency> + <groupId>org.apache.johnzon</groupId> + <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> + + +</project> http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java new file mode 100644 index 0000000..a7b47d3 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java @@ -0,0 +1,271 @@ +/* + * 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.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo; +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; +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.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.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 = "/*") +public class MPJWTFilter implements Filter { + + @Inject + private JWTAuthContextInfo authContextInfo; + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // nothing so far + + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { + + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + + // now wrap the httpServletRequest and override the principal so CXF can propagate into the SecurityContext + try { + chain.doFilter(new MPJWTServletRequestWrapper(httpServletRequest, authContextInfo), response); + + } 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()); + } + + if (MPJWTException.class.isInstance(e.getCause())) { + final MPJWTException jwtException = MPJWTException.class.cast(e.getCause()); + HttpServletResponse.class.cast(response).sendError(jwtException.getStatus(), jwtException.getMessage()); + } + + } + + } + + @Override + public void destroy() { + // nothing to do + } + + private static Function<HttpServletRequest, JsonWebToken> token(final HttpServletRequest httpServletRequest, final JWTAuthContextInfo authContextInfo) { + + return new Function<HttpServletRequest, JsonWebToken>() { + + private JsonWebToken jsonWebToken; + + @Override + 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; + + } + }; + + } + + 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", (Callable<Subject>) new Callable<Subject>() { + @Override + public Subject call() throws Exception { + final Set<Principal> principals = new LinkedHashSet<>(); + final JsonWebToken namePrincipal = tokenFunction.apply(request); + principals.add(namePrincipal); + principals.addAll(namePrincipal.getGroups().stream().map(new Function<String, Principal>() { + @Override + public Principal apply(final String role) { + return (Principal) new Principal() { + @Override + public String getName() { + return role; + } + }; + } + }).collect(Collectors.<Principal>toList())); + return new Subject(true, principals, Collections.emptySet(), Collections.emptySet()); + } + }); + } + + @Override + public Principal getUserPrincipal() { + return tokenFunction.apply(request); + } + + @Override + public boolean isUserInRole(String role) { + final JsonWebToken jsonWebToken = tokenFunction.apply(request); + return jsonWebToken.getGroups().contains(role); + } + + @Override + public String getAuthType() { + return "MP-JWT"; + } + + } + + 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(); + } + + 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/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java new file mode 100644 index 0000000..cede7dc --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTInitializer.java @@ -0,0 +1,64 @@ +/* + * 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.servlet.FilterRegistration; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.annotation.HandlesTypes; +import javax.ws.rs.core.Application; +import java.util.Set; + +/** + * Responsible for adding the filter into the chain and doing all other initialization + */ +@HandlesTypes(LoginConfig.class) +public class MPJWTInitializer implements ServletContainerInitializer { + + @Override + public void onStartup(final Set<Class<?>> classes, final ServletContext ctx) throws ServletException { + + if (classes == null || classes.isEmpty()) { + return; // no classe having @LoginConfig on it + } + + for (Class<?> clazz : classes) { + final LoginConfig loginConfig = clazz.getAnnotation(LoginConfig.class); + + if (loginConfig.authMethod() == null && !"MP-JWT".equals(loginConfig.authMethod())) { + continue; + } + + if (!Application.class.isAssignableFrom(clazz)) { + continue; + // do we really want Application? + // See https://github.com/eclipse/microprofile-jwt-auth/issues/70 to clarify this point + } + + final FilterRegistration.Dynamic mpJwtFilter = ctx.addFilter("mp-jwt-filter", MPJWTFilter.class); + mpJwtFilter.setAsyncSupported(true); + mpJwtFilter.addMappingForUrlPatterns(null, false, "/*"); + + break; // no need to add it more than once + } + + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/ParseException.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/ParseException.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/ParseException.java new file mode 100644 index 0000000..d9572d5 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/ParseException.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * The exception thrown when + */ +public class ParseException extends Exception { + private static final long serialVersionUID = 1L; + + public ParseException(final String message) { + super(message); + } + + public ParseException(final String message, final Throwable cause) { + super(message, cause); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java new file mode 100644 index 0000000..be83a9b --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimBean.java @@ -0,0 +1,373 @@ +/* + * 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.cdi; + +import org.eclipse.microprofile.jwt.Claim; +import org.eclipse.microprofile.jwt.ClaimValue; +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; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.PassivationCapable; +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.json.Json; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonValue; +import javax.json.bind.Jsonb; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Logger; + +@Vetoed +public class ClaimBean<T> implements Bean<T>, PassivationCapable { + + private static final Logger logger = Logger.getLogger(MPJWTCDIExtension.class.getName()); + + private static final Set<Annotation> QUALIFIERS = new HashSet<>(); + + static { + QUALIFIERS.add(new ClaimLiteral()); + } + + @Inject + private Jsonb jsonb; + + 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; + types = new HashSet<>(); + types.add(type); + rawType = getRawType(type); + this.id = "ClaimBean_" + types; + scope = Dependent.class; + } + + private Class getRawType(final Type type) { + if (Class.class.isInstance(type)) { + return Class.class.cast(type); + + } else if (ParameterizedType.class.isInstance(type)) { + final ParameterizedType paramType = ParameterizedType.class.cast(type); + return Class.class.cast(paramType.getRawType()); + } + + throw new UnsupportedOperationException("Unsupported type " + type); + } + + + @Override + public Set<InjectionPoint> getInjectionPoints() { + return Collections.emptySet(); + } + + @Override + public Class<?> getBeanClass() { + return rawType; + } + + @Override + public boolean isNullable() { + return false; + } + + @Override + public void destroy(final T instance, final CreationalContext<T> context) { + logger.finest("Destroying CDI Bean for type " + types.iterator().next()); + } + + @Override + public Set<Type> getTypes() { + return types; + } + + @Override + public Set<Annotation> getQualifiers() { + return QUALIFIERS; + } + + @Override + public Class<? extends Annotation> getScope() { + return scope; + } + + @Override + public String getName() { + return null; + } + + @Override + public Set<Class<? extends Annotation>> getStereotypes() { + return Collections.emptySet(); + } + + @Override + public boolean isAlternative() { + return false; + } + + @Override + public String getId() { + return id; + } + + @Override + public T create(final CreationalContext<T> context) { + logger.finest("Creating CDI Bean for type " + types.iterator().next()); + final InjectionPoint ip = (InjectionPoint) bm.getInjectableReference(new ClaimInjectionPoint(this), context); + if (ip == null) { + throw new IllegalStateException("Could not retrieve InjectionPoint for type " + types.iterator().next()); + } + + final Annotated annotated = ip.getAnnotated(); + final Claim claim = annotated.getAnnotation(Claim.class); + final String key = getClaimKey(claim); + + logger.finest(String.format("Found Claim injection with name=%s and for %s", key, ip.toString())); + + if (ParameterizedType.class.isInstance(annotated.getBaseType())) { + final ParameterizedType paramType = ParameterizedType.class.cast(annotated.getBaseType()); + final Type rawType = paramType.getRawType(); + if (Class.class.isInstance(rawType) && paramType.getActualTypeArguments().length == 1) { + + final Class<?> rawTypeClass = ((Class<?>) rawType); + + // handle Provider<T> + if (rawTypeClass.isAssignableFrom(Provider.class)) { + final Type providerType = paramType.getActualTypeArguments()[0]; + if (ParameterizedType.class.isInstance(providerType) && isOptional(ParameterizedType.class.cast(providerType))) { + return (T) Optional.ofNullable(getClaimValue(key)); + } + return getClaimValue(key); + } + + // handle Instance<T> + if (rawTypeClass.isAssignableFrom(Instance.class)) { + final Type instanceType = paramType.getActualTypeArguments()[0]; + if (ParameterizedType.class.isInstance(instanceType) && isOptional(ParameterizedType.class.cast(instanceType))) { + return (T) Optional.ofNullable(getClaimValue(key)); + } + return getClaimValue(key); + } + + // handle ClaimValue<T> + if (rawTypeClass.isAssignableFrom(ClaimValue.class)) { + final Type claimValueType = paramType.getActualTypeArguments()[0]; + + final ClaimValueWrapper claimValueWrapper = new ClaimValueWrapper(key); + if (ParameterizedType.class.isInstance(claimValueType) && isOptional(ParameterizedType.class.cast(claimValueType))) { + claimValueWrapper.setValue(new Supplier() { + @Override + public Object get() { + final T claimValue = ClaimBean.this.getClaimValue(key); + return Optional.ofNullable(claimValue); + } + }); + + } else if (ParameterizedType.class.isInstance(claimValueType) && isSet(ParameterizedType.class.cast(claimValueType))) { + claimValueWrapper.setValue(new Supplier() { + @Override + public Object get() { + final T claimValue = ClaimBean.this.getClaimValue(key); + return claimValue; + } + }); + + } else if (ParameterizedType.class.isInstance(claimValueType) && isList(ParameterizedType.class.cast(claimValueType))) { + claimValueWrapper.setValue(new Supplier() { + @Override + public Object get() { + final T claimValue = ClaimBean.this.getClaimValue(key); + return claimValue; + } + }); + + } else if (Class.class.isInstance(claimValueType)) { + claimValueWrapper.setValue(new Supplier() { + @Override + public Object get() { + final T claimValue = ClaimBean.this.getClaimValue(key); + return claimValue; + } + }); + + } else { + throw new IllegalArgumentException("Unsupported ClaimValue type " + claimValueType.toString()); + } + + return (T) claimValueWrapper; + } + + // handle Optional<T> + if (rawTypeClass.isAssignableFrom(Optional.class)) { + return getClaimValue(key); + } + + // handle Set<T> + if (rawTypeClass.isAssignableFrom(Set.class)) { + return getClaimValue(key); + } + + // handle List<T> + if (rawTypeClass.isAssignableFrom(List.class)) { + return getClaimValue(key); + } + } + + } else if (annotated.getBaseType().getTypeName().startsWith("javax.json.Json")) { + // handle JsonValue<T> (number, string, etc) + return (T) toJson(key); + + } else { + // handle Raw types + return getClaimValue(key); + } + + throw new IllegalStateException("Unhandled Claim type " + annotated.getBaseType()); + } + + public static String getClaimKey(final Claim claim) { + return claim.standard() == Claims.UNKNOWN ? claim.value() : claim.standard().name(); + } + + private T getClaimValue(final String name) { + final Bean<?> bean = bm.resolve(bm.getBeans(JsonWebToken.class)); + JsonWebToken jsonWebToken = null; + 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 = jsonWebToken.claim(name); + logger.finest(String.format("Found ClaimValue=%s for name=%s", claimValue, name)); + return claimValue.orElse(null); + } + + private JsonValue toJson(final String name) { + final T claimValue = getClaimValue(name); + return wrapValue(claimValue); + } + + private static final String TMP = "tmp"; + + private JsonValue wrapValue(final Object value) { + JsonValue jsonValue = null; + + if (JsonValue.class.isInstance(value)) { + // This may already be a JsonValue + jsonValue = JsonValue.class.cast(value); + + } else if (String.class.isInstance(value)) { + jsonValue = Json.createObjectBuilder() + .add(TMP, value.toString()) + .build() + .getJsonString(TMP); + + } else if (Number.class.isInstance(value)) { + final Number number = Number.class.cast(value); + if ((Long.class.isInstance(number)) || (Integer.class.isInstance(number))) { + jsonValue = Json.createObjectBuilder() + .add(TMP, number.longValue()) + .build() + .getJsonNumber(TMP); + + } else { + jsonValue = Json.createObjectBuilder() + .add(TMP, number.doubleValue()) + .build() + .getJsonNumber(TMP); + } + + } else if (Boolean.class.isInstance(value)) { + final Boolean flag = Boolean.class.cast(value); + jsonValue = flag ? JsonValue.TRUE : JsonValue.FALSE; + + } else if (Collection.class.isInstance(value)) { + final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + final Collection list = Collection.class.cast(value); + + for (Object element : list) { + if (String.class.isInstance(element)) { + arrayBuilder.add(element.toString()); + + } else { + final JsonValue jvalue = wrapValue(element); + arrayBuilder.add(jvalue); + } + } + jsonValue = arrayBuilder.build(); + + } else if (Map.class.isInstance(value)) { + jsonValue = jsonb.fromJson(jsonb.toJson(value), JsonObject.class); + + } + return jsonValue; + } + + private boolean isOptional(final ParameterizedType type) { + return ((Class) type.getRawType()).isAssignableFrom(Optional.class); + } + + private boolean isSet(final ParameterizedType type) { + return ((Class) type.getRawType()).isAssignableFrom(Set.class); + } + + private boolean isList(final ParameterizedType type) { + return ((Class) type.getRawType()).isAssignableFrom(List.class); + } + + private static class ClaimLiteral extends AnnotationLiteral<Claim> implements Claim { + + @Override + public String value() { + return ""; + } + + @Override + public Claims standard() { + return Claims.UNKNOWN; + } + } + +} http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimInjectionPoint.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimInjectionPoint.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimInjectionPoint.java new file mode 100644 index 0000000..17be756 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimInjectionPoint.java @@ -0,0 +1,70 @@ +/* + * 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.cdi; + +import javax.enterprise.inject.spi.Annotated; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.InjectionPoint; +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Set; + +public class ClaimInjectionPoint implements InjectionPoint { + + private final Bean bean; + + public ClaimInjectionPoint(final Bean bean) { + this.bean = bean; + } + + @Override + public boolean isTransient() { + return false; + } + + @Override + public boolean isDelegate() { + return false; + } + + @Override + public Type getType() { + return InjectionPoint.class; + } + + @Override + public Set<Annotation> getQualifiers() { + return Collections.singleton(DefaultLiteral.INSTANCE); + } + + @Override + public Member getMember() { + return null; + } + + @Override + public Bean<?> getBean() { + return bean; + } + + @Override + public Annotated getAnnotated() { + return null; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java new file mode 100644 index 0000000..2836abd --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java @@ -0,0 +1,53 @@ +/* + * 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.cdi; + +import org.eclipse.microprofile.jwt.ClaimValue; + +import java.util.function.Supplier; + +public class ClaimValueWrapper<T> implements ClaimValue<T> { + + private final String name; + private Supplier<T> value; + + public ClaimValueWrapper(final String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public T getValue() { + return value.get(); + } + + void setValue(final Supplier<T> value) { + this.value = value; + } + + @Override + public String toString() { + return "ClaimValueWrapper{" + + "name='" + name + '\'' + + ", value=" + value.get() + + '}'; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/DefaultLiteral.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/DefaultLiteral.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/DefaultLiteral.java new file mode 100644 index 0000000..273ff96 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/DefaultLiteral.java @@ -0,0 +1,24 @@ +/* + * 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.cdi; + +import javax.enterprise.inject.Default; +import javax.enterprise.util.AnnotationLiteral; + +public class DefaultLiteral extends AnnotationLiteral<Default> implements Default { + public static final Default INSTANCE = new DefaultLiteral(); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.java new file mode 100644 index 0000000..59f42c5 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonbProducer.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.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Disposes; +import javax.enterprise.inject.Produces; +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +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 final Logger log = Logger.getLogger(MPJWTCDIExtension.class.getName()); + + @Produces + public Jsonb create() { + return JsonbBuilder.create(); + } + + public void close(@Disposes final Jsonb jsonb) { + try { + jsonb.close(); + + } catch (final Exception e) { + log.log(Level.WARNING, e.getMessage(), e); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java new file mode 100644 index 0000000..051f05a --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java @@ -0,0 +1,136 @@ +/* + * 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.cdi; + +import org.apache.tomee.microprofile.jwt.MPJWTFilter; +import org.apache.tomee.microprofile.jwt.MPJWTInitializer; +import org.apache.tomee.microprofile.jwt.config.JWTAuthContextInfoProvider; +import org.eclipse.microprofile.jwt.Claim; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.BeforeBeanDiscovery; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.ProcessInjectionPoint; +import javax.inject.Provider; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class MPJWTCDIExtension implements Extension { + + private static final Predicate<InjectionPoint> NOT_PROVIDERS = new Predicate<InjectionPoint>() { + @Override + public boolean test(final InjectionPoint ip) { + return (Class.class.isInstance(ip.getType())) || (ParameterizedType.class.isInstance(ip.getType()) && ((ParameterizedType) ip.getType()).getRawType() != Provider.class); + } + }; + private static final Predicate<InjectionPoint> NOT_INSTANCES = new Predicate<InjectionPoint>() { + @Override + public boolean test(final InjectionPoint ip) { + return (Class.class.isInstance(ip.getType())) || (ParameterizedType.class.isInstance(ip.getType()) && ((ParameterizedType) ip.getType()).getRawType() != Instance.class); + } + }; + private static final Map<Type, Type> REPLACED_TYPES = new HashMap<>(); + + static { + REPLACED_TYPES.put(double.class, Double.class); + REPLACED_TYPES.put(int.class, Integer.class); + REPLACED_TYPES.put(float.class, Float.class); + REPLACED_TYPES.put(long.class, Long.class); + REPLACED_TYPES.put(boolean.class, Boolean.class); + } + + private Set<InjectionPoint> injectionPoints = new HashSet<>(); + + public void collectConfigProducer(@Observes final ProcessInjectionPoint<?, ?> pip) { + final Claim claim = pip.getInjectionPoint().getAnnotated().getAnnotation(Claim.class); + if (claim != null) { + injectionPoints.add(pip.getInjectionPoint()); + } + } + + public void registerClaimProducer(@Observes final AfterBeanDiscovery abd, final BeanManager bm) { + + final Set<Type> types = injectionPoints.stream() + .filter(NOT_PROVIDERS) + .filter(NOT_INSTANCES) + .map(new Function<InjectionPoint, Type>() { + @Override + public Type apply(final InjectionPoint ip) { + return REPLACED_TYPES.getOrDefault(ip.getType(), ip.getType()); + } + }) + .collect(Collectors.<Type>toSet()); + + final Set<Type> providerTypes = injectionPoints.stream() + .filter(NOT_PROVIDERS.negate()) + .map(new Function<InjectionPoint, Type>() { + @Override + public Type apply(final InjectionPoint ip) { + return ((ParameterizedType) ip.getType()).getActualTypeArguments()[0]; + } + }) + .collect(Collectors.<Type>toSet()); + + final Set<Type> instanceTypes = injectionPoints.stream() + .filter(NOT_INSTANCES.negate()) + .map(new Function<InjectionPoint, Type>() { + @Override + public Type apply(final InjectionPoint ip) { + return ((ParameterizedType) ip.getType()).getActualTypeArguments()[0]; + } + }) + .collect(Collectors.<Type>toSet()); + + types.addAll(providerTypes); + types.addAll(instanceTypes); + + types.stream() + .map(new Function<Type, ClaimBean>() { + @Override + public ClaimBean apply(final Type type) { + return new ClaimBean<>(bm, type); + } + }) + .forEach(new Consumer<ClaimBean>() { + @Override + public void accept(final ClaimBean claimBean) { + abd.addBean(claimBean); + } + }); + } + + public void observeBeforeBeanDiscovery(@Observes final BeforeBeanDiscovery bbd, final BeanManager beanManager) { + bbd.addAnnotatedType(beanManager.createAnnotatedType(JsonbProducer.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(MPJWTFilter.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(MPJWTInitializer.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(JWTAuthContextInfoProvider.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(MPJWTProducer.class)); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java new file mode 100644 index 0000000..42034b9 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java @@ -0,0 +1,49 @@ +/* + * 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.cdi; + +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 { + + @Inject + private HttpServletRequest httpServletRequest; + + @Produces + @RequestScoped + public JsonWebToken currentPrincipal() { + Objects.requireNonNull(httpServletRequest, "HTTP Servlet Request is required to produce a JSonWebToken principal."); + + // not very beautiful, 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 (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/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java new file mode 100644 index 0000000..a969515 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java @@ -0,0 +1,67 @@ +/* + * 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.config; + +import java.security.interfaces.RSAPublicKey; + +/** + * The public key and expected issuer needed to validate a token. + */ +public class JWTAuthContextInfo { + + private RSAPublicKey signerKey; + private String issuedBy; + private int expGracePeriodSecs = 60; + + public JWTAuthContextInfo() { + } + + public JWTAuthContextInfo(final RSAPublicKey signerKey, final String issuedBy) { + this.signerKey = signerKey; + this.issuedBy = issuedBy; + } + + public JWTAuthContextInfo(final JWTAuthContextInfo orig) { + this.signerKey = orig.signerKey; + this.issuedBy = orig.issuedBy; + this.expGracePeriodSecs = orig.expGracePeriodSecs; + } + + public RSAPublicKey getSignerKey() { + return signerKey; + } + + public void setSignerKey(final RSAPublicKey signerKey) { + this.signerKey = signerKey; + } + + public String getIssuedBy() { + return issuedBy; + } + + public void setIssuedBy(final String issuedBy) { + this.issuedBy = issuedBy; + } + + public int getExpGracePeriodSecs() { + return expGracePeriodSecs; + } + + public void setExpGracePeriodSecs(final int expGracePeriodSecs) { + this.expGracePeriodSecs = expGracePeriodSecs; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java new file mode 100644 index 0000000..9247e04 --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java @@ -0,0 +1,61 @@ +/* + * 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.config; + +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Produces; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Optional; + +@Dependent +public class JWTAuthContextInfoProvider { + + @Produces + Optional<JWTAuthContextInfo> getOptionalContextInfo() throws NoSuchAlgorithmException, InvalidKeySpecException { + JWTAuthContextInfo contextInfo = new JWTAuthContextInfo(); + + // todo use MP Config to load the configuration + contextInfo.setIssuedBy("https://server.example.com"); + + final String pemEncoded = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq" + + "Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR" + + "TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e" + + "UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9" + + "AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn" + + "sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x" + + "nQIDAQAB"; + byte[] encodedBytes = Base64.getDecoder().decode(pemEncoded); + + final X509EncodedKeySpec spec = new X509EncodedKeySpec(encodedBytes); + final KeyFactory kf = KeyFactory.getInstance("RSA"); + final RSAPublicKey pk = (RSAPublicKey) kf.generatePublic(spec); + + contextInfo.setSignerKey(pk); + + return Optional.of(contextInfo); + } + + @Produces + JWTAuthContextInfo getContextInfo() throws InvalidKeySpecException, NoSuchAlgorithmException { + return getOptionalContextInfo().get(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java new file mode 100644 index 0000000..34f152f --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWPProviderRegistration.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * 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 and providers are going to be scanned automatically + */ +public class MPJWPProviderRegistration { + + public void registerProvider(@Observes final ExtensionProviderRegistration event) { + 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/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java new file mode 100644 index 0000000..f604e6b --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptor.java @@ -0,0 +1,57 @@ +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/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/jaxrs/MPJWTSecurityAnnotationsInterceptorsFeature.java new file mode 100644 index 0000000..58b3203 --- /dev/null +++ b/mp-jwt/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) { // no need to add interceptor on the resources that don(t have any security requirements to enforce + 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) { + throw new IllegalStateException(clazz.getName() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); + } + + if (methodSecurityAnnotations.size() > 1) { + throw new IllegalStateException(method.toString() + " has more than one security annotation (RolesAllowed, PermitAll, DenyAll)."); + } + + 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 = method.getAnnotation(RolesAllowed.class); + final PermitAll mthdPermitAll = method.getAnnotation(PermitAll.class); + final DenyAll mthdDenyAll = 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/72c08321/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java ---------------------------------------------------------------------- diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java new file mode 100644 index 0000000..661fbde --- /dev/null +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java @@ -0,0 +1,360 @@ +/* + * 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.principal; + +import org.eclipse.microprofile.jwt.Claims; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; +import javax.security.auth.Subject; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A default implementation of JWTCallerPrincipal using jose4j + * Another implementation could use nimbus and another plain JSON-P + */ +public class DefaultJWTCallerPrincipal extends JWTCallerPrincipal { + + private static final Logger logger = Logger.getLogger(DefaultJWTCallerPrincipal.class.getName()); + private final String jwt; + private final String type; + private final JwtClaims claimsSet; + + /** + * Create the DefaultJWTCallerPrincipal from the parsed JWT token and the extracted principal name + * + * @param jwt - the parsed JWT token representation + * @param name - the extracted unqiue name to use as the principal name; from "upn", "preferred_username" or "sub" claim + */ + public DefaultJWTCallerPrincipal(final String jwt, final String type, final JwtClaims claimsSet, final String name) { + super(name); + this.jwt = jwt; + this.type = type; + this.claimsSet = claimsSet; + fixJoseTypes(); + } + + @Override + public Set<String> getAudience() { + final Set<String> audSet = new HashSet<>(); + try { + final List<String> audList = claimsSet.getStringListClaimValue("aud"); + if (audList != null) { + audSet.addAll(audList); + } + + } catch (final MalformedClaimException e) { + try { + final String aud = claimsSet.getStringClaimValue("aud"); + audSet.add(aud); + } catch (final MalformedClaimException e1) { + logger.log(Level.FINEST, "Can't retrieve malformed 'aud' claim.", e); + } + } + return audSet.isEmpty() ? null : audSet; + } + + @Override + public Set<String> getGroups() { + final HashSet<String> groups = new HashSet<>(); + try { + final List<String> globalGroups = claimsSet.getStringListClaimValue("groups"); + if (globalGroups != null) { + groups.addAll(globalGroups); + } + + } catch (final MalformedClaimException e) { + logger.log(Level.FINEST, "Can't retrieve malformed 'groups' claim.", e); + } + return groups; + } + + + @Override + public Set<String> getClaimNames() { + return new HashSet<>(claimsSet.getClaimNames()); + } + + public String getRawToken() { + return jwt; + } + + @Override + public Object getClaim(final String claimName) { + Claims claimType = Claims.UNKNOWN; + Object claim = null; + try { + claimType = Claims.valueOf(claimName); + } catch (IllegalArgumentException e) { + } + // Handle the jose4j NumericDate types and + switch (claimType) { + case exp: + case iat: + case auth_time: + case nbf: + case updated_at: + try { + claim = claimsSet.getClaimValue(claimType.name(), Long.class); + if (claim == null) { + claim = new Long(0); + } + } catch (final MalformedClaimException e) { + logger.log(Level.FINEST, "Can't retrieve 'updated_at' a malformed claim.", e); + } + break; + case groups: + claim = getGroups(); + break; + case aud: + claim = getAudience(); + break; + case UNKNOWN: + claim = claimsSet.getClaimValue(claimName); + break; + default: + claim = claimsSet.getClaimValue(claimType.name()); + } + return claim; + } + + @Override + public boolean implies(final Subject subject) { + return false; + } + + public String toString() { + return toString(false); + } + + /** + * TODO: showAll is ignored and currently assumed true + * + * @param showAll - should all claims associated with the JWT be displayed or should only those defined in the + * JsonWebToken interface be displayed. + * @return JWTCallerPrincipal string view + */ + @Override + public String toString(boolean showAll) { + String toString = "DefaultJWTCallerPrincipal{" + + "id='" + getTokenID() + '\'' + + ", name='" + getName() + '\'' + + ", expiration=" + getExpirationTime() + + ", notBefore=" + getClaim(Claims.nbf.name()) + + ", issuedAt=" + getIssuedAtTime() + + ", issuer='" + getIssuer() + '\'' + + ", audience=" + getAudience() + + ", subject='" + getSubject() + '\'' + + ", type='" + type + '\'' + + ", issuedFor='" + getClaim("azp") + '\'' + + ", authTime=" + getClaim("auth_time") + + ", givenName='" + getClaim("given_name") + '\'' + + ", familyName='" + getClaim("family_name") + '\'' + + ", middleName='" + getClaim("middle_name") + '\'' + + ", nickName='" + getClaim("nickname") + '\'' + + ", preferredUsername='" + getClaim("preferred_username") + '\'' + + ", email='" + getClaim("email") + '\'' + + ", emailVerified=" + getClaim(Claims.email_verified.name()) + + ", allowedOrigins=" + getClaim("allowedOrigins") + + ", updatedAt=" + getClaim("updated_at") + + ", acr='" + getClaim("acr") + '\''; + + final StringBuilder tmp = new StringBuilder(toString); + tmp.append(", groups=["); + for (String group : getGroups()) { + tmp.append(group); + tmp.append(','); + } + tmp.setLength(tmp.length() - 1); + tmp.append("]}"); + return tmp.toString(); + } + + /** + * Convert the types jose4j uses for address, sub_jwk, and jwk + */ + private void fixJoseTypes() { + if (claimsSet.hasClaim(Claims.address.name())) { + replaceMap(Claims.address.name()); + } + if (claimsSet.hasClaim(Claims.jwk.name())) { + replaceMap(Claims.jwk.name()); + } + if (claimsSet.hasClaim(Claims.sub_jwk.name())) { + replaceMap(Claims.sub_jwk.name()); + } + + // Handle custom claims + final Set<String> customClaimNames = filterCustomClaimNames(claimsSet.getClaimNames()); + for (String name : customClaimNames) { + final Object claimValue = claimsSet.getClaimValue(name); + if (claimValue instanceof List) { + replaceList(name); + + } else if (claimValue instanceof Map) { + replaceMap(name); + + } else if (claimValue instanceof Number) { + replaceNumber(name); + } + } + } + + /** + * Determine the custom claims in the set + * + * @param claimNames - the current set of claim names in this token + * @return the possibly empty set of names for non-Claims claims + */ + private Set<String> filterCustomClaimNames(final Collection<String> claimNames) { + final HashSet<String> customNames = new HashSet<>(claimNames); + for (Claims claim : Claims.values()) { + customNames.remove(claim.name()); + } + return customNames; + } + + /** + * Replace the jose4j Map<String,Object> with a JsonObject + * + * @param name - claim name + */ + private void replaceMap(final String name) { + try { + final Map<String, Object> map = claimsSet.getClaimValue(name, Map.class); + final JsonObject jsonObject = replaceMap(map); + claimsSet.setClaim(name, jsonObject); + + } catch (final MalformedClaimException e) { + logger.log(Level.WARNING, "replaceMap failure for: " + name, e); + } + } + + private JsonObject replaceMap(final Map<String, Object> map) { + final JsonObjectBuilder builder = Json.createObjectBuilder(); + + for (Map.Entry<String, Object> entry : map.entrySet()) { + final Object entryValue = entry.getValue(); + if (entryValue instanceof Map) { + final JsonObject entryJsonObject = replaceMap((Map<String, Object>) entryValue); + builder.add(entry.getKey(), entryJsonObject); + + } else if (entryValue instanceof List) { + final JsonArray array = (JsonArray) wrapValue(entryValue); + builder.add(entry.getKey(), array); + + } else if (entryValue instanceof Long || entryValue instanceof Integer) { + final long lvalue = ((Number) entryValue).longValue(); + builder.add(entry.getKey(), lvalue); + + } else if (entryValue instanceof Double || entryValue instanceof Float) { + final double value = ((Number) entryValue).doubleValue(); + builder.add(entry.getKey(), value); + + } else if (entryValue instanceof Boolean) { + final boolean flag = ((Boolean) entryValue).booleanValue(); + builder.add(entry.getKey(), flag); + + } else if (entryValue instanceof String) { + builder.add(entry.getKey(), entryValue.toString()); + } + } + return builder.build(); + } + + private JsonValue wrapValue(final Object value) { + JsonValue jsonValue = null; + if (value instanceof Number) { + final Number number = (Number) value; + if ((number instanceof Long) || (number instanceof Integer)) { + jsonValue = Json.createObjectBuilder() + .add("tmp", number.longValue()) + .build() + .getJsonNumber("tmp"); + + } else { + jsonValue = Json.createObjectBuilder() + .add("tmp", number.doubleValue()) + .build() + .getJsonNumber("tmp"); + } + + } else if (value instanceof Boolean) { + final Boolean flag = (Boolean) value; + jsonValue = flag ? JsonValue.TRUE : JsonValue.FALSE; + + } else if (value instanceof List) { + final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + final List list = (List) value; + for (Object element : list) { + if (element instanceof String) { + arrayBuilder.add(element.toString()); + + } else { + JsonValue jvalue = wrapValue(element); + arrayBuilder.add(jvalue); + } + + } + jsonValue = arrayBuilder.build(); + + } + return jsonValue; + } + + + /** + * Replace the jose4j List<?> with a JsonArray + * + * @param name - claim name + */ + private void replaceList(final String name) { + try { + final List list = claimsSet.getClaimValue(name, List.class); + final JsonArray array = (JsonArray) wrapValue(list); + claimsSet.setClaim(name, array); + + } catch (final MalformedClaimException e) { + logger.log(Level.WARNING, "replaceList failure for: " + name, e); + } + } + + private void replaceNumber(final String name) { + try { + final Number number = claimsSet.getClaimValue(name, Number.class); + final JsonNumber jsonNumber = (JsonNumber) wrapValue(number); + claimsSet.setClaim(name, jsonNumber); + + } catch (final MalformedClaimException e) { + logger.log(Level.WARNING, "replaceNumber failure for: " + name, e); + } + } + +} \ No newline at end of file