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 §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> — 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 — 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 — 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}