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