001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.auth.jwt;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.net.*;
023import java.security.*;
024import java.text.*;
025import java.time.*;
026import java.util.*;
027
028import org.apache.juneau.commons.inject.*;
029import org.apache.juneau.commons.utils.*;
030import org.apache.juneau.rest.server.auth.*;
031
032import com.nimbusds.jose.*;
033import com.nimbusds.jose.jwk.source.*;
034import com.nimbusds.jose.proc.*;
035import com.nimbusds.jwt.*;
036import com.nimbusds.jwt.proc.*;
037
038/**
039 * {@link TokenValidator} that validates signed JWTs against a JWKS-backed key source.
040 *
041 * <p>
042 * Designed to be paired with {@link BearerTokenGuard} in
043 * {@code juneau-rest-server}. It enforces a deliberately strict default policy so a misconfigured
044 * call site can't accidentally accept dangerous tokens:
045 *
046 * <ul>
047 *    <li><b>Algorithm allowlisting</b> — defaults to {@code [RS256, ES256]}. {@code HS256} is rejected
048 *       unless the caller explicitly opts in via
049 *       {@link Builder#algorithms(JWSAlgorithm...) algorithms(...)}. {@code "none"} (unsigned JWTs) is
050 *       <i>permanently</i> rejected — there is no opt-in. This defeats the well-known
051 *       <i>algorithm confusion</i> attack where a JWT is signed with {@code HS256} using the RS256
052 *       public key as the HMAC secret.
053 *    <li><b>Mandatory claims</b> — {@code iss}, {@code aud}, {@code exp}, {@code nbf} are required by
054 *       default. A token missing any of them is rejected.
055 *    <li><b>Clock skew</b> — defaults to 60 seconds tolerance for {@code exp} and {@code nbf}.
056 *       Capped at 300 seconds by the builder.
057 *    <li><b>JWKS caching</b> — keys are fetched and cached for 5 minutes by default. On fetch failure
058 *       the cache continues serving past TTL with a warning logged (see {@link JwksCache}). When a
059 *       fresh-cache selection returns no keys (typically because an IdP rotated its signing key
060 *       mid-TTL), the cache performs one eager out-of-band refresh before failing the request. This
061 *       behavior is <b>enabled by default</b> ({@link Builder#jwksEagerRefreshOnKidMiss(boolean)
062 *       jwksEagerRefreshOnKidMiss(true)}) and bounded by a per-cache cooldown (default 10 seconds)
063 *       plus a single-in-flight guard. Opt out via
064 *       {@link Builder#jwksEagerRefreshOnKidMiss(boolean) jwksEagerRefreshOnKidMiss(false)}.
065 *       Applies only to {@link Builder#jwksUrl(URI) jwksUrl(...)}-backed caches; a
066 *       caller-supplied {@link Builder#jwkSource(JWKSource) jwkSource(...)} is unaffected.
067 * </ul>
068 *
069 * <h5 class='topic'>Usage</h5>
070 *
071 * <p class='bjava'>
072 *    <jk>var</jk> validator = JwtTokenValidator.<jsm>create</jsm>()
073 *       .jwksUrl(URI.<jsm>create</jsm>(<js>"https://issuer.example.com/.well-known/jwks.json"</js>))
074 *       .issuer(<js>"https://issuer.example.com/"</js>)
075 *       .audience(<js>"https://api.example.com"</js>)
076 *       .build();
077 *
078 *    <jk>var</jk> guard = BearerTokenGuard.<jsm>create</jsm>()
079 *       .realm(<js>"api"</js>)
080 *       .validator(validator)
081 *       .build();
082 * </p>
083 *
084 * <h5 class='topic'>Security notes</h5>
085 *
086 * <ul>
087 *    <li>Never disable {@code iss} / {@code aud} validation. The builder enforces them.
088 *    <li>Allowlist {@code HS256} only when both ends of the connection are services you control AND
089 *       you have a secure secret distribution mechanism. JWKS-published JWTs should be asymmetric.
090 *    <li>Rotate JWKS keys on a schedule. {@link JwksCache} picks up new keys after the configured TTL.
091 * </ul>
092 *
093 * <h5 class='section'>See Also:</h5>
094 * <ul>
095 *    <li class='jc'>{@link TokenValidator}
096 *    <li class='jc'>{@link BearerTokenGuard}
097 *    <li class='jc'>{@link ClaimsPrincipal}
098 *    <li class='link'><a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519 — JSON Web Token</a>
099 *    <li class='link'><a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc7517">RFC 7517 — JSON Web Key</a>
100 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerAuthGuards">AuthN Guards — Bearer / API-Key / JWT</a>
101 * </ul>
102 *
103 * @since 10.0.0
104 */
105@SuppressWarnings({
106   "java:S1192" // Duplicate string literals are JWT claim names and algorithm identifiers; intentional
107})
108public class JwtTokenValidator implements TokenValidator {
109
110   /** Maximum clock-skew tolerance the builder will accept. */
111   private static final Duration MAX_CLOCK_SKEW = Duration.ofSeconds(300);
112
113   /**
114    * Static creator.
115    *
116    * <p>
117    * Routes builder construction through {@link BeanInstantiator} so the
118    * {@link Builder#jwksCacheTtl jwksCacheTtl} default is resolved via {@link Value @Value} from the
119    * active {@link org.apache.juneau.commons.settings.Settings Settings} chain
120    * ({@code juneau.jwt.jwksCacheTtl} property &rarr; {@code PT5M}).
121    *
122    * @return A new builder.
123    */
124   public static Builder create() {
125      return create(BasicBeanStore.INSTANCE);
126   }
127
128   /**
129    * Static creator with explicit bean store.
130    *
131    * @param beanStore The bean store to use for dependency injection.
132    * @return A new builder.
133    */
134   public static Builder create(BeanStore beanStore) {
135      return BeanInstantiator.of(Builder.class, beanStore).run();
136   }
137
138   /**
139    * Builder class for {@link JwtTokenValidator}.
140    */
141   public static class Builder {
142
143      private URI jwksUrl;
144      private JWKSource<SecurityContext> jwkSource;
145      private String issuer;
146      private String audience;
147      private Set<JWSAlgorithm> algorithms = new LinkedHashSet<>(Arrays.asList(JWSAlgorithm.RS256, JWSAlgorithm.ES256));
148      private Duration clockSkew = Duration.ofSeconds(60);
149
150      /**
151       * JWKS cache TTL; populated via {@link Value @Value} from
152       * {@code juneau.jwt.jwksCacheTtl} (ISO-8601 duration, default {@code PT5M} = 5 minutes).
153       */
154      @Value("${juneau.jwt.jwksCacheTtl:PT5M}")
155      Duration jwksCacheTtl;
156
157      /**
158       * Whether to perform an eager JWKS refresh on a {@code kid} cache miss; populated via
159       * {@link Value @Value} from {@code juneau.jwt.jwksEagerRefreshOnKidMiss} (boolean, default
160       * {@code true}).
161       */
162      @Value("${juneau.jwt.jwksEagerRefreshOnKidMiss:true}")
163      boolean jwksEagerRefreshOnKidMiss;
164
165      /**
166       * Minimum interval between eager JWKS refreshes; populated via {@link Value @Value} from
167       * {@code juneau.jwt.jwksEagerRefreshCooldown} (ISO-8601 duration, default {@code PT10S} = 10
168       * seconds).
169       */
170      @Value("${juneau.jwt.jwksEagerRefreshCooldown:PT10S}")
171      Duration jwksEagerRefreshCooldown;
172
173      private Clock clock = Clock.systemUTC();
174
175      /** Constructor. */
176      protected Builder() {}
177
178      /**
179       * Sets the JWKS endpoint URL the validator fetches keys from.
180       *
181       * <p>
182       * Mutually exclusive with {@link #jwkSource(JWKSource)} — set one or the other.
183       *
184       * <p>
185       * The URL must use <js>"https"</js> or target a loopback host; the signing-key material fetched from
186       * this endpoint is the trust anchor for every token, so a plaintext transport to a non-loopback host
187       * is rejected.
188       *
189       * @param value The JWKS URL. Must not be <jk>null</jk> and must use HTTPS or target a loopback host.
190       * @return This object.
191       */
192      public Builder jwksUrl(URI value) {
193         jwksUrl = UriUtils.assertSecureOrLoopback(reqnn("value", value));
194         return this;
195      }
196
197      /**
198       * Sets a custom {@link JWKSource} the validator fetches keys from.
199       *
200       * <p>
201       * Mutually exclusive with {@link #jwksUrl(URI)} — set one or the other. Useful for HSM-backed
202       * signers, multi-issuer federation, or unit tests that want to inject a deterministic key
203       * set without binding to a real HTTPS endpoint.
204       *
205       * @param value The JWK source. Must not be <jk>null</jk>.
206       * @return This object.
207       */
208      public Builder jwkSource(JWKSource<SecurityContext> value) {
209         jwkSource = reqnn("value", value);
210         return this;
211      }
212
213      /**
214       * Sets the required {@code iss} (issuer) claim value.
215       *
216       * @param value The issuer. Must not be <jk>null</jk> or blank.
217       * @return This object.
218       */
219      public Builder issuer(String value) {
220         issuer = reqnb("value", value);
221         return this;
222      }
223
224      /**
225       * Sets the required {@code aud} (audience) claim value.
226       *
227       * @param value The audience. Must not be <jk>null</jk> or blank.
228       * @return This object.
229       */
230      public Builder audience(String value) {
231         audience = reqnb("value", value);
232         return this;
233      }
234
235      /**
236       * Sets the signing-algorithm allowlist.
237       *
238       * <p>
239       * Replaces (not adds to) the default allowlist. Callers wanting {@code HS256} support must
240       * include it explicitly. {@link JWSAlgorithm#NONE} is rejected at build time and at runtime
241       * regardless of what is passed here.
242       *
243       * @param values The allowed algorithms. Must contain at least one element; must not contain
244       *    {@link JWSAlgorithm#NONE}.
245       * @return This object.
246       */
247      public Builder algorithms(JWSAlgorithm...values) {
248         reqnn("values", values);
249         if (values.length == 0)
250            throw iaex("algorithms allowlist must be non-empty");
251         Set<JWSAlgorithm> next = st();
252         for (var a : values) {
253            reqnn("algorithm", a);
254            if (Algorithm.NONE.equals(a))
255               throw iaex("\"none\" algorithm is permanently rejected (RFC 7518 §3.6 unsafe)");
256            next.add(a);
257         }
258         algorithms = next;
259         return this;
260      }
261
262      /**
263       * Sets the clock-skew tolerance for {@code exp} / {@code nbf} validation.
264       *
265       * @param value The tolerance. Must be non-negative and not exceed 5 minutes.
266       * @return This object.
267       */
268      public Builder clockSkew(Duration value) {
269         reqnn("value", value);
270         if (value.isNegative())
271            throw iaex("clockSkew must be non-negative");
272         if (value.compareTo(MAX_CLOCK_SKEW) > 0)
273            throw iaex("clockSkew must not exceed 5 minutes (was %s)", value);
274         clockSkew = value;
275         return this;
276      }
277
278      /**
279       * Sets the JWKS cache TTL.
280       *
281       * @param value The TTL. Must be positive.
282       * @return This object.
283       */
284      public Builder jwksCacheTtl(Duration value) {
285         reqnn("value", value);
286         if (value.isZero() || value.isNegative())
287            throw iaex("jwksCacheTtl must be positive");
288         jwksCacheTtl = value;
289         return this;
290      }
291
292      /**
293       * Enables or disables the eager JWKS refresh that fires on a fresh-cache {@code kid} miss.
294       *
295       * <p>
296       * When {@code true} (the default), the first request that presents a token whose signing
297       * key is not in the current (still-fresh) cache triggers a single out-of-band JWKS refresh.
298       * This closes the rotation window where an IdP publishes a new key mid-TTL and clients would
299       * otherwise see up to {@code jwksCacheTtl} minutes of spurious 401 responses for perfectly
300       * valid tokens. The refresh is bounded by {@link #jwksEagerRefreshCooldown(Duration)} and a
301       * single-in-flight guard (no thundering herd).
302       *
303       * <p>
304       * Applies only to {@link #jwksUrl(URI) jwksUrl(...)}-backed caches. A
305       * caller-supplied {@link #jwkSource(JWKSource) jwkSource(...)} is unaffected.
306       *
307       * @param value {@code true} to enable (default); {@code false} to restore pre-10.0.0 behavior.
308       * @return This object.
309       */
310      public Builder jwksEagerRefreshOnKidMiss(boolean value) {
311         jwksEagerRefreshOnKidMiss = value;
312         return this;
313      }
314
315      /**
316       * Sets the minimum spacing between consecutive eager JWKS refreshes.
317       *
318       * <p>
319       * A burst of unknown-{@code kid} tokens (e.g. misconfigured client, replay attack) or a
320       * JWKS-endpoint outage will trigger at most one eager refresh per cooldown window. This
321       * prevents the eager-refresh path from becoming a vector for amplified JWKS-endpoint load.
322       *
323       * <p>
324       * Must be positive, at most 60 seconds, and at most {@link #jwksCacheTtl(Duration)
325       * jwksCacheTtl} (validated at {@link #build()} time).
326       *
327       * @param value The cooldown. Must not be {@code null}.
328       * @return This object.
329       */
330      public Builder jwksEagerRefreshCooldown(Duration value) {
331         reqnn("value", value);
332         if (value.isZero() || value.isNegative())
333            throw iaex("jwksEagerRefreshCooldown must be positive");
334         if (value.compareTo(Duration.ofSeconds(60)) > 0)
335            throw iaex("jwksEagerRefreshCooldown must not exceed 60 seconds (was %s)", value);
336         jwksEagerRefreshCooldown = value;
337         return this;
338      }
339
340      /**
341       * Overrides the {@link Clock} used for {@code exp} / {@code nbf} comparisons and JWKS cache
342       * TTL. Useful in tests.
343       *
344       * @param value The clock. Must not be <jk>null</jk>.
345       * @return This object.
346       */
347      public Builder clock(Clock value) {
348         clock = reqnn("value", value);
349         return this;
350      }
351
352      /**
353       * Builds the validator.
354       *
355       * @return A new {@link JwtTokenValidator}.
356       */
357      public JwtTokenValidator build() {
358         if (issuer == null)
359            throw isex("JwtTokenValidator requires an issuer");
360         if (audience == null)
361            throw isex("JwtTokenValidator requires an audience");
362         if (jwksUrl == null && jwkSource == null)
363            throw isex("JwtTokenValidator requires either jwksUrl(...) or jwkSource(...)");
364         if (jwksUrl != null && jwkSource != null)
365            throw isex("JwtTokenValidator: jwksUrl(...) and jwkSource(...) are mutually exclusive");
366         if (jwksEagerRefreshCooldown.compareTo(jwksCacheTtl) > 0)
367            throw isex("jwksEagerRefreshCooldown (%s) must not exceed jwksCacheTtl (%s)", jwksEagerRefreshCooldown, jwksCacheTtl);
368         return new JwtTokenValidator(this);
369      }
370   }
371
372   private final String issuer;
373   private final String audience;
374   private final Set<JWSAlgorithm> algorithms;
375   private final Duration clockSkew;
376   private final Clock clock;
377   private final JWKSource<SecurityContext> jwkSource;
378
379   /**
380    * Constructor.
381    *
382    * @param builder The builder to read configuration from.
383    */
384   protected JwtTokenValidator(Builder builder) {
385      this.issuer = builder.issuer;
386      this.audience = builder.audience;
387      this.algorithms = u(cp(builder.algorithms));
388      this.clockSkew = builder.clockSkew;
389      this.clock = builder.clock;
390      this.jwkSource = builder.jwkSource != null
391         ? builder.jwkSource
392         : new JwksCache(createRemoteJwkSource(builder.jwksUrl), builder.jwksCacheTtl, builder.clock,
393            builder.jwksEagerRefreshOnKidMiss, builder.jwksEagerRefreshCooldown);
394   }
395
396   /**
397    * Returns the issuer this validator requires on tokens.
398    *
399    * @return The configured {@code iss} value.
400    */
401   public String getIssuer() {
402      return issuer;
403   }
404
405   /**
406    * Returns the audience this validator requires on tokens.
407    *
408    * @return The configured {@code aud} value.
409    */
410   public String getAudience() {
411      return audience;
412   }
413
414   /**
415    * Returns the allowlist of signing algorithms.
416    *
417    * @return An unmodifiable copy of the allowlist.
418    */
419   public Set<JWSAlgorithm> getAlgorithms() {
420      return algorithms;
421   }
422
423   /**
424    * Returns the configured clock-skew tolerance.
425    *
426    * @return The clock skew duration.
427    */
428   public Duration getClockSkew() {
429      return clockSkew;
430   }
431
432   @Override /* Overridden from TokenValidator */
433   public Principal validate(String token) throws AuthenticationException {
434      reqnn("token", token);
435      JWT jwt;
436      try {
437         jwt = JWTParser.parse(token);
438      } catch (ParseException e) {
439         throw new AuthenticationException(e, "JWT could not be parsed")
440            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"malformed JWT\"");
441      }
442      if (! (jwt instanceof SignedJWT jwt2))
443         throw new AuthenticationException("JWT must be signed (alg=none and unsigned JWTs are rejected)")
444            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"unsigned JWT\"");
445      var alg = jwt2.getHeader().getAlgorithm();
446      if (alg == null || Algorithm.NONE.equals(alg))
447         throw new AuthenticationException("JWT alg \"none\" is not permitted")
448            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"alg=none rejected\"");
449      if (! algorithms.contains(alg))
450         throw new AuthenticationException("JWT algorithm not allowlisted: " + alg)
451            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"algorithm not allowed\"");
452
453      // Verify signature.
454      var processor = new DefaultJWTProcessor<SecurityContext>();
455      processor.setJWSKeySelector(new JWSVerificationKeySelector<>(algorithms, jwkSource));
456      // Disable nimbus' internal claims verifier — we run our own below so we control the Clock.
457      processor.setJWTClaimsSetVerifier((claims, ctx) -> { /* no-op */ });
458
459      JWTClaimsSet claims;
460      try {
461         claims = processor.process(jwt2, null);
462      } catch (BadJOSEException e) {
463         throw new AuthenticationException(e, "JWT validation failed: " + e.getMessage())
464            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"" + sanitize(e.getMessage()) + "\"");
465      } catch (JOSEException e) {
466         throw new AuthenticationException(e, "JWT signature verification failed")
467            .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"signature verification failed\"");
468      }
469
470      verifyClaims(claims);
471
472      var subject = claims.getSubject();
473      if (isBlank(subject))
474         subject = "<no-sub>";
475      return new ClaimsPrincipal(subject, claims.toJSONObject());
476   }
477
478   private void verifyClaims(JWTClaimsSet claims) throws AuthenticationException {
479      // Required claims: iss, aud, exp, nbf are non-negotiable defaults.
480      var iss = claims.getIssuer();
481      if (isBlank(iss))
482         throw reject("\"iss\" claim is required");
483      if (neq(issuer, iss))
484         throw reject("\"iss\" claim does not match expected issuer");
485
486      var aud = claims.getAudience();
487      if (ie(aud))
488         throw reject("\"aud\" claim is required");
489      if (! aud.contains(audience))
490         throw reject("\"aud\" claim does not include expected audience");
491
492      var exp = claims.getExpirationTime();
493      if (exp == null)
494         throw reject("\"exp\" claim is required");
495      var nbf = claims.getNotBeforeTime();
496      if (nbf == null)
497         throw reject("\"nbf\" claim is required");
498
499      var now = clock.instant();
500      if (exp.toInstant().plus(clockSkew).isBefore(now))
501         throw reject("token has expired");
502      if (nbf.toInstant().minus(clockSkew).isAfter(now))
503         throw reject("token not yet valid");
504   }
505
506   private static AuthenticationException reject(String reason) {
507      return new AuthenticationException("JWT validation failed: " + reason)
508         .wwwAuthenticate("Bearer error=\"invalid_token\", error_description=\"" + sanitize(reason) + "\"");
509   }
510
511   private static JWKSource<SecurityContext> createRemoteJwkSource(URI jwksUrl) {
512      try {
513         return JWKSourceBuilder.create(jwksUrl.toURL()).build();
514      } catch (MalformedURLException e) {
515         throw iaex(e, "Invalid JWKS URL: %s", jwksUrl);
516      }
517   }
518
519   private static String sanitize(String s) {
520      // Don't echo arbitrary characters into a WWW-Authenticate header value.
521      // Both call sites pass a non-null message: reject(String) always passes a string literal, and the
522      // BadJOSEException branch in validate() only ever catches Nimbus exceptions thrown (for a SignedJWT) with a
523      // literal, non-null message.
524      return s.replaceAll("[\"\\r\\n]", " ");
525   }
526}