001/* 002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.jwt; 020 021import static java.util.stream.Collectors.toMap; 022import static org.apache.commons.lang3.StringUtils.isBlank; 023 024import java.io.IOException; 025import java.time.Instant; 026import java.util.ArrayList; 027import java.util.Date; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031import java.util.Objects; 032 033import org.apache.commons.lang3.reflect.FieldUtils; 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.api.NuxeoPrincipal; 038import org.nuxeo.runtime.model.ComponentInstance; 039import org.nuxeo.runtime.model.DefaultComponent; 040import org.nuxeo.runtime.model.SimpleContributionRegistry; 041 042import com.auth0.jwt.JWT; 043import com.auth0.jwt.JWTCreator.Builder; 044import com.auth0.jwt.JWTVerifier; 045import com.auth0.jwt.algorithms.Algorithm; 046import com.auth0.jwt.exceptions.JWTCreationException; 047import com.auth0.jwt.exceptions.JWTVerificationException; 048import com.auth0.jwt.interfaces.DecodedJWT; 049import com.fasterxml.jackson.core.JsonParser; 050import com.fasterxml.jackson.core.type.TypeReference; 051import com.fasterxml.jackson.databind.JsonNode; 052import com.fasterxml.jackson.databind.ObjectMapper; 053 054/** 055 * The JSON Web Token Service implementation. 056 * 057 * @since 10.3 058 */ 059public class JWTServiceImpl extends DefaultComponent implements JWTService { 060 061 private static final Log log = LogFactory.getLog(JWTServiceImpl.class); 062 063 public static final String XP_CONFIGURATION = "configuration"; 064 065 public static final String NUXEO_ISSUER = "nuxeo"; 066 067 protected static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 068 069 protected static final TypeReference<Map<String, Object>> MAP_STRING_OBJECT = new TypeReference<Map<String, Object>>() { 070 }; 071 072 protected static class JWTServiceConfigurationRegistry 073 extends SimpleContributionRegistry<JWTServiceConfigurationDescriptor> { 074 075 protected static final String KEY = ""; // value doesn't matter as long as we use a fixed one 076 077 protected static final JWTServiceConfigurationDescriptor DEFAULT_CONTRIBUTION = new JWTServiceConfigurationDescriptor(); 078 079 @Override 080 public String getContributionId(JWTServiceConfigurationDescriptor contrib) { 081 return KEY; 082 } 083 084 @Override 085 public boolean isSupportingMerge() { 086 return true; 087 } 088 089 @Override 090 public JWTServiceConfigurationDescriptor clone(JWTServiceConfigurationDescriptor orig) { 091 return new JWTServiceConfigurationDescriptor(orig); 092 } 093 094 @Override 095 public void merge(JWTServiceConfigurationDescriptor src, JWTServiceConfigurationDescriptor dst) { 096 dst.merge(src); 097 } 098 099 public JWTServiceConfigurationDescriptor getContribution() { 100 JWTServiceConfigurationDescriptor contribution = getContribution(KEY); 101 if (contribution == null) { 102 contribution = DEFAULT_CONTRIBUTION; 103 } 104 return contribution; 105 } 106 } 107 108 protected final JWTServiceConfigurationRegistry registry = new JWTServiceConfigurationRegistry(); 109 110 @Override 111 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 112 if (XP_CONFIGURATION.equals(extensionPoint)) { 113 registry.addContribution((JWTServiceConfigurationDescriptor) contribution); 114 } else { 115 throw new NuxeoException("Unknown extension point: " + extensionPoint); 116 } 117 } 118 119 @Override 120 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 121 if (XP_CONFIGURATION.equals(extensionPoint)) { 122 registry.removeContribution((JWTServiceConfigurationDescriptor) contribution); 123 } 124 } 125 126 // -------------------- JWTService API -------------------- 127 128 @Override 129 public JWTBuilder newBuilder() { 130 return new JWTBuilderImpl(); 131 } 132 133 /** 134 * Implementation of {@link JWTBuilder} delegating to the auth0 JWT library. 135 * 136 * @since 10.3 137 */ 138 public class JWTBuilderImpl implements JWTBuilder { 139 140 public final Builder builder; 141 142 public JWTBuilderImpl() { 143 builder = JWT.create(); 144 // default Nuxeo issuer 145 builder.withIssuer(NUXEO_ISSUER); 146 // default to current principal as subject 147 String subject = NuxeoPrincipal.getCurrent().getActingUser(); 148 if (subject == null) { 149 throw new NuxeoException("No currently logged-in user"); 150 } 151 builder.withSubject(subject); 152 // default TTL 153 withTTL(0); 154 } 155 156 @Override 157 public JWTBuilderImpl withTTL(int ttlSeconds) { 158 if (ttlSeconds <= 0) { 159 ttlSeconds = getDefaultTTL(); 160 } 161 builder.withExpiresAt(Date.from(Instant.now().plusSeconds(ttlSeconds))); 162 return this; 163 } 164 165 @Override 166 public JWTBuilderImpl withClaim(String name, Object value) { 167 if (value instanceof Boolean) { 168 builder.withClaim(name, (Boolean) value); 169 } else if (value instanceof Date) { 170 builder.withClaim(name, (Date) value); 171 } else if (value instanceof Double) { 172 builder.withClaim(name, (Double) value); 173 } else if (value instanceof Integer) { 174 builder.withClaim(name, (Integer) value); 175 } else if (value instanceof Long) { 176 builder.withClaim(name, (Long) value); 177 } else if (value instanceof String) { 178 builder.withClaim(name, (String) value); 179 } else if (value instanceof Integer[]) { 180 builder.withArrayClaim(name, (Integer[]) value); 181 } else if (value instanceof Long[]) { 182 builder.withArrayClaim(name, (Long[]) value); 183 } else if (value instanceof String[]) { 184 builder.withArrayClaim(name, (String[]) value); 185 } else { 186 throw new NuxeoException("Unknown claim type: " + value); 187 } 188 return this; 189 } 190 191 @Override 192 public String build() { 193 try { 194 Algorithm algorithm = getAlgorithm(); 195 if (algorithm == null) { 196 throw new NuxeoException("JWTService secret not configured"); 197 } 198 return builder.sign(algorithm); 199 } catch (JWTCreationException e) { 200 throw new NuxeoException(e); 201 } 202 } 203 } 204 205 protected void builderWithClaim(Builder builder, String name, Object value) { 206 if (value instanceof Boolean) { 207 builder.withClaim(name, (Boolean) value); 208 } else if (value instanceof Date) { 209 builder.withClaim(name, (Date) value); 210 } else if (value instanceof Double) { 211 builder.withClaim(name, (Double) value); 212 } else if (value instanceof Integer) { 213 builder.withClaim(name, (Integer) value); 214 } else if (value instanceof Long) { 215 builder.withClaim(name, (Long) value); 216 } else if (value instanceof String) { 217 builder.withClaim(name, (String) value); 218 } else if (value instanceof Integer[]) { 219 builder.withArrayClaim(name, (Integer[]) value); 220 } else if (value instanceof Long[]) { 221 builder.withArrayClaim(name, (Long[]) value); 222 } else if (value instanceof String[]) { 223 builder.withArrayClaim(name, (String[]) value); 224 } else { 225 throw new NuxeoException("Unknown claim type: " + value); 226 } 227 } 228 229 @Override 230 public Map<String, Object> verifyToken(String token) { 231 Objects.requireNonNull(token); 232 Algorithm algorithm = getAlgorithm(); 233 if (algorithm == null) { 234 log.debug("secret not configured, cannot verify token"); 235 return null; // no secret 236 } 237 JWTVerifier verifier = JWT.require(algorithm).build(); 238 DecodedJWT jwt; 239 try { 240 jwt = verifier.verify(token); 241 } catch (JWTVerificationException e) { 242 if (log.isTraceEnabled()) { 243 log.trace("token verification failed: " + e.toString()); 244 } 245 return null; // invalid 246 } 247 Map<String, JsonNode> tree; 248 try { 249 Object payload = FieldUtils.readField(jwt, "payload", true); // com.auth0.jwt.impl.PayloadImpl 250 tree = (Map<String, JsonNode>) FieldUtils.readField(payload, "tree", true); 251 } catch (ReflectiveOperationException e) { 252 throw new NuxeoException(e); 253 } 254 return tree.entrySet().stream().collect(toMap(Entry::getKey, e -> nodeToValue(e.getValue()))); 255 } 256 257 /** 258 * Converts a {@link JsonNode} to a Java value. 259 */ 260 protected static Object nodeToValue(JsonNode node) { 261 if (node == null || node.isNull() || node.isMissingNode()) { 262 return null; 263 } else if (node.isObject()) { 264 try { 265 try (JsonParser parser = OBJECT_MAPPER.treeAsTokens(node)) { 266 return parser.readValueAs(MAP_STRING_OBJECT); 267 } 268 } catch (IOException e) { 269 throw new NuxeoException("Cannot map claim value to Map", e); 270 } 271 } else if (node.isArray()) { 272 List<Object> list = new ArrayList<>(); 273 for (JsonNode elem : node) { 274 try { 275 list.add(OBJECT_MAPPER.treeToValue(elem, Object.class)); 276 } catch (IOException e) { 277 throw new NuxeoException("Cannot map Claim array value to Object", e); 278 } 279 } 280 return list; 281 } else { 282 // Jackson doesn't seem to have an easy way to do this, other than checking each possible type 283 Object value; 284 try { 285 value = FieldUtils.readField(node, "_value", true); 286 } catch (ReflectiveOperationException e) { 287 log.warn("Cannot extract primitive value from JsonNode: " + node.getClass().getName()); 288 value = null; 289 } 290 if (value instanceof Integer) { 291 // normalize to Long for caller convenience 292 value = Long.valueOf(((Integer) value).longValue()); 293 } 294 return value; 295 } 296 } 297 298 protected int getDefaultTTL() { 299 return registry.getContribution().getDefaultTTL(); 300 } 301 302 protected Algorithm getAlgorithm() { 303 String secret = registry.getContribution().getSecret(); 304 if (isBlank(secret)) { 305 return null; 306 } 307 return Algorithm.HMAC512(secret); 308 } 309 310}