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                                try {
095                                        return JSONObjectUtils.getNonNegativeLong(jsonObject, "expires_in");
096                                } catch (ParseException e) {
097                                        throw new ParseException("Invalid expires_in");
098                                }
099                        } else {
100                                String lifetimeStr = JSONObjectUtils.getNonBlankString(jsonObject, "expires_in");
101                                try {
102                                        return Long.parseLong(lifetimeStr);
103                                } catch (NumberFormatException e) {
104                                        throw new ParseException("Invalid expires_in");
105                                }
106                        }
107                }
108                
109                return 0L;
110        }
111        
112        
113        /**
114         * Parses a {@code scope} parameter from a JSON object.
115         *
116         * @param jsonObject The JSON object. Must not be {@code null}.
117         *
118         * @return The scope, {@code null} if not specified.
119         *
120         * @throws ParseException If parsing failed.
121         */
122        public static Scope parseScopeFromJSONObject(final JSONObject jsonObject)
123                throws ParseException {
124                
125                return Scope.parse(JSONObjectUtils.getString(jsonObject, "scope", null));
126        }
127
128
129        /**
130         * Parses an {@code authorization_details} parameter from a JSON
131         * object.
132         *
133         * @param jsonObject The JSON object. Must not be {@code null}.
134         *
135         * @return The authorisation details, {@code null} if not specified.
136         *
137         * @throws ParseException If parsing failed.
138         */
139        public static List<AuthorizationDetail> parseAuthorizationDetailsFromJSONObject(final JSONObject jsonObject)
140                throws ParseException {
141
142                JSONArray jsonArray = JSONObjectUtils.getJSONArray(jsonObject, "authorization_details", null);
143
144                if (jsonArray == null) {
145                        return null;
146                }
147
148                return AuthorizationDetail.parseList(JSONArrayUtils.toJSONObjectList(jsonArray));
149        }
150
151        
152        /**
153         * Parses an {@code issued_token_type} parameter from a JSON object.
154         *
155         * @param jsonObject The JSON object. Must not be {@code null}.
156         *
157         * @return The issued token type, {@code null} if not specified.
158         *
159         * @throws ParseException If parsing failed.
160         */
161        public static TokenTypeURI parseIssuedTokenTypeFromJSONObject(final JSONObject jsonObject)
162                throws ParseException {
163                
164                String issuedTokenTypeString = JSONObjectUtils.getString(jsonObject, "issued_token_type", null);
165
166                if (issuedTokenTypeString == null) {
167                        return null;
168                }
169
170                try {
171                        return TokenTypeURI.parse(issuedTokenTypeString);
172                } catch (ParseException e) {
173                        throw new ParseException("Invalid issued_token_type", e);
174                }
175        }
176
177
178        private static class GenericTokenSchemeError extends TokenSchemeError {
179
180                private static final long serialVersionUID = -8049139536364886132L;
181
182                public GenericTokenSchemeError(final AccessTokenType scheme,
183                                               final String code,
184                                               final String description,
185                                               final int httpStatusCode) {
186                        super(scheme, code, description, httpStatusCode, null, null, null);
187                }
188
189                @Override
190                public TokenSchemeError setDescription(String description) {
191                        return this;
192                }
193
194                @Override
195                public TokenSchemeError appendDescription(String text) {
196                        return this;
197                }
198
199                @Override
200                public TokenSchemeError setHTTPStatusCode(int httpStatusCode) {
201                        return this;
202                }
203
204                @Override
205                public TokenSchemeError setURI(URI uri) {
206                        return this;
207                }
208
209                @Override
210                public TokenSchemeError setRealm(String realm) {
211                        return this;
212                }
213
214                @Override
215                public TokenSchemeError setScope(Scope scope) {
216                        return this;
217                }
218        }
219
220
221        private static TokenSchemeError getTypedMissingTokenError(final AccessTokenType type) {
222                if (AccessTokenType.BEARER.equals(type)) {
223                        return BearerTokenError.MISSING_TOKEN;
224                } else if (AccessTokenType.DPOP.equals(type)) {
225                        return DPoPTokenError.MISSING_TOKEN;
226                } else {
227                        return new GenericTokenSchemeError(type, null, null, HTTPResponse.SC_UNAUTHORIZED);
228                }
229        }
230
231
232        private static TokenSchemeError getTypedInvalidRequestError(final AccessTokenType type) {
233                if (AccessTokenType.BEARER.equals(type)) {
234                        return BearerTokenError.INVALID_REQUEST;
235                } else if (AccessTokenType.DPOP.equals(type)) {
236                        return DPoPTokenError.INVALID_REQUEST;
237                } else {
238                        return new GenericTokenSchemeError(type, "invalid_request", "Invalid request", HTTPResponse.SC_BAD_REQUEST);
239                }
240        }
241        
242        
243        /**
244         * Parses an access token value from an {@code Authorization} HTTP
245         * request header.
246         *
247         * @param header The {@code Authorization} header value, {@code null}
248         *               if not specified.
249         * @param type   The expected access token type, such as
250         *               {@link AccessTokenType#BEARER} or
251         *               {@link AccessTokenType#DPOP}. Must not be
252         *               {@code null}.
253         *
254         * @return The access token value.
255         *
256         * @throws ParseException If parsing failed.
257         */
258        public static String parseValueFromAuthorizationHeader(final String header,
259                                                               final AccessTokenType type)
260                throws ParseException {
261                
262                if (StringUtils.isBlank(header)) {
263                        TokenSchemeError schemeError = getTypedMissingTokenError(type);
264                        throw new ParseException("Missing HTTP Authorization header", schemeError);
265                }
266                
267                String[] parts = header.split("\\s", 2);
268                
269                if (parts.length != 2) {
270                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
271                        throw new ParseException("Invalid HTTP Authorization header value", schemeError);
272                }
273                
274                if (! parts[0].equalsIgnoreCase(type.getValue())) {
275                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
276                        throw new ParseException("Token type must be " + type, schemeError);
277                }
278                
279                if (StringUtils.isBlank(parts[1])) {
280                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
281                        throw new ParseException("Invalid HTTP Authorization header value: Missing token", schemeError);
282                }
283                
284                return parts[1].trim();
285        }
286        
287        
288        /**
289         * Parses an {@code access_token} values from a query or form
290         * parameters.
291         *
292         * @param parameters The parameters. Must not be {@code null}.
293         * @param type       The expected access token type, such as
294         *                   {@link AccessTokenType#BEARER} or
295         *                   {@link AccessTokenType#DPOP}. Must not be
296         *                   {@code null}.
297         *
298         * @return The access token value.
299         *
300         * @throws ParseException If parsing failed.
301         */
302        public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters,
303                                                           final AccessTokenType type)
304                throws ParseException {
305                
306                if (! parameters.containsKey("access_token")) {
307                        TokenSchemeError schemeError = getTypedMissingTokenError(type);
308                        throw new ParseException("Missing access token parameter", schemeError);
309                }
310                
311                String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token");
312                
313                if (StringUtils.isBlank(accessTokenValue)) {
314                        TokenSchemeError schemeError = getTypedInvalidRequestError(type);
315                        throw new ParseException("Blank / empty access token", schemeError);
316                }
317                
318                return accessTokenValue;
319        }
320        
321        
322        /**
323         * Parses an {@code access_token} value from a query or form
324         * parameters.
325         *
326         * @param parameters The query parameters. Must not be {@code null}.
327         *
328         * @return The access token value.
329         *
330         * @throws ParseException If parsing failed.
331         */
332        public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters)
333                throws ParseException {
334                
335                String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token");
336                
337                if (StringUtils.isBlank(accessTokenValue)) {
338                        throw new ParseException("Missing access token");
339                }
340                
341                return accessTokenValue;
342        }
343        
344        
345        /**
346         * Determines the access token type from an {@code Authorization} HTTP
347         * request header.
348         *
349         * @param header The {@code Authorization} header value. Must not be
350         *               {@code null}.
351         *
352         * @return The access token type.
353         *
354         * @throws ParseException If parsing failed.
355         */
356        public static AccessTokenType determineAccessTokenTypeFromAuthorizationHeader(final String header)
357                throws ParseException {
358
359                String[] parts = header.split("\\s", 2);
360
361                if (parts.length < 2 || StringUtils.isBlank(parts[0]) || StringUtils.isBlank(parts[1])) {
362                        throw new ParseException("Invalid Authorization header");
363                }
364
365                if (parts[0].equalsIgnoreCase(AccessTokenType.BEARER.getValue())) {
366                        return AccessTokenType.BEARER;
367                }
368
369                if (parts[0].equalsIgnoreCase(AccessTokenType.DPOP.getValue())) {
370                        return AccessTokenType.DPOP;
371                }
372
373                return new AccessTokenType(parts[0]);
374        }
375        
376        
377        private AccessTokenParseUtils() {}
378}