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}