001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2021, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.token;
019
020
021import com.nimbusds.oauth2.sdk.ParseException;
022import com.nimbusds.oauth2.sdk.Scope;
023import com.nimbusds.oauth2.sdk.http.HTTPResponse;
024import com.nimbusds.oauth2.sdk.rar.AuthorizationDetail;
025import com.nimbusds.oauth2.sdk.util.JSONArrayUtils;
026import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
028import com.nimbusds.oauth2.sdk.util.StringUtils;
029import net.minidev.json.JSONArray;
030import net.minidev.json.JSONObject;
031
032import java.net.URI;
033import java.util.List;
034import java.util.Map;
035
036
037/**
038 * Access token parse utilities.
039 */
040public class AccessTokenParseUtils {
041        
042        
043        /**
044         * Parses a {@code token_type} from a JSON object and ensures it
045         * matches the specified.
046         *
047         * @param jsonObject The JSON object. Must not be {@code null}.
048         * @param type       The expected token type. Must not be {@code null}.
049         *
050         * @throws ParseException If parsing failed.
051         */
052        public static void parseAndEnsureTypeFromJSONObject(final JSONObject jsonObject, final AccessTokenType type)
053                throws ParseException {
054                
055                if (! new AccessTokenType(JSONObjectUtils.getNonBlankString(jsonObject, "token_type")).equals(type)) {
056                        throw new ParseException("The token type must be " + type);
057                }
058        }
059        
060        
061        /**
062         * Parses an {code access_token} value from a JSON object.
063         *
064         * @param params The JSON object. Must not be {@code null}.
065         *
066         * @return The access token value.
067         *
068         * @throws ParseException If parsing failed.
069         */
070        public static String parseValueFromJSONObject(final JSONObject params)
071                throws ParseException {
072                
073                return JSONObjectUtils.getNonBlankString(params, "access_token");
074        }
075        
076        
077        /**
078         * Parses an access token {@code expires_in} parameter from a JSON
079         * object.
080         *
081         * @param jsonObject The JSON object. Must not be {@code null}.
082         *
083         * @return The access token lifetime, in seconds, zero if not
084         *         specified.
085         *
086         * @throws ParseException If parsing failed.
087         */
088        public static long parseLifetimeFromJSONObject(final JSONObject jsonObject)
089                throws ParseException {
090                
091                if (jsonObject.containsKey("expires_in")) {
092                        // Lifetime can be a JSON number or string
093                        if (jsonObject.get("expires_in") instanceof Number) {
094                                return JSONObjectUtils.getLong(jsonObject, "expires_in");
095                        } else {
096                                String lifetimeStr = JSONObjectUtils.getNonBlankString(jsonObject, "expires_in");
097                                try {
098                                        return Long.parseLong(lifetimeStr);
099                                } catch (NumberFormatException e) {
100                                        throw new ParseException("expires_in must be an integer");
101                                }
102                        }
103                }
104                
105                return 0L;
106        }
107        
108        
109        /**
110         * Parses a {@code scope} parameter from a JSON object.
111         *
112         * @param jsonObject The JSON object. Must not be {@code null}.
113         *
114         * @return The scope, {@code null} if not specified.
115         *
116         * @throws ParseException If parsing failed.
117         */
118        public static Scope parseScopeFromJSONObject(final JSONObject jsonObject)
119                throws ParseException {
120                
121                return Scope.parse(JSONObjectUtils.getString(jsonObject, "scope", null));
122        }
123
124
125        /**
126         * Parses an {@code authorization_details} parameter from a JSON
127         * object.
128         *
129         * @param jsonObject The JSON object. Must not be {@code null}.
130         *
131         * @return The authorisation details, {@code null} if not specified.
132         *
133         * @throws ParseException If parsing failed.
134         */
135        public static List<AuthorizationDetail> parseAuthorizationDetailsFromJSONObject(final JSONObject jsonObject)
136                throws ParseException {
137
138                JSONArray jsonArray = JSONObjectUtils.getJSONArray(jsonObject, "authorization_details", null);
139
140                if (jsonArray == null) {
141                        return null;
142                }
143
144                return AuthorizationDetail.parseList(JSONArrayUtils.toJSONObjectList(jsonArray));
145        }
146
147        
148        /**
149         * Parses an {@code issued_token_type} parameter from a JSON object.
150         *
151         * @param jsonObject The JSON object. Must not be {@code null}.
152         *
153         * @return The issued token type, {@code null} if not specified.
154         *
155         * @throws ParseException If parsing failed.
156         */
157        public static TokenTypeURI parseIssuedTokenTypeFromJSONObject(final JSONObject jsonObject)
158                throws ParseException {
159                
160                String issuedTokenTypeString = JSONObjectUtils.getString(jsonObject, "issued_token_type", null);
161
162                if (issuedTokenTypeString == null) {
163                        return null;
164                }
165
166                try {
167                        return TokenTypeURI.parse(issuedTokenTypeString);
168                } catch (ParseException e) {
169                        throw new ParseException("Invalid issued_token_type", e);
170                }
171        }
172
173
174        private static class GenericTokenSchemeError extends TokenSchemeError {
175
176                private static final long serialVersionUID = -8049139536364886132L;
177
178                public GenericTokenSchemeError(final AccessTokenType scheme,
179                                               final String code,
180                                               final String description,
181                                               final int httpStatusCode) {
182                        super(scheme, code, description, httpStatusCode, null, null, null);
183                }
184
185                @Override
186                public TokenSchemeError setDescription(String description) {
187                        return this;
188                }
189
190                @Override
191                public TokenSchemeError appendDescription(String text) {
192                        return this;
193                }
194
195                @Override
196                public TokenSchemeError setHTTPStatusCode(int httpStatusCode) {
197                        return this;
198                }
199
200                @Override
201                public TokenSchemeError setURI(URI uri) {
202                        return this;
203                }
204
205                @Override
206                public TokenSchemeError setRealm(String realm) {
207                        return this;
208                }
209
210                @Override
211                public TokenSchemeError setScope(Scope scope) {
212                        return this;
213                }
214        }
215
216
217        private static TokenSchemeError getTypedMissingTokenError(final AccessTokenType type) {
218                if (AccessTokenType.BEARER.equals(type)) {
219                        return BearerTokenError.MISSING_TOKEN;
220                } else if (AccessTokenType.DPOP.equals(type)) {
221                        return DPoPTokenError.MISSING_TOKEN;
222                } else {
223                        return new GenericTokenSchemeError(type, null, null, HTTPResponse.SC_UNAUTHORIZED);
224                }
225        }
226
227
228        private static TokenSchemeError getTypedInvalidRequestError(final AccessTokenType type) {
229                if (AccessTokenType.BEARER.equals(type)) {
230                        return BearerTokenError.INVALID_REQUEST;
231                } else if (AccessTokenType.DPOP.equals(type)) {
232                        return DPoPTokenError.INVALID_REQUEST;
233                } else {
234                        return new GenericTokenSchemeError(type, "invalid_request", "Invalid request", HTTPResponse.SC_BAD_REQUEST);
235                }
236        }
237        
238        
239        /**
240         * Parses an access token value from an {@code Authorization} HTTP
241         * request header.
242         *
243         * @param header The {@code Authorization} header value, {@code null}
244         *               if not specified.
245         * @param type   The expected access token type, such as
246         *               {@link AccessTokenType#BEARER} or
247         *               {@link AccessTokenType#DPOP}. Must not be
248         *               {@code null}.
249         *
250         * @return The access token value.
251         *
252         * @throws ParseException If parsing failed.
253         */
254        public static String parseValueFromAuthorizationHeader(final String header,
255                                                               final AccessTokenType type)
256                throws ParseException {
257                
258                if (StringUtils.isBlank(header)) {
259                        TokenSchemeError schemeError = getTypedMissingTokenError(type);
260                        throw new ParseException("Missing HTTP Authorization header", schemeError);
261                }
262                
263                String[] parts = header.split("\\s", 2);
264                
265                if (parts.length != 2) {
266                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
267                        throw new ParseException("Invalid HTTP Authorization header value", schemeError);
268                }
269                
270                if (! parts[0].equalsIgnoreCase(type.getValue())) {
271                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
272                        throw new ParseException("Token type must be " + type, schemeError);
273                }
274                
275                if (StringUtils.isBlank(parts[1])) {
276                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
277                        throw new ParseException("Invalid HTTP Authorization header value: Missing token", schemeError);
278                }
279                
280                return parts[1];
281        }
282        
283        
284        /**
285         * Parses an {@code access_token} values from a query or form
286         * parameters.
287         *
288         * @param parameters The parameters. Must not be {@code null}.
289         * @param type       The expected access token type, such as
290         *                   {@link AccessTokenType#BEARER} or
291         *                   {@link AccessTokenType#DPOP}. Must not be
292         *                   {@code null}.
293         *
294         * @return The access token value.
295         *
296         * @throws ParseException If parsing failed.
297         */
298        public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters,
299                                                           final AccessTokenType type)
300                throws ParseException {
301                
302                if (! parameters.containsKey("access_token")) {
303                        TokenSchemeError schemeError = getTypedMissingTokenError(type);
304                        throw new ParseException("Missing access token parameter", schemeError);
305                }
306                
307                String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token");
308                
309                if (StringUtils.isBlank(accessTokenValue)) {
310                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
311                        throw new ParseException("Blank / empty access token", schemeError);
312                }
313                
314                return accessTokenValue;
315        }
316        
317        
318        /**
319         * Parses an {@code access_token} value from a query or form
320         * parameters.
321         *
322         * @param parameters The query parameters. Must not be {@code null}.
323         *
324         * @return The access token value.
325         *
326         * @throws ParseException If parsing failed.
327         */
328        public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters)
329                throws ParseException {
330                
331                String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token");
332                
333                if (StringUtils.isBlank(accessTokenValue)) {
334                        throw new ParseException("Missing access token");
335                }
336                
337                return accessTokenValue;
338        }
339        
340        
341        /**
342         * Determines the access token type from an {@code Authorization} HTTP
343         * request header.
344         *
345         * @param header The {@code Authorization} header value. Must not be
346         *               {@code null}.
347         *
348         * @return The access token type.
349         *
350         * @throws ParseException If parsing failed.
351         */
352        public static AccessTokenType determineAccessTokenTypeFromAuthorizationHeader(final String header)
353                throws ParseException {
354
355                String[] parts = header.split("\\s", 2);
356
357                if (parts.length < 2 || StringUtils.isBlank(parts[0]) || StringUtils.isBlank(parts[1])) {
358                        throw new ParseException("Invalid Authorization header");
359                }
360
361                if (parts[0].equalsIgnoreCase(AccessTokenType.BEARER.getValue())) {
362                        return AccessTokenType.BEARER;
363                }
364
365                if (parts[0].equalsIgnoreCase(AccessTokenType.DPOP.getValue())) {
366                        return AccessTokenType.DPOP;
367                }
368
369                return new AccessTokenType(parts[0]);
370        }
371        
372        
373        private AccessTokenParseUtils() {}
374}