All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arangodb.shaded.vertx.ext.auth.impl.jose.JWT Maven / Gradle / Ivy

There is a newer version: 7.8.0
Show newest version
/*
 * Copyright 2015 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */
package com.arangodb.shaded.vertx.ext.auth.impl.jose;

import com.arangodb.shaded.vertx.core.buffer.Buffer;
import com.arangodb.shaded.vertx.core.impl.logging.Logger;
import com.arangodb.shaded.vertx.core.impl.logging.LoggerFactory;
import com.arangodb.shaded.vertx.core.json.JsonArray;
import com.arangodb.shaded.vertx.core.json.JsonObject;
import com.arangodb.shaded.vertx.ext.auth.JWTOptions;
import com.arangodb.shaded.vertx.ext.auth.NoSuchKeyIdException;
import com.arangodb.shaded.vertx.ext.auth.impl.CertificateHelper;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static com.arangodb.shaded.vertx.ext.auth.impl.Codec.*;

/**
 * JWT and JWS implementation draft-ietf-oauth-json-web-token-32.
 *
 * @author Paulo Lopes
 */
public final class JWT {

  private static final Logger LOG = LoggerFactory.getLogger(JWT.class);

  // simple random as its value is just to create entropy
  private static final Random RND = new Random();

  private static final Charset UTF8 = StandardCharsets.UTF_8;

  private boolean allowEmbeddedKey = false;
  private X509Certificate rootCA;
  private MessageDigest nonceDigest;

  // keep 2 maps (1 for sing, 1 for verify) this simplifies the lookups
  private final Map> SIGN = new ConcurrentHashMap<>();
  private final Map> VERIFY = new ConcurrentHashMap<>();

  /**
   * Adds a JSON Web Key (rfc7517) to the signature maps.
   *
   * @param jwk a JSON Web Key
   * @return self
   */
  public JWT addJWK(JWK jwk) {

    if (jwk.use() == null || "sig".equals(jwk.use())) {
      List current;
      synchronized (this) {
        if (jwk.mac() != null || jwk.publicKey() != null) {
          current = VERIFY.computeIfAbsent(jwk.getAlgorithm(), k -> new ArrayList<>());
          addJWK(current, jwk);
        }
        if (jwk.mac() != null || jwk.privateKey() != null) {
          current = SIGN.computeIfAbsent(jwk.getAlgorithm(), k -> new ArrayList<>());
          addJWK(current, jwk);
        }
      }
    } else {
      LOG.warn("JWK skipped: use: sig != " + jwk.use());
    }

    return this;
  }

