/* * Copyright 2016 Stormpath, Inc. * * Licensed 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 com.stormpath.sdk.convert; import com.stormpath.sdk.lang.Assert; import com.stormpath.sdk.lang.Collections; import com.stormpath.sdk.lang.Function; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; import java.util.Date; import java.util.Map; import java.util.UUID; /** * Converts a <code>Map<String,?></code> to a compact JWT string. * * <p>See the {@link #MapToJwtConverter(Map, Map, String, SignatureAlgorithm, Key) constructor JavaDoc} for usage * options.</p> * * <p>Instances of this class are immutable and thread-safe.</p> * * @since 1.3.0 */ public class MapToJwtConverter implements Function<Map<String, ?>, String> { private final Map<String,?> baseHeader; private final Map<String, ?> baseClaims; private final String valueClaimName; private final Long expirationSeconds; private final Long notBeforeSeconds; private final SignatureAlgorithm signatureAlgorithm; private final Key signingKey; /** * Creates a new instance based on the specified arguments. * * <h5>{@code baseHeader}</h5> * * <p>The optional (nullable) {@code baseHeader} argument allows you to supply any base header name/value pairs * that should be in the JWT before the JWT is constructed. Any header values required by JWT construction * will overwrite any identically named values in this map. For example, {@code iat} - 'issued at' will always * be the timestamp when the JWT is constructed, even if {@code baseHeader} contains an {@code iat} member.</p> * * <p>A common reason for setting this value would be, for example, to set the identifier of the signing key used * during signing (via the {@code kid} header). This allows a recipient of the JWT to look up the same signing * key based on the {@code kid} header so the recipient can verify the signature.</p> * * <h5>{@code baseClaims}</h5> * * <p>The optional (nullable) {@code baseClaims} argument allows you to supply any base claims you wish to add as * JWT claims <em>before</em> the function value's name/value pairs are applied as claims.</p> * * <p>For example, consider the following code with a {@code baseClaims} constructor argument:</p> * * <pre><code> * Map<String,Object> baseClaims = new HashMap<>(); * baseClaims.put("iss", "My Company"); * baseClaims.put("aud", "mywebapp.com"); * * MapToJwtConverter converter = new MapToJwtConverter(baseClaims, null, null, null); * </code></pre> * * <p>If you were to invoke this converter with a Map value:</p> * * <pre><code> * Map<String,Object> value = new HashMap<>(); * value.put("username", "jsmith"); * value.put("email", "jsmith@mailinator.com"); * * String jwt = converter.apply(value); * </code></pre> * * <p>The resulting JWT claims will be the set union of both maps and look like this:</p> * * <pre><code> * { * "iss": "My Company", * "aud": "webapp.com", * "username": "jsmith", * "email", "jsmith@mailinator.com" * } * </code></pre> * * <p>Any name/value pairs in the function value take precedence and will overwrite (replace) any identically * named pairs from the {@code baseClaims}.</p> * * <p>If you don't want the {@code value} name/value pairs to be set as common/top-level claims and instead want * them to be set as a single claim with nested pairs, specify the {@code valueClaimName} argument.</p> * * <p>A {@code null} {@code baseClaims} argument value indicates no base claims need to be represented in the * resulting JWT.</p> * * <h5>{@code valueClaimName}</h5> * * <p>By default, any name/value pairs in the Map value supplied to the {@link #apply(Map) apply} method will be * merged and potentially overwrite any pairs that might have been set in the {@code baseClaims}.</p> * * <p>If you don't want the function value's name/value pairs to be intermixed with any other JWT base claims, you * can set the {@code valueClaimName} argument and that will be used to set a single top-level JWT claim using * the entire Map value as the claim value.</p> * * <p>For example, consider the following code with a {@code valueClaimName} constructor argument:</p> * * <pre><code> * MapToJwtConverter converter = new MapToJwtConverter(null, "account", null, null, null); * </code></pre> * * <p>If you were to invoke this converter with a Map value:</p> * * <pre><code> * Map<String,Object> value = new HashMap<>(); * value.put("username", "jsmith"); * value.put("email", "jsmith@mailinator.com"); * * String jwt = converter.apply(value); * </code></pre> * * <p>The resulting JWT claims will include an {@code account} claim with the same value as the function * argument. The resulting JWT claims would look like this:</p> * * <pre><code> * { * "iat": "2016-12-15T19:58:55.272Z", * //other claims added by the JWT building process truncated for brevity... * "account": { * "username": "jsmith", * "email": "jsmith@mailinator.com" * } * } * </code></pre> * * <p>The entire map value is 'nested' under a claim name equal to the specified {@code valueClaimName} argument * value ("account" in this example).</p> * * <p>A {@code null} {@code valueClaimName} argument value indicates that any name/value pairs from the function * value should not be nested, and instead be represented as common/top-level JWT claims.</p> * * <h5>{@code signatureAlgorithm}</h5> * * <p>The JWT signature algorithm to use when signing the JWT with the specified {@code signingKey} argument.</p> * * <p>A {@code null} value indicates the JWT should not be signed at all. * <b>WARNING:</b> JWT values in most production environments should usually always be signed to ensure the JWT * cannot be manipulated after construction. Not signing JWTs usually leads to security risks.</p> * * <p>If {@code signatureAlgorithm} is specified (non-null), the {@code signingKey} argument must be specified as * well.</p> * * <h5>{@code signingKey}</h5> * * <p>The signing key to use when signing the JWT based on the specified {@code signatureAlgorithm} argument.</p> * * <p>If {@code signatureAlgorithm} is provided, the {@code signingKey} must be provided as well.</p> * * @param baseHeader any base name/value pairs to add to the JWT header before constructing the JWT, or * {@code null} if no base header pairs are desired. * @param baseClaims any base claims to add to the JWT before the function value is applied as JWT baseClaims, * or {@code null} if no base claims are desired. * @param valueClaimName the name of the JWT claim to represent/'wrap' the function value's name/value pairs, or * {@code null} if any value pairs should be added directly as common/top-level JWT claims. * @param signatureAlgorithm the signature algorithm to use when signing the key or {@code null} if the JWT should * not be signed. * @param signingKey the signing key to use with the specified {@code signatureAlgorithm} or {@code null} if the * JWT should not be signed. * @param expirationSeconds the number of seconds to add to the JWT's creation timestamp, the resulting value of * which will be used to set the {@code exp} Date claim. A {@code null} value indicates * that the {@code exp} claim will not be set. * @param notBeforeSeconds the number of seconds to add (or subtract) to the JWT's creation timestamp, the * resulting value of which will be used to set the {@code nbf} Date claim. A {@code null} * value indicates that the {@code nbf} claim will not be set. */ public MapToJwtConverter(Map<String,?> baseHeader, Map<String, ?> baseClaims, String valueClaimName, SignatureAlgorithm signatureAlgorithm, Key signingKey, Long expirationSeconds, Long notBeforeSeconds) { this.valueClaimName = valueClaimName; if (baseHeader == null) { this.baseHeader = java.util.Collections.emptyMap(); } else { this.baseHeader = baseHeader; } if (baseClaims == null) { this.baseClaims = java.util.Collections.emptyMap(); } else { this.baseClaims = baseClaims; } this.expirationSeconds = expirationSeconds; this.notBeforeSeconds = notBeforeSeconds; this.signatureAlgorithm = signatureAlgorithm; this.signingKey = signingKey; if (signatureAlgorithm != null) { Assert.notNull(signingKey, "A signing Key argument is required when specifying a SignatureAlgorithm."); } else if (signingKey != null) { String msg = "A SignatureAlgorithm argument is required when specifying a signing Key."; throw new IllegalArgumentException(msg); } } @SuppressWarnings("unchecked") @Override public String apply(Map<String, ?> value) { JwtBuilder builder = Jwts.builder(); if (!Collections.isEmpty(baseHeader)) { builder.setHeader((Map<String,Object>) baseHeader); } if (!Collections.isEmpty(baseClaims)) { builder.setClaims((Map<String, Object>) baseClaims); } if (!Collections.isEmpty(value)) { if (valueClaimName != null) { builder.claim(valueClaimName, value); } else { for (Map.Entry<String, ?> entry : value.entrySet()) { builder.claim(entry.getKey(), entry.getValue()); } } } Date now = new Date(); builder.setIssuedAt(now); long nowMillis = now.getTime(); if (this.expirationSeconds != null) { long expMillis = nowMillis + (expirationSeconds * 1000); Date exp = new Date(expMillis); builder.setExpiration(exp); } if (this.notBeforeSeconds != null) { long nbfMillis = nowMillis + (notBeforeSeconds * 1000); Date nbf = new Date(nbfMillis); builder.setNotBefore(nbf); } if (signatureAlgorithm != null) { Assert.notNull(signingKey, "Illegal state: signingKey cannot be null if signatureAlgorithm exists."); builder.signWith(signatureAlgorithm, signingKey); } return builder.compact(); } }