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}