001package com.nimbusds.common.oauth2; 002 003 004import com.nimbusds.oauth2.sdk.ParseException; 005import com.nimbusds.oauth2.sdk.token.BearerAccessToken; 006import com.thetransactioncompany.util.PropertyParseException; 007import com.thetransactioncompany.util.PropertyRetriever; 008import jakarta.servlet.http.HttpServletRequest; 009import jakarta.servlet.http.HttpServletResponse; 010import jakarta.ws.rs.WebApplicationException; 011import net.jcip.annotations.ThreadSafe; 012import org.apache.commons.codec.DecoderException; 013import org.apache.commons.codec.binary.Hex; 014import org.apache.commons.lang3.StringUtils; 015 016import java.io.IOException; 017import java.util.Collections; 018import java.util.List; 019 020 021/** 022 * SHA-256 based access token validator. The expected access tokens are 023 * configured as their SHA-256 hashes, to prevent accidental leaks into logs, 024 * etc. Supports servlet-based and JAX-RS based web applications. 025 */ 026@ThreadSafe 027public class SHA256BasedAccessTokenValidator extends AbstractAccessTokenValidator { 028 029 030 /** 031 * The minimum acceptable access token length. 032 */ 033 public static final int MIN_TOKEN_LENGTH = 32; 034 035 036 /** 037 * Creates a new access token validator. 038 * 039 * @param tokenHash The Bearer access token SHA-256 hash (in hex). If 040 * {@code null} access to the web API will be 041 * disabled. 042 */ 043 public SHA256BasedAccessTokenValidator(final String tokenHash) { 044 045 this(new String[]{tokenHash}); 046 } 047 048 /** 049 * Creates a new access token validator. 050 * 051 * @param tokenHashes The Bearer access token SHA-256 hashes (in hex). 052 * If {@code null} access to the web API will be 053 * disabled. 054 */ 055 public SHA256BasedAccessTokenValidator(final String ... tokenHashes) { 056 057 for (String hash: tokenHashes) { 058 if (hash == null) continue; 059 try { 060 expectedTokenHashes.add(Hex.decodeHex(hash.toCharArray())); 061 } catch (DecoderException e) { 062 throw new IllegalArgumentException("Invalid hex for access token SHA-256: " + hash); 063 } 064 } 065 066 hashSalt = null; 067 } 068 069 070 /** 071 * Creates a new access token validator. 072 * 073 * @param tokenHash The main Bearer access token SHA-256 074 * hash (in hex). If {@code null} access 075 * to the web API will be disabled. 076 * @param additionalTokenHashes Additional Bearer access token SHA-256 077 * hashes (in hex), empty or {@code null} 078 * if none. 079 */ 080 public SHA256BasedAccessTokenValidator(final String tokenHash, final List<String> additionalTokenHashes) { 081 082 if (tokenHash == null) { 083 return; 084 } 085 086 try { 087 expectedTokenHashes.add(Hex.decodeHex(tokenHash.toCharArray())); 088 } catch (DecoderException e) { 089 throw new IllegalArgumentException("Invalid hex for access token SHA-256: " + tokenHash); 090 } 091 092 if (additionalTokenHashes != null) { 093 for (String hash: additionalTokenHashes) { 094 if (hash != null) { 095 try { 096 expectedTokenHashes.add(Hex.decodeHex(hash.toCharArray())); 097 } catch (DecoderException e) { 098 throw new IllegalArgumentException("Invalid hex for access token SHA-256: " + hash); 099 } 100 } 101 } 102 } 103 } 104 105 106 /** 107 * Creates a new access token validator from the specified properties 108 * retriever. 109 * 110 * @param pr The properties retriever. Must 111 * not be {@code null}. 112 * @param propertyName The property name for the main 113 * Bearer access token SHA-256 hash 114 * (in hex). If {@code null} access 115 * to the web API will be disabled. 116 * Must not be {@code null}. 117 * @param propertyRequired {@code true} if the property is 118 * required, {@code false} if 119 * optional. 120 * @param additionalPropertyNamePrefix The property name prefix for the 121 * additional Bearer access token 122 * SHA-256 hashes (in hex), 123 * {@code null} if not used. 124 * 125 * @return The access token validator. 126 * 127 * @throws PropertyParseException If parsing failed. 128 */ 129 public static SHA256BasedAccessTokenValidator from(final PropertyRetriever pr, 130 final String propertyName, 131 final boolean propertyRequired, 132 final String additionalPropertyNamePrefix) 133 throws PropertyParseException { 134 135 String tokenHash; 136 137 if (propertyRequired) { 138 tokenHash = pr.getString(propertyName); 139 } else { 140 tokenHash = pr.getOptString(propertyName, null); 141 } 142 143 if (additionalPropertyNamePrefix == null) { 144 return new SHA256BasedAccessTokenValidator(tokenHash); 145 } 146 147 List<String> additionalTokenHashes = pr.getOptStringListMulti(additionalPropertyNamePrefix, Collections.emptyList()); 148 149 return new SHA256BasedAccessTokenValidator(tokenHash, additionalTokenHashes); 150 } 151 152 153 @Override 154 public void validateBearerAccessToken(final String authzHeader) 155 throws WebApplicationException { 156 157 // Web API disabled? 158 if (accessIsDisabled()) { 159 throw WEB_API_DISABLED.toWebAppException(); 160 } 161 162 if (StringUtils.isBlank(authzHeader)) { 163 throw MISSING_BEARER_TOKEN.toWebAppException(); 164 } 165 166 BearerAccessToken receivedToken; 167 168 try { 169 receivedToken = BearerAccessToken.parse(authzHeader); 170 171 } catch (ParseException e) { 172 throw MISSING_BEARER_TOKEN.toWebAppException(); 173 } 174 175 if (null != log) { 176 log.trace("[CM3000] Validating bearer access token: {}", TokenAbbreviator.abbreviate(receivedToken)); 177 } 178 179 // Check min length 180 if (receivedToken.getValue().length() < MIN_TOKEN_LENGTH) { 181 throw INVALID_BEARER_TOKEN.toWebAppException(); 182 } 183 184 if (isValid(receivedToken)) { 185 return; // pass 186 } 187 188 throw INVALID_BEARER_TOKEN.toWebAppException(); 189 } 190 191 192 @Override 193 public boolean validateBearerAccessToken(final HttpServletRequest servletRequest, 194 final HttpServletResponse servletResponse) 195 throws IOException { 196 197 // Web API disabled? 198 if (accessIsDisabled()) { 199 WEB_API_DISABLED.apply(servletResponse); 200 return false; 201 } 202 203 BearerAccessToken receivedToken; 204 205 if (servletRequest.getHeader("Authorization") != null) { 206 207 String authzHeaderValue = servletRequest.getHeader("Authorization"); 208 209 if (StringUtils.isBlank(authzHeaderValue)) { 210 MISSING_BEARER_TOKEN.apply(servletResponse); 211 return false; 212 } 213 214 try { 215 receivedToken = BearerAccessToken.parse(authzHeaderValue); 216 217 } catch (ParseException e) { 218 MISSING_BEARER_TOKEN.apply(servletResponse); 219 return false; 220 } 221 222 } else if (servletRequest.getParameter("access_token") != null) { 223 224 String accessTokenValue = servletRequest.getParameter("access_token"); 225 226 if (StringUtils.isBlank(accessTokenValue)) { 227 MISSING_BEARER_TOKEN.apply(servletResponse); 228 return false; 229 } 230 231 receivedToken = new BearerAccessToken(accessTokenValue); 232 } else { 233 MISSING_BEARER_TOKEN.apply(servletResponse); 234 return false; 235 } 236 237 if (null != log) { 238 log.trace("[CM3000] Validating bearer access token: {}", TokenAbbreviator.abbreviate(receivedToken)); 239 } 240 241 // Check min length 242 if (receivedToken.getValue().length() < MIN_TOKEN_LENGTH) { 243 INVALID_BEARER_TOKEN.apply(servletResponse); 244 return false; 245 } 246 247 // Compare hashes 248 if (isValid(receivedToken)) { 249 return true; // pass 250 } 251 252 INVALID_BEARER_TOKEN.apply(servletResponse); 253 return false; 254 } 255}