001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, 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 java.net.URI;
022import java.net.URISyntaxException;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import net.jcip.annotations.Immutable;
027
028import com.nimbusds.oauth2.sdk.ErrorObject;
029import com.nimbusds.oauth2.sdk.ParseException;
030import com.nimbusds.oauth2.sdk.Scope;
031import com.nimbusds.oauth2.sdk.http.HTTPResponse;
032
033
034/**
035 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 
036 * protected by a Bearer access token is denied, due to the request or token 
037 * being invalid, or due to the access token having insufficient scope.
038 *
039 * <p>Standard bearer access token errors:
040 *
041 * <ul>
042 *     <li>{@link #MISSING_TOKEN}
043 *     <li>{@link #INVALID_REQUEST}
044 *     <li>{@link #INVALID_TOKEN}
045 *     <li>{@link #INSUFFICIENT_SCOPE}
046 * </ul>
047 *
048 * <p>Example HTTP response:
049 *
050 * <pre>
051 * HTTP/1.1 401 Unauthorized
052 * WWW-Authenticate: Bearer realm="example.com",
053 *                   error="invalid_token",
054 *                   error_description="The access token expired"
055 * </pre>
056 *
057 * <p>Related specifications:
058 *
059 * <ul>
060 *     <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.1.
061 *     <li>Hypertext Transfer Protocol (HTTP/1.1): Authentication (RFC 7235),
062 *         section 4.1.
063 * </ul>
064 */
065@Immutable
066public class BearerTokenError extends ErrorObject {
067
068
069        /**
070         * The request does not contain an access token. No error code or
071         * description is specified for this error, just the HTTP status code
072         * is set to 401 (Unauthorized).
073         *
074         * <p>Example:
075         *
076         * <pre>
077         * HTTP/1.1 401 Unauthorized
078         * WWW-Authenticate: Bearer
079         * </pre>
080         */
081        public static final BearerTokenError MISSING_TOKEN =
082                new BearerTokenError(null, null, HTTPResponse.SC_UNAUTHORIZED);
083
084        /**
085         * The request is missing a required parameter, includes an unsupported
086         * parameter or parameter value, repeats the same parameter, uses more
087         * than one method for including an access token, or is otherwise 
088         * malformed. The HTTP status code is set to 400 (Bad Request).
089         */
090        public static final BearerTokenError INVALID_REQUEST = 
091                new BearerTokenError("invalid_request", "Invalid request", 
092                                     HTTPResponse.SC_BAD_REQUEST);
093
094
095        /**
096         * The access token provided is expired, revoked, malformed, or invalid
097         * for other reasons.  The HTTP status code is set to 401 
098         * (Unauthorized).
099         */
100        public static final BearerTokenError INVALID_TOKEN =
101                new BearerTokenError("invalid_token", "Invalid access token", 
102                                     HTTPResponse.SC_UNAUTHORIZED);
103        
104        
105        /**
106         * The request requires higher privileges than provided by the access 
107         * token. The HTTP status code is set to 403 (Forbidden).
108         */
109        public static final BearerTokenError INSUFFICIENT_SCOPE =
110                new BearerTokenError("insufficient_scope", "Insufficient scope", 
111                                     HTTPResponse.SC_FORBIDDEN);
112        
113        
114        /**
115         * Returns {@code true} if the specified error code consists of valid
116         * characters. Values for the "error" and "error_description"
117         * attributes must not include characters outside the [0x20, 0x21] |
118         * [0x23 - 0x5B] | [0x5D - 0x7E] range. See RFC 6750, section 3.
119         *
120         * @see ErrorObject#isLegal(String)
121         *
122         * @param errorCode The error code string.
123         *
124         * @return {@code true} if the error code string contains valid
125         *         characters, else {@code false}.
126         */
127        @Deprecated
128        public static boolean isCodeWithValidChars(final String errorCode) {
129                
130                return ErrorObject.isLegal(errorCode);
131        }
132        
133        
134        /**
135         * Returns {@code true} if the specified error description consists of
136         * valid characters. Values for the "error" and "error_description"
137         * attributes must not include characters outside the [0x20, 0x21] |
138         * [0x23 - 0x5B] | [0x5D - 0x7E] range. See RFC 6750, section 3.
139         *
140         * @see ErrorObject#isLegal(String)
141         *
142         * @param errorDescription The error description string.
143         *
144         * @return {@code true} if the error description string contains valid
145         *         characters, else {@code false}.
146         */
147        @Deprecated
148        public static boolean isDescriptionWithValidChars(final String errorDescription) {
149        
150                return ErrorObject.isLegal(errorDescription);
151        }
152        
153        
154        /**
155         * Returns {@code true} if the specified scope consists of valid
156         * characters. Values for the "scope" attributes must not include
157         * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E]
158         * range. See RFC 6750, section 3.
159         *
160         * @see ErrorObject#isLegal(String)
161         *
162         * @param scope The scope.
163         *
164         * @return {@code true} if the scope contains valid characters, else
165         *         {@code false}.
166         */
167        public static boolean isScopeWithValidChars(final Scope scope) {
168                
169                
170                return ErrorObject.isLegal(scope.toString());
171        }
172        
173        
174        /**
175         * Regex pattern for matching the realm parameter of a WWW-Authenticate 
176         * header.
177         */
178        private static final Pattern realmPattern = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.)*)\"");
179
180        
181        /**
182         * Regex pattern for matching the error parameter of a WWW-Authenticate 
183         * header. Double quoting is optional.
184         */
185        private static final Pattern errorPattern = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))");
186
187
188        /**
189         * Regex pattern for matching the error description parameter of a 
190         * WWW-Authenticate header.
191         */
192        private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\"");
193        
194        
195        /**
196         * Regex pattern for matching the error URI parameter of a 
197         * WWW-Authenticate header.
198         */
199        private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\"");
200
201
202        /**
203         * Regex pattern for matching the scope parameter of a WWW-Authenticate 
204         * header.
205         */
206        private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)");
207        
208        
209        /**
210         * The realm, {@code null} if not specified.
211         */
212        private final String realm;
213
214
215        /**
216         * Required scope, {@code null} if not specified.
217         */
218        private final Scope scope;
219        
220        
221        /**
222         * Creates a new OAuth 2.0 bearer token error with the specified code
223         * and description.
224         *
225         * @param code        The error code, {@code null} if not specified.
226         * @param description The error description, {@code null} if not
227         *                    specified.
228         */
229        public BearerTokenError(final String code, final String description) {
230        
231                this(code, description, 0, null, null, null);
232        }
233
234
235        /**
236         * Creates a new OAuth 2.0 bearer token error with the specified code,
237         * description and HTTP status code.
238         *
239         * @param code           The error code, {@code null} if not specified.
240         * @param description    The error description, {@code null} if not
241         *                       specified.
242         * @param httpStatusCode The HTTP status code, zero if not specified.
243         */
244        public BearerTokenError(final String code, final String description, final int httpStatusCode) {
245        
246                this(code, description, httpStatusCode, null, null, null);
247        }
248
249
250        /**
251         * Creates a new OAuth 2.0 bearer token error with the specified code,
252         * description, HTTP status code, page URI, realm and scope.
253         *
254         * @param code           The error code, {@code null} if not specified.
255         * @param description    The error description, {@code null} if not
256         *                       specified.
257         * @param httpStatusCode The HTTP status code, zero if not specified.
258         * @param uri            The error page URI, {@code null} if not
259         *                       specified.
260         * @param realm          The realm, {@code null} if not specified.
261         * @param scope          The required scope, {@code null} if not 
262         *                       specified.
263         */
264        public BearerTokenError(final String code, 
265                                final String description, 
266                                final int httpStatusCode, 
267                                final URI uri,
268                                final String realm,
269                                final Scope scope) {
270        
271                super(code, description, httpStatusCode, uri);
272                this.realm = realm;
273                this.scope = scope;
274                
275                if (scope != null && ! isScopeWithValidChars(scope)) {
276                        throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3");
277                }
278        }
279
280
281        @Override
282        public BearerTokenError setDescription(final String description) {
283
284                return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope);
285        }
286
287
288        @Override
289        public BearerTokenError appendDescription(final String text) {
290
291                String newDescription;
292
293                if (getDescription() != null)
294                        newDescription = getDescription() + text;
295                else
296                        newDescription = text;
297
298                return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope);
299        }
300
301
302        @Override
303        public BearerTokenError setHTTPStatusCode(final int httpStatusCode) {
304
305                return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope);
306        }
307
308
309        @Override
310        public BearerTokenError setURI(final URI uri) {
311
312                return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope);
313        }
314        
315        
316        /**
317         * Gets the realm.
318         *
319         * @return The realm, {@code null} if not specified.
320         */
321        public String getRealm() {
322        
323                return realm;
324        }
325
326
327        /**
328         * Sets the realm.
329         *
330         * @param realm realm, {@code null} if not specified.
331         *
332         * @return A copy of this error with the specified realm.
333         */
334        public BearerTokenError setRealm(final String realm) {
335
336                return new BearerTokenError(getCode(), 
337                                            getDescription(), 
338                                            getHTTPStatusCode(), 
339                                            getURI(), 
340                                            realm, 
341                                            getScope());
342        }
343
344
345        /**
346         * Gets the required scope.
347         *
348         * @return The required scope, {@code null} if not specified.
349         */
350        public Scope getScope() {
351
352                return scope;
353        }
354
355
356        /**
357         * Sets the required scope.
358         *
359         * @param scope The required scope, {@code null} if not specified.
360         *
361         * @return A copy of this error with the specified required scope.
362         */
363        public BearerTokenError setScope(final Scope scope) {
364
365                return new BearerTokenError(getCode(),
366                                            getDescription(),
367                                            getHTTPStatusCode(),
368                                            getURI(),
369                                            getRealm(),
370                                            scope);
371        }
372
373
374        /**
375         * Returns the {@code WWW-Authenticate} HTTP response header code for 
376         * this bearer access token error response.
377         *
378         * <p>Example:
379         *
380         * <pre>
381         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
382         * </pre>
383         *
384         * @return The {@code Www-Authenticate} header value.
385         */
386        public String toWWWAuthenticateHeader() {
387
388                StringBuilder sb = new StringBuilder("Bearer");
389                
390                int numParams = 0;
391                
392                // Serialise realm, may contain double quotes
393                if (realm != null) {
394                        sb.append(" realm=\"");
395                        sb.append(getRealm().replaceAll("\"","\\\\\""));
396                        sb.append('"');
397                        
398                        numParams++;
399                }
400                
401                // Serialise error, error_description, error_uri
402                if (getCode() != null) {
403                        
404                        if (numParams > 0)
405                                sb.append(',');
406                        
407                        sb.append(" error=\"");
408                        sb.append(getCode());
409                        sb.append('"');
410                        numParams++;
411                        
412                        if (getDescription() != null) {
413
414                                if (numParams > 0)
415                                        sb.append(',');
416
417                                sb.append(" error_description=\"");
418                                sb.append(getDescription());
419                                sb.append('"');
420                                numParams++;
421                        }
422
423                        if (getURI() != null) {
424                
425                                if (numParams > 0)
426                                        sb.append(',');
427                                
428                                sb.append(" error_uri=\"");
429                                sb.append(getURI().toString()); // double quotes always escaped in URI representation
430                                sb.append('"');
431                                numParams++;
432                        }
433                }
434
435                // Serialise scope
436                if (scope != null) {
437
438                        if (numParams > 0)
439                                sb.append(',');
440
441                        sb.append(" scope=\"");
442                        sb.append(scope.toString());
443                        sb.append('"');
444                }
445
446
447                return sb.toString();
448        }
449
450
451        /**
452         * Parses an OAuth 2.0 bearer token error from the specified HTTP
453         * response {@code WWW-Authenticate} header.
454         *
455         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
456         *                Must not be {@code null}.
457         *
458         * @return The bearer token error.
459         *
460         * @throws ParseException If the {@code WWW-Authenticate} header value 
461         *                        couldn't be parsed to a Bearer token error.
462         */
463        public static BearerTokenError parse(final String wwwAuth)
464                throws ParseException {
465
466                // We must have a WWW-Authenticate header set to Bearer .*
467                if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length()))
468                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer");
469                
470                Matcher m;
471                
472                // Parse optional realm
473                m = realmPattern.matcher(wwwAuth);
474                
475                String realm = null;
476                
477                if (m.find())
478                        realm = m.group(1);
479                
480                if (realm != null)
481                        realm = realm.replace("\\\"", "\""); // strip escaped double quotes
482                
483                
484                // Parse optional error 
485                String errorCode = null;
486                String errorDescription = null;
487                URI errorURI = null;
488
489                m = errorPattern.matcher(wwwAuth);
490                
491                if (m.find()) {
492                        
493                        // Error code: try group with double quotes, else group with no quotes
494                        errorCode = m.group(2) != null ? m.group(2) : m.group(3);
495                        
496                        if (! ErrorObject.isLegal(errorCode))
497                                errorCode = null; // found invalid chars
498
499                        // Parse optional error description
500                        m = errorDescriptionPattern.matcher(wwwAuth);
501
502                        if (m.find())
503                                errorDescription = m.group(1);
504
505                        
506                        // Parse optional error URI
507                        m = errorURIPattern.matcher(wwwAuth);
508                        
509                        if (m.find()) {
510                                try {
511                                        errorURI = new URI(m.group(1));
512                                } catch (URISyntaxException e) {
513                                        // ignore, URI is not required to construct error object
514                                }
515                        }
516                }
517
518
519                Scope scope = null;
520
521                m = scopePattern.matcher(wwwAuth);
522
523                if (m.find())
524                        scope = Scope.parse(m.group(1));
525                
526
527                return new BearerTokenError(errorCode, 
528                                            errorDescription, 
529                                            0, // HTTP status code not known
530                                            errorURI, 
531                                            realm, 
532                                            scope);
533        }
534}