  /**
   * Enable/Disable support for embedded keys. Default {@code false}.
   * 

* By default this is disabled as it could be used as an attack vector to the application. A malicious user could * generate a self signed certificate and embed the public certificate on the token, which would always pass the * validation. *

* Users of this feature should regardless of the validation status, ensure that the chain is valid by adding a * well known root certificate (that has been previously agreed with the server). * * @param allowEmbeddedKey when true embedded keys are used to check the signature. * @return fluent self. */ public JWT allowEmbeddedKey(boolean allowEmbeddedKey) { this.allowEmbeddedKey = allowEmbeddedKey; return this; } /** * Set the root CA certificate for the embedded keys. When handling tokens with embedded keys, certificate chains * shall be verified against the provided root CA to ensure a web of trust. * * @param rootCA base64-encoded (Section 4 of [RFC4648] -- not base64url-encoded) DER [ITU.X690.2008] PKIX * certificate value. * @return fluent self. */ public JWT embeddedKeyRootCA(String rootCA) throws CertificateException { this.rootCA = JWS.parseX5c(base64Decode(rootCA)); this.allowEmbeddedKey = true; return this; } public JWT nonceAlgorithm(String alg) { if (alg == null) { nonceDigest = null; } else { try { nonceDigest = MessageDigest.getInstance(alg); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException(e); } } return this; } private void addJWK(List current, JWK jwk) { boolean replaced = false; for (int i = 0; i < current.size(); i++) { if (current.get(i).jwk().label().equals(jwk.label())) { // replace LOG.info("replacing JWK with label " + jwk.label()); current.set(i, new JWS(jwk)); replaced = true; break; } } if (!replaced) { // non existent, add it! current.add(new JWS(jwk)); } } public static JsonObject parse(final byte[] token) { return parse(new String(token, UTF8)); } public static JsonObject parse(final String token) { String[] segments = token.split("\\."); if (segments.length < 2 || segments.length > 3) { throw new RuntimeException("Not enough or too many segments [" + segments.length + "]"); } // All segment should be base64 String headerSeg = segments[0]; String payloadSeg = segments[1]; String signatureSeg = segments.length == 2 ? null : segments[2]; // base64 decode and parse JSON JsonObject header = new JsonObject(new String(base64UrlDecode(headerSeg), UTF8)); JsonObject payload = new JsonObject(new String(base64UrlDecode(payloadSeg), UTF8)); return new JsonObject() .put("header", header) .put("payload", payload) .put("signatureBase", (headerSeg + "." + payloadSeg)) .put("signature", signatureSeg); } public JsonObject decode(final String token) { return decode(token, false, null); } public JsonObject decode(final String token, List crls) { return decode(token, false, crls); } public JsonObject decode(final String token, boolean full, List crls) { // lock the secure state String[] segments = token.split("\\."); if (segments.length < 2) { throw new IllegalStateException("Invalid format for JWT"); } // All segment should be base64 String headerSeg = segments[0]; String payloadSeg = segments[1]; String signatureSeg = segments.length == 3 ? segments[2] : null; // empty signature is never allowed if ("".equals(signatureSeg)) { throw new IllegalStateException("Signature is required"); } // base64 decode and parse JSON JsonObject header = new JsonObject(Buffer.buffer(base64UrlDecode(headerSeg))); final boolean unsecure = isUnsecure(); if (unsecure) { // if there isn't a certificate chain in the header, we are dealing with a strictly // unsecure mode validation. In this case the number of segments must be 2 // if there is a certificate chain, we allow it to proceed and later we will assert // against this chain if (!allowEmbeddedKey && segments.length != 2) { throw new IllegalStateException("JWT is in unsecured mode but token is signed."); } } else { if (!allowEmbeddedKey && segments.length != 3) { throw new IllegalStateException("JWT is in secure mode but token is not signed."); } } JsonObject payload = new JsonObject(Buffer.buffer(base64UrlDecode(payloadSeg))); String alg = header.getString("alg"); // if we only allow secure alg, then none is not a valid option if (!unsecure && "none".equals(alg)) { throw new IllegalStateException("Algorithm \"none\" not allowed"); } // handle the x5c case, only in allowEmbeddedKey mode if (allowEmbeddedKey && header.containsKey("x5c")) { // if signatureSeg is null fail if (signatureSeg == null) { throw new IllegalStateException("missing signature segment"); } try { JsonArray chain = header.getJsonArray("x5c"); List certChain = new ArrayList<>(); if (chain == null || chain.size() == 0) { throw new IllegalStateException("x5c chain is null or empty"); } for (int i = 0; i < chain.size(); i++) { // "x5c" (X.509 Certificate Chain) Header Parameter // https://tools.ietf.org/html/rfc7515#section-4.1.6 // states: // Each string in the array is a base64-encoded (Section 4 of [RFC4648] -- not base64url-encoded) DER // [ITU.X690.2008] PKIX certificate value. certChain.add(JWS.parseX5c(base64Decode(chain.getString(i)))); } if (rootCA != null) { certChain.add(rootCA); CertificateHelper.checkValidity(certChain, true, crls); } else { CertificateHelper.checkValidity(certChain, false, crls); } if (JWS.verifySignature(alg, certChain.get(0), base64UrlDecode(signatureSeg), (headerSeg + "." + payloadSeg).getBytes(UTF8))) { // ok return full ? new JsonObject().put("header", header).put("payload", payload) : payload; } else { throw new RuntimeException("Signature verification failed"); } } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | InvalidAlgorithmParameterException | NoSuchProviderException e) { throw new RuntimeException("Signature verification failed", e); } } // verify signature. `sign` will return base64 string. if (!unsecure) { List signatures = VERIFY.get(alg); if (signatures == null || signatures.size() == 0) { throw new NoSuchKeyIdException(alg); } // if signatureSeg is null fail if (signatureSeg == null) { throw new IllegalStateException("missing signature segment"); } byte[] payloadInput = base64UrlDecode(signatureSeg); if (nonceDigest != null && header.containsKey("nonce")) { // this is an Azure Graph extension, a nonce is added to the token // after the serialization. The original value is the digest of the // post value. synchronized (this) { nonceDigest.reset(); header.put("nonce", base64UrlEncode(nonceDigest.digest(header.getString("nonce").getBytes(StandardCharsets.UTF_8)))); headerSeg = base64UrlEncode(header.encode().getBytes(StandardCharsets.UTF_8)); } } byte[] signingInput = (headerSeg + "." + payloadSeg).getBytes(UTF8); String kid = header.getString("kid"); boolean hasKey = false; for (JWS jws : signatures) { // if a token has a kid and it doesn't match the crypto id skip it if (kid != null && jws.jwk().getId() != null && !kid.equals(jws.jwk().getId())) { continue; } // signal that this object crypto's list has the required key hasKey = true; if (jws.verify(payloadInput, signingInput)) { return full ? new JsonObject().put("header", header).put("payload", payload) : payload; } } if (hasKey) { throw new RuntimeException("Signature verification failed"); } else { throw new NoSuchKeyIdException(alg, kid); } } return full ? new JsonObject().put("header", header).put("payload", payload) : payload; } public String sign(JsonObject payload, JWTOptions options) { final boolean unsecure = isUnsecure(); final String algorithm = options.getAlgorithm(); // if we only allow secure alg, then none is not a valid option if (!unsecure && "none".equals(algorithm)) { throw new IllegalStateException("Algorithm \"none\" not allowed"); } final JWS jws; final String kid; if (!unsecure) { List signatures = SIGN.get(algorithm); if (signatures == null || signatures.size() == 0) { throw new RuntimeException("Algorithm not supported/allowed: " + algorithm); } // lock the crypto implementation jws = signatures.get(signatures.size() == 1 ? 0 : RND.nextInt(signatures.size())); kid = jws.jwk().getId(); } else { jws = null; kid = null; } // header, typ is fixed value. JsonObject header = new JsonObject() .mergeIn(options.getHeader()) .put("typ", "JWT") .put("alg", algorithm); // add kid if present if (kid != null) { header.put("kid", kid); } // NumericDate is a number is seconds since 1st Jan 1970 in UTC long timestamp = System.currentTimeMillis() / 1000; if (!options.isNoTimestamp()) { payload.put("iat", payload.getValue("iat", timestamp)); } if (options.getExpiresInSeconds() > 0) { payload.put("exp", timestamp + options.getExpiresInSeconds()); } if (options.getAudience() != null && options.getAudience().size() >= 1) { if (options.getAudience().size() > 1) { payload.put("aud", new JsonArray(options.getAudience())); } else { payload.put("aud", options.getAudience().get(0)); } } if (options.getIssuer() != null) { payload.put("iss", options.getIssuer()); } if (options.getSubject() != null) { payload.put("sub", options.getSubject()); } // create segments, all segment should be base64 string String headerSegment = base64UrlEncode(header.encode().getBytes(StandardCharsets.UTF_8)); String payloadSegment = base64UrlEncode(payload.encode().getBytes(StandardCharsets.UTF_8)); if (!unsecure) { String signingInput = headerSegment + "." + payloadSegment; String signSegment = base64UrlEncode(jws.sign(signingInput.getBytes(UTF8))); return headerSegment + "." + payloadSegment + "." + signSegment; } else { return headerSegment + "." + payloadSegment; } } public boolean isUnsecure() { return VERIFY.size() == 0 && SIGN.size() == 0; } public Collection availableAlgorithms() { Set algorithms = new HashSet<>(); // the spec requires none to be always available algorithms.add("none"); algorithms.addAll(VERIFY.keySet()); algorithms.addAll(SIGN.keySet()); return algorithms; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy