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;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.security.*;
023
024import org.apache.juneau.rest.server.*;
025import org.apache.juneau.rest.server.guard.*;
026
027/**
028 * AuthN {@link RestGuard} that authenticates requests via the
029 * <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6750">RFC 6750 Bearer Token</a> scheme.
030 *
031 * <p>
032 * On every request, the guard:
033 * <ol>
034 *    <li>Reads the {@code Authorization} request header.
035 *    <li>Confirms it starts with {@code "Bearer "} (case-insensitive) followed by a non-blank token.
036 *    <li>Delegates the raw token to the configured {@link TokenValidator}.
037 *    <li>On success, stashes the returned {@link Principal} on the request attributes under
038 *       {@code "juneau.principal"} so downstream handlers can read it via
039 *       {@link AuthArg @Auth Principal} or {@code req.getAttributes().get(PRINCIPAL_ATTR)}.
040 *    <li>On any failure path (missing header, malformed scheme, validator throws), throws
041 *       {@link AuthenticationException} with a {@code WWW-Authenticate: Bearer realm="<realm>"} response
042 *       header set per RFC 7235 &sect;4.1.
043 * </ol>
044 *
045 * <h5 class='topic'>Usage</h5>
046 *
047 * <p class='bjava'>
048 *    <ja>@Rest</ja>(path=<js>"/api"</js>)
049 *    <jk>public class</jk> ApiResource <jk>extends</jk> RestServlet {
050 *
051 *       <ja>@Bean</ja>(name=<js>"guards"</js>)
052 *       <jk>public</jk> RestGuardList guards(BeanStore <jv>bs</jv>) {
053 *          <jk>return</jk> RestGuardList.<jsm>create</jsm>(<jv>bs</jv>)
054 *             .append(
055 *                BearerTokenGuard.<jsm>create</jsm>()
056 *                   .realm(<js>"api"</js>)
057 *                   .validator(<jv>myTokenValidator</jv>)
058 *                   .build())
059 *             .build();
060 *       }
061 *    }
062 * </p>
063 *
064 * <h5 class='topic'>Security notes</h5>
065 *
066 * <ul>
067 *    <li>Bearer tokens are <b>credentials</b> &mdash; never log the full token, never echo it in a
068 *       response body, and always pair with TLS in production.
069 *    <li>{@link TokenValidator}s should run in constant time when possible (in particular, opaque-token
070 *       lookups should use constant-time comparison to defeat timing side-channels).
071 *    <li>For JWT verification, use the {@code JwtTokenValidator} shipped in the optional
072 *       {@code juneau-rest-server-auth-jwt} sub-module &mdash; it enforces algorithm allowlisting and
073 *       mandatory {@code iss} / {@code aud} / {@code exp} / {@code nbf} checks by default.
074 * </ul>
075 *
076 * <h5 class='section'>See Also:</h5><ul>
077 *    <li class='jc'>{@link TokenValidator}
078 *    <li class='jc'>{@link AuthenticationException}
079 *    <li class='jc'>{@link Auth}
080 *    <li class='link'><a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6750">RFC 6750 &mdash; Bearer Token Usage</a>
081 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerAuthGuards">AuthN Guards — Bearer / API-Key / JWT</a>
082 * </ul>
083 *
084 * @since 10.0.0
085 */
086public class BearerTokenGuard extends RestGuard {
087
088   /** WWW-Authenticate response header name (RFC 7235 §4.1). */
089   private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
090
091   /**
092    * Static creator.
093    *
094    * @return A new builder.
095    */
096   public static Builder create() {
097      return new Builder();
098   }
099
100   /**
101    * Builder class.
102    */
103   public static class Builder {
104
105      private TokenValidator validator;
106      private String realm = "api";
107
108      /**
109       * Constructor.
110       */
111      protected Builder() {}
112
113      /**
114       * Sets the token validator the guard delegates to.
115       *
116       * @param value The validator. Must not be <jk>null</jk>.
117       * @return This object.
118       */
119      public Builder validator(TokenValidator value) {
120         validator = reqnn("value", value);
121         return this;
122      }
123
124      /**
125       * Sets the {@code WWW-Authenticate: Bearer realm="<value>"} challenge realm.
126       *
127       * <p>
128       * The realm string communicates to the client which protection space the credentials apply to.
129       * Defaults to {@code "api"} when not set.
130       *
131       * @param value The realm. Must not be <jk>null</jk> or blank.
132       * @return This object.
133       */
134      public Builder realm(String value) {
135         realm = reqnb("value", value);
136         return this;
137      }
138
139      /**
140       * Builds the guard.
141       *
142       * @return A new {@link BearerTokenGuard}.
143       */
144      public BearerTokenGuard build() {
145         if (validator == null)
146            throw new IllegalStateException("BearerTokenGuard requires a TokenValidator");
147         return new BearerTokenGuard(this);
148      }
149   }
150
151   private final TokenValidator validator;
152   private final String realm;
153   private final String challenge;
154
155   /**
156    * Constructor.
157    *
158    * @param builder The builder to read configuration from. Must not be <jk>null</jk>.
159    */
160   protected BearerTokenGuard(Builder builder) {
161      this.validator = builder.validator;
162      this.realm = builder.realm;
163      this.challenge = "Bearer realm=\"" + realm + "\"";
164   }
165
166   /**
167    * Convenience constructor that uses the default realm ({@code "api"}).
168    *
169    * @param validator The token validator. Must not be <jk>null</jk>.
170    */
171   public BearerTokenGuard(TokenValidator validator) {
172      this(create().validator(validator));
173   }
174
175   @Override /* Overridden from RestGuard */
176   public boolean guard(RestRequest req, RestResponse res) {
177      var header = req.getHeader(BearerTokenExtractor.AUTHORIZATION);
178      if (isBlank(header)) {
179         res.setHeader(WWW_AUTHENTICATE, challenge);
180         throw missing();
181      }
182      var optToken = BearerTokenExtractor.extract(header);
183      if (optToken.isEmpty()) {
184         res.setHeader(WWW_AUTHENTICATE, challenge);
185         throw malformed();
186      }
187      var token = optToken.get();
188      Principal p;
189      try {
190         p = validator.validate(token);
191      } catch (AuthenticationException e) {
192         // Preserve any richer challenge the validator already set on the exception; otherwise stamp our basic one.
193         var override = e.getHeaders().stream()
194            .filter(h -> eqic(WWW_AUTHENTICATE, h.getName()))
195            .map(h -> h.getValue())
196            .findFirst()
197            .orElse(challenge);
198         res.setHeader(WWW_AUTHENTICATE, override);
199         throw e;
200      } catch (RuntimeException e) {
201         res.setHeader(WWW_AUTHENTICATE, challenge);
202         throw new AuthenticationException(e, "Token validation failed").wwwAuthenticate(challenge);
203      }
204      if (p == null) {
205         res.setHeader(WWW_AUTHENTICATE, challenge);
206         throw new AuthenticationException("Token validation returned no principal").wwwAuthenticate(challenge);
207      }
208      req.getAttributes().set("juneau.principal", p);
209      return true;
210   }
211
212   @Override /* Overridden from RestGuard */
213   public boolean isRequestAllowed(RestRequest req) {
214      // Not used: this guard overrides guard(req, res) directly because it needs to mutate request
215      // attributes and set the WWW-Authenticate challenge on the rejection path.
216      return false;
217   }
218
219   private AuthenticationException missing() {
220      return new AuthenticationException("Authorization header missing").wwwAuthenticate(challenge);
221   }
222
223   private AuthenticationException malformed() {
224      return new AuthenticationException("Authorization header malformed").wwwAuthenticate(challenge);
225   }
226}