http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueProducer.java new file mode 100644 index 0000000..d69df02 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueProducer.java @@ -0,0 +1,75 @@ +/* + * 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 java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; + +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; + +import org.eclipse.microprofile.jwt.Claim; +import org.eclipse.microprofile.jwt.ClaimValue; +import org.eclipse.microprofile.jwt.Claims; + +/** + * A producer for the ClaimValue<T> wrapper injection sites. + * @param <T> the raw claim type + */ +public class ClaimValueProducer<T> { + + @Produces + @Claim("") + ClaimValue<T> produce(InjectionPoint ip) { + String name = getName(ip); + ClaimValue<Optional<T>> cv = MPJWTProducer.generalClaimValueProducer(name); + ClaimValue<T> returnValue = (ClaimValue<T>) cv; + Optional<T> value = cv.getValue(); + // Pull out the ClaimValue<T> T type, + Type matchType = ip.getType(); + Type actualType = Object.class; + boolean isOptional = false; + if (matchType instanceof ParameterizedType) { + actualType = ((ParameterizedType) matchType).getActualTypeArguments()[0]; + isOptional = matchType.getTypeName().equals(Optional.class.getTypeName()); + if (isOptional) { + actualType = ((ParameterizedType) matchType).getActualTypeArguments()[0]; + } + } + + if (!actualType.getTypeName().startsWith(Optional.class.getTypeName())) { + T nestedValue = value.orElse(null); + ClaimValueWrapper<T> wrapper = new ClaimValueWrapper<>(cv.getName()); + wrapper.setValue(nestedValue); + returnValue = wrapper; + } + return returnValue; + } + + String getName(InjectionPoint ip) { + String name = null; + for (Annotation ann : ip.getQualifiers()) { + if (ann instanceof Claim) { + Claim claim = (Claim) ann; + name = claim.standard() == Claims.UNKNOWN ? claim.value() : claim.standard().name(); + } + } + return name; + } +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java new file mode 100644 index 0000000..6776191 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/ClaimValueWrapper.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * An implementation of the ClaimValue interface + * + * @param <T> the claim value type + */ +public class ClaimValueWrapper<T> implements ClaimValue<T> { + private String name; + + private T value; + + public ClaimValueWrapper(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + @Override + public String toString() { + return String.format("ClaimValueWrapper[@%s], name=%s, value[%s]=%s", Integer.toHexString(hashCode()), + name, value.getClass(), value); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonValueProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonValueProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonValueProducer.java new file mode 100644 index 0000000..2f991b2 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/JsonValueProducer.java @@ -0,0 +1,111 @@ +/* + * 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.Claims; + +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.json.JsonArray; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonString; +import javax.json.JsonValue; +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A producer for JsonValue injection types + */ +public class JsonValueProducer { + private static Logger log = Logger.getLogger(JsonValueProducer.class.getName()); + + @Produces + @Claim("") + public JsonString getJsonString(InjectionPoint ip) { + return getValue(ip); + } + + @Produces + @Claim("") + public Optional<JsonString> getOptionalJsonString(InjectionPoint ip) { + return getOptionalValue(ip); + } + + @Produces + @Claim("") + public JsonNumber getJsonNumber(InjectionPoint ip) { + return getValue(ip); + } + + @Produces + @Claim("") + public Optional<JsonNumber> getOptionalJsonNumber(InjectionPoint ip) { + return getOptionalValue(ip); + } + + @Produces + @Claim("") + public JsonArray getJsonArray(InjectionPoint ip) { + return getValue(ip); + } + + @Produces + @Claim("") + public Optional<JsonArray> getOptionalJsonArray(InjectionPoint ip) { + return getOptionalValue(ip); + } + + @Produces + @Claim("") + public JsonObject getJsonObject(InjectionPoint ip) { + return getValue(ip); + } + + @Produces + @Claim("") + public Optional<JsonObject> getOptionalJsonObject(InjectionPoint ip) { + return getOptionalValue(ip); + } + + public <T extends JsonValue> T getValue(InjectionPoint ip) { + log.fine(String.format("JsonValueProducer(%s).produce", ip)); + String name = getName(ip); + T jsonValue = (T) MPJWTProducer.generalJsonValueProducer(name); + return jsonValue; + } + + public <T extends JsonValue> Optional<T> getOptionalValue(InjectionPoint ip) { + log.fine(String.format("JsonValueProducer(%s).produce", ip)); + String name = getName(ip); + T jsonValue = (T) MPJWTProducer.generalJsonValueProducer(name); + return Optional.ofNullable(jsonValue); + } + + String getName(InjectionPoint ip) { + String name = null; + for (Annotation ann : ip.getQualifiers()) { + if (ann instanceof Claim) { + Claim claim = (Claim) ann; + name = claim.standard() == Claims.UNKNOWN ? claim.value() : claim.standard().name(); + } + } + return name; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java new file mode 100644 index 0000000..95e1aea --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTCDIExtension.java @@ -0,0 +1,395 @@ +/* + * 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.config.JWTAuthContextInfoProvider; +import org.apache.tomee.microprofile.jwt.MPJWTFilter; +import org.apache.tomee.microprofile.jwt.MPJWTInitializer; +import org.apache.tomee.microprofile.jwt.TCKTokenParser; +import org.eclipse.microprofile.jwt.Claim; +import org.eclipse.microprofile.jwt.Claims; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.SessionScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AfterDeploymentValidation; +import javax.enterprise.inject.spi.BeanAttributes; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.BeforeBeanDiscovery; +import javax.enterprise.inject.spi.DeploymentException; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.enterprise.inject.spi.ProcessBeanAttributes; +import javax.enterprise.inject.spi.ProcessInjectionPoint; +import javax.enterprise.inject.spi.ProcessProducer; +import javax.inject.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +/** + * A CDI extension that provides a producer for the current authenticated JsonWebToken based on a thread + * local value that is managed by the {@link JWTAuthMechanism} request + * authentication handler. + * <p> + * This also installs the producer methods for the discovered: + * <ul> + * <li>@Claim ClaimValue<T> injection sites.</li> + * <li>@Claim raw type<T> injection sites.</li> + * <li>@Claim JsonValue injection sites.</li> + * </ul> + * + * @see JWTAuthMechanism + */ +public class MPJWTCDIExtension implements Extension { + private static Logger log = Logger.getLogger(MPJWTCDIExtension.class.getName()); + + /** + * Register the MPJWTProducer JsonWebToken producer bean + * + * @param bbd before discovery event + * @param beanManager cdi bean manager + */ + public void observeBeforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd, BeanManager beanManager) { + log.fine("MPJWTExtension(), added JWTPrincipalProducer"); + bbd.addAnnotatedType(beanManager.createAnnotatedType(TCKTokenParser.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)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(RawClaimTypeProducer.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(ClaimValueProducer.class)); + bbd.addAnnotatedType(beanManager.createAnnotatedType(JsonValueProducer.class)); + } + + /** + * Replace the general producer method BeanAttributes with one bound to the collected injection site + * types to properly reflect all of the type locations the producer method applies to. + * + * @param pba the ProcessBeanAttributes + * @see ClaimProviderBeanAttributes + */ + public void addTypeToClaimProducer(@Observes ProcessBeanAttributes pba) { + if (pba.getAnnotated().isAnnotationPresent(Claim.class)) { + Claim claim = pba.getAnnotated().getAnnotation(Claim.class); + if (claim.value().length() == 0 && claim.standard() == Claims.UNKNOWN) { + log.fine(String.format("addTypeToClaimProducer: %s\n", pba.getAnnotated())); + BeanAttributes delegate = pba.getBeanAttributes(); + String name = delegate.getName(); + if (delegate.getTypes().contains(Optional.class)) { + if (providerOptionalTypes.size() == 0) { + providerOptionalTypes.add(Optional.class); + } + pba.setBeanAttributes(new ClaimProviderBeanAttributes(delegate, providerOptionalTypes, providerQualifiers)); + // This is + } else if (name != null && name.startsWith("RawClaimTypeProducer#")) { + if (rawTypes.size() == 0) { + rawTypes.add(Object.class); + } + pba.setBeanAttributes(new ClaimProviderBeanAttributes(delegate, rawTypes, rawTypeQualifiers)); + log.fine(String.format("Setup RawClaimTypeProducer BeanAttributes")); + } + } + } + } + + public void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager beanManager) { + } + + void doProcessProducers(@Observes ProcessProducer pp) { + } + + /** + * Handle the non-{@linkplain Provider}, {@linkplain org.eclipse.microprofile.jwt.ClaimValue}, and + * {@linkplain javax.json.JsonValue} claim injection types. + * + * @param pip - the injection point event information + * @see RawClaimTypeProducer + */ + void processClaimInjections(@Observes ProcessInjectionPoint pip) { + log.fine(String.format("pipRaw: %s", pip.getInjectionPoint())); + InjectionPoint ip = pip.getInjectionPoint(); + if (ip.getAnnotated().isAnnotationPresent(Claim.class)) { + Claim claim = ip.getAnnotated().getAnnotation(Claim.class); + if (ip.getType() instanceof Class) { + Class rawClass = (Class) ip.getType(); + // Primative types + if (Modifier.isFinal(rawClass.getModifiers())) { + rawTypes.add(ip.getType()); + rawTypeQualifiers.add(claim); + log.fine(String.format("+++ Added Claim raw type: %s", ip.getType())); + Class declaringClass = ip.getMember().getDeclaringClass(); + Annotation[] appScoped = declaringClass.getAnnotationsByType(ApplicationScoped.class); + Annotation[] sessionScoped = declaringClass.getAnnotationsByType(SessionScoped.class); + if ((appScoped != null && appScoped.length > 0) || (sessionScoped != null && sessionScoped.length > 0)) { + String err = String.format("A raw type cannot be injected into application/session scope: IP=%s", ip); + pip.addDefinitionError(new DeploymentException(err)); + } + } + // This handles collections of primative types + } else if (isRawParameterizedType(ip.getType())) { + log.fine(String.format("+++ Added Claim ParameterizedType: %s", ip.getType())); + rawTypes.add(ip.getType()); + rawTypeQualifiers.add(claim); + } + } else { + log.fine(String.format("Skipping pip: %s, type: %s/%s", ip, ip.getType(), ip.getType().getClass())); + } + } + + /** + * Collect the types of all {@linkplain Provider} injection points annotated with {@linkplain Claim}. + * + * @param pip - the injection point event information + */ + void processClaimProviderInjections(@Observes ProcessInjectionPoint<?, ? extends Provider> pip) { + log.fine(String.format("pip: %s", pip.getInjectionPoint())); + final InjectionPoint ip = pip.getInjectionPoint(); + if (ip.getAnnotated().isAnnotationPresent(Claim.class)) { + Claim claim = ip.getAnnotated().getAnnotation(Claim.class); + if (claim.value().length() == 0 && claim.standard() == Claims.UNKNOWN) { + pip.addDefinitionError(new DeploymentException("@Claim at: " + ip + " has no name or valid standard enum setting")); + } + boolean usesEnum = claim.standard() != Claims.UNKNOWN; + final String claimName = usesEnum ? claim.standard().name() : claim.value(); + log.fine(String.format("Checking Provider Claim(%s), ip: %s", claimName, ip)); + ClaimIP claimIP = claims.get(claimName); + Type matchType = ip.getType(); + // The T from the Provider<T> injection site + Type actualType = ((ParameterizedType) matchType).getActualTypeArguments()[0]; + // Don't add Optional or JsonValue as this is handled specially + if (!optionalOrJsonValue(actualType)) { + rawTypes.add(actualType); + } else if (!actualType.getTypeName().startsWith("javax.json.Json")) { + // Validate that this is not an Optional<JsonValue> + Type innerType = ((ParameterizedType) actualType).getActualTypeArguments()[0]; + if (!innerType.getTypeName().startsWith("javax.json.Json")) { + providerOptionalTypes.add(actualType); + providerQualifiers.add(claim); + } + } + rawTypeQualifiers.add(claim); + ClaimIPType key = new ClaimIPType(claimName, actualType); + if (claimIP == null) { + claimIP = new ClaimIP(actualType, actualType, false, claim); + claimIP.setProviderSite(true); + claims.put(key, claimIP); + } + claimIP.getInjectionPoints().add(ip); + log.fine(String.format("+++ Added Provider Claim(%s) ip: %s", claimName, ip)); + + } + } + + /** + * Create producer methods for each ClaimValue injection site + * + * @param event - AfterBeanDiscovery + * @param beanManager - CDI bean manager + */ + void observesAfterBeanDiscovery(@Observes final AfterBeanDiscovery event, final BeanManager beanManager) { + log.fine(String.format("observesAfterBeanDiscovery, %s", claims)); + installClaimValueProducerMethodsViaSyntheticBeans(event, beanManager); + + //installClaimValueProducesViaTemplateType(event, beanManager); + } + + /** + * Create a synthetic bean with a custom Producer for the non-Provider injection sites. + * + * @param event - AfterBeanDiscovery + * @param beanManager - CDI bean manager + */ + private void installClaimValueProducerMethodsViaSyntheticBeans(final AfterBeanDiscovery event, final BeanManager beanManager) { + + } + + private boolean optionalOrJsonValue(Type type) { + boolean isOptionOrJson = type.getTypeName().startsWith(Optional.class.getTypeName()) + | type.getTypeName().startsWith("javax.json.Json"); + return isOptionOrJson; + } + + private boolean isRawParameterizedType(Type type) { + boolean isRawParameterizedType = false; + if (type instanceof ParameterizedType) { + ParameterizedType ptype = ParameterizedType.class.cast(type); + Type rawType = ptype.getRawType(); + String rawTypeName = rawType.getTypeName(); + isRawParameterizedType = !rawTypeName.startsWith("org.eclipse.microprofile.jwt"); + } + return isRawParameterizedType; + } + + /** + * A map of claim,type pairs to the injection site information + */ + private HashMap<ClaimIPType, ClaimIP> claims = new HashMap<>(); + + private Set<Type> providerOptionalTypes = new HashSet<>(); + + private Set<Type> providerTypes = new HashSet<>(); + + private Set<Type> rawTypes = new HashSet<>(); + + private Set<Annotation> rawTypeQualifiers = new HashSet<>(); + + private Set<Annotation> providerQualifiers = new HashSet<>(); + + /** + * A key for a claim,injection site type pair + */ + public static class ClaimIPType implements Comparable<ClaimIPType> { + public ClaimIPType(String claimName, Type ipType) { + this.claimName = claimName; + this.ipType = ipType; + } + + /** + * Order the @Claim ClaimValue<T> on the @Claim.value and then T type name + * + * @param o - ClaimIP to compare to + * @return the ordering of this claim relative to o + */ + @Override + public int compareTo(ClaimIPType o) { + int compareTo = claimName.compareTo(o.claimName); + if (compareTo == 0) { + compareTo = ipType.getTypeName().compareTo(o.ipType.getTypeName()); + } + return compareTo; + } + + private String claimName; + + private Type ipType; + } + + /** + * The representation of an @Claim annotated injection site + */ + public static class ClaimIP { + /** + * Create a ClaimIP from the injection site information + * + * @param matchType - the outer type of the injection site + * @param valueType - the parameterized type of the injection site + * @param isOptional - is the injection site an Optional + * @param claim - the Claim qualifier + */ + public ClaimIP(Type matchType, Type valueType, boolean isOptional, Claim claim) { + this.matchType = matchType; + this.valueType = valueType; + this.claim = claim; + } + + public Type getMatchType() { + return matchType; + } + + public String getClaimName() { + return claim.standard() == Claims.UNKNOWN ? claim.value() : claim.standard().name(); + } + + public Claim getClaim() { + return claim; + } + + public Type getValueType() { + return valueType; + } + + public boolean isOptional() { + return isOptional; + } + + public boolean isProviderSite() { + return isProviderSite; + } + + public void setProviderSite(boolean providerSite) { + this.isProviderSite = providerSite; + } + + public boolean isNonStandard() { + return isNonStandard; + } + + public void setNonStandard(boolean nonStandard) { + isNonStandard = nonStandard; + } + + public boolean isJsonValue() { + return isJsonValue; + } + + public void setJsonValue(boolean jsonValue) { + isJsonValue = jsonValue; + } + + public Set<InjectionPoint> getInjectionPoints() { + return injectionPoints; + } + + @Override + public String toString() { + return "ClaimIP{" + + "type=" + matchType + + ", claim=" + claim + + ", ips=" + injectionPoints + + '}'; + } + + /** + * The injection site value type + */ + private Type matchType; + + /** + * The actual type of of the ParameterizedType matchType + */ + private Type valueType; + + /** + * Is valueType actually wrapped in an Optional + */ + private boolean isOptional; + + private boolean isProviderSite; + + private boolean isNonStandard; + + private boolean isJsonValue; + + /** + * The injection site @Claim annotation value + */ + private Claim claim; + + /** + * The location that share the @Claim/type combination + */ + private HashSet<InjectionPoint> injectionPoints = new HashSet<>(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java new file mode 100644 index 0000000..f267437 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/MPJWTProducer.java @@ -0,0 +1,196 @@ +/* + * 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 org.eclipse.microprofile.jwt.JsonWebToken; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.context.RequestScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Produces; +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A class that tracks the current validated MP-JWT and associated JsonWebToken via a thread + * local to provide a @RequestScoped JsonWebToken producer method. + * <p> + * It also provides utility methods for access the current JsonWebToken claim values. + */ +@ApplicationScoped +public class MPJWTProducer { + private static Logger log = Logger.getLogger(MPJWTProducer.class.getName()); + private static final String TMP = "tmp"; + private static ThreadLocal<JsonWebToken> currentPrincipal = new ThreadLocal<>(); + + public static void setJWTPrincipal(JsonWebToken principal) { + currentPrincipal.set(principal); + } + + public static JsonWebToken getJWTPrincpal() { + return currentPrincipal.get(); + } + + @PostConstruct + void init() { + log.fine("MPJWTProducer initialized"); + } + + void observeRequestInitialized(@Observes @Initialized(RequestScoped.class) Object event) { + log.finest(String.format("observeRequestInitialized, event=%s", event)); + } + + void observeRequestDestroyed(@Observes @Destroyed(RequestScoped.class) Object event) { + log.finest(String.format("observeRequestDestroyed, event=%s", event)); + } + + /** + * The @RequestScoped producer method for the current JsonWebToken + * + * @return + */ + @Produces + @RequestScoped + JsonWebToken currentPrincipalOrNull() { + return currentPrincipal.get(); + } + + /** + * A utility method for accessing a claim from the current JsonWebToken as a ClaimValue<Optional<T>> object. + * + * @param name - name of the claim + * @param <T> expected actual type of the claim + * @return the claim value wrapper object + */ + static <T> ClaimValue<Optional<T>> generalClaimValueProducer(String name) { + ClaimValueWrapper<Optional<T>> wrapper = new ClaimValueWrapper<>(name); + T value = getValue(name, false); + Optional<T> optValue = Optional.ofNullable(value); + wrapper.setValue(optValue); + return wrapper; + } + + /** + * Return the indicated claim value as a JsonValue + * + * @param name - name of the claim + * @return a JsonValue wrapper + */ + static JsonValue generalJsonValueProducer(String name) { + Object value = getValue(name, false); + JsonValue jsonValue = wrapValue(value); + return jsonValue; + } + + public static <T> T getValue(String name, boolean isOptional) { + JsonWebToken jwt = getJWTPrincpal(); + if (jwt == null) { + log.fine(String.format("getValue(%s), null JsonWebToken", name)); + return null; + } + + Optional<T> claimValue = jwt.claim(name); + if (!isOptional && !claimValue.isPresent()) { + log.fine(String.format("Failed to find Claim for: %s", name)); + } + log.fine(String.format("getValue(%s), isOptional=%s, claimValue=%s", name, isOptional, claimValue)); + return claimValue.orElse(null); + } + + static JsonObject replaceMap(Map<String, Object> map) { + JsonObjectBuilder builder = Json.createObjectBuilder(); + for (Map.Entry<String, Object> entry : map.entrySet()) { + Object entryValue = entry.getValue(); + if (entryValue instanceof Map) { + JsonObject entryJsonObject = replaceMap((Map<String, Object>) entryValue); + builder.add(entry.getKey(), entryJsonObject); + } else if (entryValue instanceof List) { + JsonArray array = (JsonArray) wrapValue(entryValue); + builder.add(entry.getKey(), array); + } else if (entryValue instanceof Long || entryValue instanceof Integer) { + long lvalue = ((Number) entryValue).longValue(); + builder.add(entry.getKey(), lvalue); + } else if (entryValue instanceof Double || entryValue instanceof Float) { + double dvalue = ((Number) entryValue).doubleValue(); + builder.add(entry.getKey(), dvalue); + } else if (entryValue instanceof Boolean) { + boolean flag = ((Boolean) entryValue).booleanValue(); + builder.add(entry.getKey(), flag); + } else if (entryValue instanceof String) { + builder.add(entry.getKey(), entryValue.toString()); + } + } + return builder.build(); + } + + static JsonValue wrapValue(Object value) { + JsonValue jsonValue = null; + if (value instanceof JsonValue) { + // This may already be a JsonValue + jsonValue = (JsonValue) value; + } else if (value instanceof String) { + jsonValue = Json.createObjectBuilder() + .add(TMP, value.toString()) + .build() + .getJsonString(TMP); + } else if (value instanceof Number) { + 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) { + Boolean flag = (Boolean) value; + jsonValue = flag ? JsonValue.TRUE : JsonValue.FALSE; + } else if (value instanceof Collection) { + JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + Collection list = (Collection) value; + for (Object element : list) { + if (element instanceof String) { + arrayBuilder.add(element.toString()); + } else { + JsonValue jvalue = wrapValue(element); + arrayBuilder.add(jvalue); + } + } + jsonValue = arrayBuilder.build(); + } else if (value instanceof Map) { + jsonValue = replaceMap((Map) value); + } + return jsonValue; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/RawClaimTypeProducer.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/RawClaimTypeProducer.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/RawClaimTypeProducer.java new file mode 100644 index 0000000..1ab817e --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/cdi/RawClaimTypeProducer.java @@ -0,0 +1,69 @@ +/* + * 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 javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Named; +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * + */ +public class RawClaimTypeProducer { + private static Logger log = Logger.getLogger(RawClaimTypeProducer.class.getName()); + + @Produces + @Claim("") + @Named("RawClaimTypeProducer#getValue") + public Object getValue(InjectionPoint ip) { + log.fine(String.format("getValue(%s)", ip)); + String name = getName(ip); + ClaimValue<Optional<Object>> cv = MPJWTProducer.generalClaimValueProducer(name); + Optional<Object> value = cv.getValue(); + Object returnValue = value.orElse(null); + return returnValue; + } + + @Produces + @Claim("") + @Named("RawClaimTypeProducer#getOptionalValue") + public Optional getOptionalValue(InjectionPoint ip) { + log.fine(String.format("getOptionalValue(%s)", ip)); + String name = getName(ip); + ClaimValue<Optional<Object>> cv = MPJWTProducer.generalClaimValueProducer(name); + Optional<Object> value = cv.getValue(); + return value; + } + + String getName(InjectionPoint ip) { + String name = null; + for (Annotation ann : ip.getQualifiers()) { + if (ann instanceof Claim) { + Claim claim = (Claim) ann; + name = claim.standard() == Claims.UNKNOWN ? claim.value() : claim.standard().name(); + } + } + return name; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java new file mode 100644 index 0000000..ef8c0b0 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfo.java @@ -0,0 +1,66 @@ +/* + * 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(RSAPublicKey signerKey, String issuedBy) { + this.signerKey = signerKey; + this.issuedBy = issuedBy; + } + + public JWTAuthContextInfo(JWTAuthContextInfo orig) { + this.signerKey = orig.signerKey; + this.issuedBy = orig.issuedBy; + this.expGracePeriodSecs = orig.expGracePeriodSecs; + } + + public RSAPublicKey getSignerKey() { + return signerKey; + } + + public void setSignerKey(RSAPublicKey signerKey) { + this.signerKey = signerKey; + } + + public String getIssuedBy() { + return issuedBy; + } + + public void setIssuedBy(String issuedBy) { + this.issuedBy = issuedBy; + } + + public int getExpGracePeriodSecs() { + return expGracePeriodSecs; + } + + public void setExpGracePeriodSecs(int expGracePeriodSecs) { + this.expGracePeriodSecs = expGracePeriodSecs; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthContextInfoProvider.java new file mode 100644 index 0000000..b810fcf --- /dev/null +++ b/tck/mp-jwt-embedded/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 org.apache.tomee.microprofile.jwt.KeyUtils; + +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Produces; +import java.security.interfaces.RSAPublicKey; +import java.util.Optional; + +@Dependent +public class JWTAuthContextInfoProvider { + + @Produces + Optional<JWTAuthContextInfo> getOptionalContextInfo() { + JWTAuthContextInfo contextInfo = new JWTAuthContextInfo(); + + // todo use MP Config to load the configuration + contextInfo.setIssuedBy("https://server.example.com"); + RSAPublicKey pk = null; + try { + pk = (RSAPublicKey) KeyUtils.decodePublicKey("-----BEGIN RSA PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq\n" + + "Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR\n" + + "TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e\n" + + "UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9\n" + + "AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn\n" + + "sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x\n" + + "nQIDAQAB\n" + + "-----END RSA PUBLIC KEY-----\n"); + + } catch (final Exception e) { + e.printStackTrace(); + // todo better handling + throw new RuntimeException(e); + } + contextInfo.setSignerKey(pk); + + return Optional.of(contextInfo); + } + + @Produces + JWTAuthContextInfo getContextInfo() { + return getOptionalContextInfo().get(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java new file mode 100644 index 0000000..120058d --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipal.java @@ -0,0 +1,334 @@ +/* + * 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 Logger logger = Logger.getLogger(DefaultJWTCallerPrincipal.class.getName()); + private String jwt; + private String type; + private 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(String jwt, String type, JwtClaims claimsSet, String name) { + super(name); + this.jwt = jwt; + this.type = type; + this.claimsSet = claimsSet; + fixJoseTypes(); + } + + @Override + public Set<String> getAudience() { + Set<String> audSet = new HashSet<>(); + try { + List<String> audList = claimsSet.getStringListClaimValue("aud"); + if (audList != null) { + audSet.addAll(audList); + } + } catch (MalformedClaimException e) { + try { + String aud = claimsSet.getStringClaimValue("aud"); + audSet.add(aud); + } catch (MalformedClaimException e1) { + } + } + return audSet; + } + + @Override + public Set<String> getGroups() { + HashSet<String> groups = new HashSet<>(); + try { + List<String> globalGroups = claimsSet.getStringListClaimValue("groups"); + if (globalGroups != null) { + groups.addAll(globalGroups); + } + } catch (MalformedClaimException e) { + e.printStackTrace(); + } + return groups; + } + + + @Override + public Set<String> getClaimNames() { + return new HashSet<>(claimsSet.getClaimNames()); + } + + @Override + public Object getClaim(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 (MalformedClaimException 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(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") + '\''; + 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 + Set<String> customClaimNames = filterCustomClaimNames(claimsSet.getClaimNames()); + for (String name : customClaimNames) { + Object claimValue = claimsSet.getClaimValue(name); + Class claimType = claimValue.getClass(); + 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(Collection<String> claimNames) { + 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(String name) { + try { + Map<String, Object> map = claimsSet.getClaimValue(name, Map.class); + JsonObject jsonObject = replaceMap(map); + claimsSet.setClaim(name, jsonObject); + } catch (MalformedClaimException e) { + logger.log(Level.WARNING, "replaceMap failure for: " + name, e); + } + } + + private JsonObject replaceMap(Map<String, Object> map) { + JsonObjectBuilder builder = Json.createObjectBuilder(); + for (Map.Entry<String, Object> entry : map.entrySet()) { + Object entryValue = entry.getValue(); + if (entryValue instanceof Map) { + JsonObject entryJsonObject = replaceMap((Map<String, Object>) entryValue); + builder.add(entry.getKey(), entryJsonObject); + } else if (entryValue instanceof List) { + JsonArray array = (JsonArray) wrapValue(entryValue); + builder.add(entry.getKey(), array); + } else if (entryValue instanceof Long || entryValue instanceof Integer) { + long lvalue = ((Number) entryValue).longValue(); + builder.add(entry.getKey(), lvalue); + } else if (entryValue instanceof Double || entryValue instanceof Float) { + double dvalue = ((Number) entryValue).doubleValue(); + builder.add(entry.getKey(), dvalue); + } else if (entryValue instanceof Boolean) { + 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(Object value) { + JsonValue jsonValue = null; + if (value instanceof Number) { + 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) { + Boolean flag = (Boolean) value; + jsonValue = flag ? JsonValue.TRUE : JsonValue.FALSE; + } else if (value instanceof List) { + JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); + 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(String name) { + try { + List list = claimsSet.getClaimValue(name, List.class); + JsonArray array = (JsonArray) wrapValue(list); + claimsSet.setClaim(name, array); + } catch (MalformedClaimException e) { + logger.log(Level.WARNING, "replaceList failure for: " + name, e); + } + } + + private void replaceNumber(String name) { + try { + Number number = claimsSet.getClaimValue(name, Number.class); + JsonNumber jsonNumber = (JsonNumber) wrapValue(number); + claimsSet.setClaim(name, jsonNumber); + } catch (MalformedClaimException e) { + logger.log(Level.WARNING, "replaceNumber failure for: " + name, e); + } + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipalFactory.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipalFactory.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipalFactory.java new file mode 100644 index 0000000..a420dde --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/DefaultJWTCallerPrincipalFactory.java @@ -0,0 +1,88 @@ +/* + * 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.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo; +import org.apache.tomee.microprofile.jwt.ParseException; +import org.eclipse.microprofile.jwt.Claims; +import org.jose4j.jwa.AlgorithmConstraints; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.NumericDate; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.jwt.consumer.JwtContext; + +/** + * A default implementation of the abstract JWTCallerPrincipalFactory that uses the Keycloak token parsing classes. + */ +public class DefaultJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory { + + /** + * Tries to load the JWTAuthContextInfo from CDI if the class level authContextInfo has not been set. + */ + public DefaultJWTCallerPrincipalFactory() { + } + + @Override + public JWTCallerPrincipal parse(final String token, final JWTAuthContextInfo authContextInfo) throws ParseException { + JWTCallerPrincipal principal = null; + + try { + JwtConsumerBuilder builder = new JwtConsumerBuilder() + .setRequireExpirationTime() + .setRequireSubject() + .setSkipDefaultAudienceValidation() + .setExpectedIssuer(authContextInfo.getIssuedBy()) + .setVerificationKey(authContextInfo.getSignerKey()) + .setJwsAlgorithmConstraints( + new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST, + AlgorithmIdentifiers.RSA_USING_SHA256)); + if (authContextInfo.getExpGracePeriodSecs() > 0) { + builder.setAllowedClockSkewInSeconds(authContextInfo.getExpGracePeriodSecs()); + } else { + builder.setEvaluationTime(NumericDate.fromSeconds(0)); + } + + JwtConsumer jwtConsumer = builder.build(); + JwtContext jwtContext = jwtConsumer.process(token); + String type = jwtContext.getJoseObjects().get(0).getHeader("typ"); + // Validate the JWT and process it to the Claims + jwtConsumer.processContext(jwtContext); + JwtClaims claimsSet = jwtContext.getJwtClaims(); + + // We have to determine the unique name to use as the principal name. It comes from upn, preferred_username, sub in that order + String principalName = claimsSet.getClaimValue("upn", String.class); + if (principalName == null) { + principalName = claimsSet.getClaimValue("preferred_username", String.class); + if (principalName == null) { + principalName = claimsSet.getSubject(); + } + } + claimsSet.setClaim(Claims.raw_token.name(), token); + principal = new DefaultJWTCallerPrincipal(token, type, claimsSet, principalName); + } catch (InvalidJwtException e) { + throw new ParseException("Failed to verify token", e); + } catch (MalformedClaimException e) { + throw new ParseException("Failed to verify token claims", e); + } + + return principal; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipal.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipal.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipal.java new file mode 100644 index 0000000..26d9406 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipal.java @@ -0,0 +1,59 @@ +/* + * 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.JsonWebToken; + +import java.util.Optional; + +/** + * An abstract CallerPrincipal implementation that provides access to the JWT claims that are required by + * the microprofile token. + */ +public abstract class JWTCallerPrincipal implements JsonWebToken { + + private String name; + + /** + * Create a JWTCallerPrincipal with the caller's name + * + * @param name - caller's name + */ + public JWTCallerPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + /** + * Generate a human readable version of the caller principal and associated JWT. + * + * @param showAll - should all claims associated with the JWT be displayed or should only those defined in the + * JsonWebToken interface be displayed. + * @return human readable presentation of the caller principal and associated JWT. + */ + public abstract String toString(boolean showAll); + + public <T> Optional<T> claim(final String claimName) { + final T claim = (T) getClaim(claimName); + return Optional.ofNullable(claim); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipalFactory.java ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipalFactory.java b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipalFactory.java new file mode 100644 index 0000000..3aec7f0 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/java/org/apache/tomee/microprofile/jwt/principal/JWTCallerPrincipalFactory.java @@ -0,0 +1,127 @@ +/* + * 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.apache.tomee.microprofile.jwt.config.JWTAuthContextInfo; +import org.apache.tomee.microprofile.jwt.ParseException; + +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ServiceLoader; + +/** + * The factory class that provides the token string to JWTCallerPrincipal parsing for a given implementation. + */ +public abstract class JWTCallerPrincipalFactory { + private static JWTCallerPrincipalFactory instance; + + /** + * Obtain the JWTCallerPrincipalFactory that has been set or by using the ServiceLoader pattern. + * + * @return the factory instance + * @see #setInstance(JWTCallerPrincipalFactory) + */ + public static JWTCallerPrincipalFactory instance() { + if (instance == null) { + synchronized (JWTCallerPrincipalFactory.class) { + if (instance != null) { + return instance; + } + + ClassLoader cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { + @Override + public ClassLoader run() { + return Thread.currentThread().getContextClassLoader(); + } + }); + if (cl == null) { + cl = JWTCallerPrincipalFactory.class.getClassLoader(); + } + + JWTCallerPrincipalFactory newInstance = loadSpi(cl); + + if (newInstance == null && cl != JWTCallerPrincipalFactory.class.getClassLoader()) { + cl = JWTCallerPrincipalFactory.class.getClassLoader(); + newInstance = loadSpi(cl); + } + if (newInstance == null) { + throw new IllegalStateException("No JWTCallerPrincipalFactory implementation found!"); + } + + instance = newInstance; + } + } + + return instance; + } + + /** + * Look for a JWTCallerPrincipalFactory service implementation using the ServiceLoader. + * + * @param cl - the ClassLoader to pass into the {@link ServiceLoader#load(Class, ClassLoader)} method. + * @return the JWTCallerPrincipalFactory if found, null otherwise + */ + private static JWTCallerPrincipalFactory loadSpi(ClassLoader cl) { + if (cl == null) { + return null; + } + + // start from the root CL and go back down to the TCCL + JWTCallerPrincipalFactory instance = loadSpi(cl.getParent()); + + if (instance == null) { + ServiceLoader<JWTCallerPrincipalFactory> sl = ServiceLoader.load(JWTCallerPrincipalFactory.class, cl); + URL u = cl.getResource("/META-INF/services/org.apache.tomee.microprofile.jwt.JWTCallerPrincipalFactory"); + System.out.printf("JWTCallerPrincipalFactory, cl=%s, u=%s, sl=%s\n", cl, u, sl); + try { + for (JWTCallerPrincipalFactory spi : sl) { + if (instance != null) { + throw new IllegalStateException( + "Multiple JWTCallerPrincipalFactory implementations found: " + + spi.getClass().getName() + " and " + + instance.getClass().getName()); + } else { + System.out.printf("sl=%s, loaded=%s\n", sl, spi); + instance = spi; + } + } + } catch (Throwable e) { + System.err.printf("Warning: %s\n", e.getMessage()); + } + } + return instance; + } + + /** + * Set the instance. It is used by OSGi environment where service loader pattern is not supported. + * + * @param resolver the instance to use. + */ + public static void setInstance(JWTCallerPrincipalFactory resolver) { + instance = resolver; + } + + /** + * Parse the given bearer token string into a JWTCallerPrincipal instance. + * + * @param token - the bearer token provided for authorization + * @return A JWTCallerPrincipal representation for the token. + * @throws ParseException on parse or verification failure. + */ + public abstract JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/tck/mp-jwt-embedded/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension index 5e3bccc..d5eea47 100644 --- a/tck/mp-jwt-embedded/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension +++ b/tck/mp-jwt-embedded/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -1 +1 @@ -org.apache.tomee.microprofile.jwt.MPJWTCDIExtension \ No newline at end of file +org.apache.tomee.microprofile.jwt.cdi.MPJWTCDIExtension \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.JWTCallerPrincipalFactory ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.JWTCallerPrincipalFactory b/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.JWTCallerPrincipalFactory deleted file mode 100644 index 67f39db..0000000 --- a/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.JWTCallerPrincipalFactory +++ /dev/null @@ -1 +0,0 @@ -org.apache.tomee.microprofile.jwt.DefaultJWTCallerPrincipalFactory \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tomee/blob/c5964e07/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.principal.JWTCallerPrincipalFactory ---------------------------------------------------------------------- diff --git a/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.principal.JWTCallerPrincipalFactory b/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.principal.JWTCallerPrincipalFactory new file mode 100644 index 0000000..21c9831 --- /dev/null +++ b/tck/mp-jwt-embedded/src/main/resources/META-INF/services/org.apache.tomee.microprofile.jwt.principal.JWTCallerPrincipalFactory @@ -0,0 +1 @@ +org.apache.tomee.microprofile.jwt.principal.DefaultJWTCallerPrincipalFactory \ No newline at end of file