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