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. Limits the realm string length to 256 chars to prevent
177         * potential stack overflow exception for very long strings due to
178         * recursive nature of regex.
179         */
180        private static final Pattern realmPattern = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\"");
181
182        
183        /**
184         * Regex pattern for matching the error parameter of a WWW-Authenticate 
185         * header. Double quoting is optional.
186         */
187        private static final Pattern errorPattern = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))");
188
189
190        /**
191         * Regex pattern for matching the error description parameter of a 
192         * WWW-Authenticate header.
193         */
194        private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\"");
195        
196        
197        /**
198         * Regex pattern for matching the error URI parameter of a 
199         * WWW-Authenticate header.
200         */
201        private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\"");
202
203
204        /**
205         * Regex pattern for matching the scope parameter of a WWW-Authenticate 
206         * header.
207         */
208        private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)");
209        
210        
211        /**
212         * The realm, {@code null} if not specified.
213         */
214        private final String realm;
215
216
217        /**
218         * Required scope, {@code null} if not specified.
219         */
220        private final Scope scope;
221        
222        
223        /**
224         * Creates a new OAuth 2.0 bearer token error with the specified code
225         * and description.
226         *
227         * @param code        The error code, {@code null} if not specified.
228         * @param description The error description, {@code null} if not
229         *                    specified.
230         */
231        public BearerTokenError(final String code, final String description) {
232        
233                this(code, description, 0, null, null, null);
234        }
235
236
237        /**
238         * Creates a new OAuth 2.0 bearer token error with the specified code,
239         * description and HTTP status code.
240         *
241         * @param code           The error code, {@code null} if not specified.
242         * @param description    The error description, {@code null} if not
243         *                       specified.
244         * @param httpStatusCode The HTTP status code, zero if not specified.
245         */
246        public BearerTokenError(final String code, final String description, final int httpStatusCode) {
247        
248                this(code, description, httpStatusCode, null, null, null);
249        }
250
251
252        /**
253         * Creates a new OAuth 2.0 bearer token error with the specified code,
254         * description, HTTP status code, page URI, realm and scope.
255         *
256         * @param code           The error code, {@code null} if not specified.
257         * @param description    The error description, {@code null} if not
258         *                       specified.
259         * @param httpStatusCode The HTTP status code, zero if not specified.
260         * @param uri            The error page URI, {@code null} if not
261         *                       specified.
262         * @param realm          The realm, {@code null} if not specified.
263         * @param scope          The required scope, {@code null} if not 
264         *                       specified.
265         */
266        public BearerTokenError(final String code, 
267                                final String description, 
268                                final int httpStatusCode, 
269                                final URI uri,
270                                final String realm,
271                                final Scope scope) {
272        
273                super(code, description, httpStatusCode, uri);
274                this.realm = realm;
275                this.scope = scope;
276                
277                if (scope != null && ! isScopeWithValidChars(scope)) {
278                        throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3");
279                }
280        }
281
282
283        @Override
284        public BearerTokenError setDescription(final String description) {
285
286                return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope);
287        }
288
289
290        @Override
291        public BearerTokenError appendDescription(final String text) {
292
293                String newDescription;
294
295                if (getDescription() != null)
296                        newDescription = getDescription() + text;
297                else
298                        newDescription = text;
299
300                return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope);
301        }
302
303
304        @Override
305        public BearerTokenError setHTTPStatusCode(final int httpStatusCode) {
306
307                return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope);
308        }
309
310
311        @Override
312        public BearerTokenError setURI(final URI uri) {
313
314                return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope);
315        }
316        
317        
318        /**
319         * Gets the realm.
320         *
321         * @return The realm, {@code null} if not specified.
322         */
323        public String getRealm() {
324        
325                return realm;
326        }
327
328
329        /**
330         * Sets the realm.
331         *
332         * @param realm realm, {@code null} if not specified.
333         *
334         * @return A copy of this error with the specified realm.
335         */
336        public BearerTokenError setRealm(final String realm) {
337
338                return new BearerTokenError(getCode(), 
339                                            getDescription(), 
340                                            getHTTPStatusCode(), 
341                                            getURI(), 
342                                            realm, 
343                                            getScope());
344        }
345
346
347        /**
348         * Gets the required scope.
349         *
350         * @return The required scope, {@code null} if not specified.
351         */
352        public Scope getScope() {
353
354                return scope;
355        }
356
357
358        /**
359         * Sets the required scope.
360         *
361         * @param scope The required scope, {@code null} if not specified.
362         *
363         * @return A copy of this error with the specified required scope.
364         */
365        public BearerTokenError setScope(final Scope scope) {
366
367                return new BearerTokenError(getCode(),
368                                            getDescription(),
369                                            getHTTPStatusCode(),
370                                            getURI(),
371                                            getRealm(),
372                                            scope);
373        }
374
375
376        /**
377         * Returns the {@code WWW-Authenticate} HTTP response header code for 
378         * this bearer access token error response.
379         *
380         * <p>Example:
381         *
382         * <pre>
383         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
384         * </pre>
385         *
386         * @return The {@code Www-Authenticate} header value.
387         */
388        public String toWWWAuthenticateHeader() {
389
390                StringBuilder sb = new StringBuilder("Bearer");
391                
392                int numParams = 0;
393                
394                // Serialise realm, may contain double quotes
395                if (realm != null) {
396                        sb.append(" realm=\"");
397                        sb.append(getRealm().replaceAll("\"","\\\\\""));
398                        sb.append('"');
399                        
400                        numParams++;
401                }
402                
403                // Serialise error, error_description, error_uri
404                if (getCode() != null) {
405                        
406                        if (numParams > 0)
407                                sb.append(',');
408                        
409                        sb.append(" error=\"");
410                        sb.append(getCode());
411                        sb.append('"');
412                        numParams++;
413                        
414                        if (getDescription() != null) {
415
416                                if (numParams > 0)
417                                        sb.append(',');
418
419                                sb.append(" error_description=\"");
420                                sb.append(getDescription());
421                                sb.append('"');
422                                numParams++;
423                        }
424
425                        if (getURI() != null) {
426                
427                                if (numParams > 0)
428                                        sb.append(',');
429                                
430                                sb.append(" error_uri=\"");
431                                sb.append(getURI().toString()); // double quotes always escaped in URI representation
432                                sb.append('"');
433                                numParams++;
434                        }
435                }
436
437                // Serialise scope
438                if (scope != null) {
439
440                        if (numParams > 0)
441                                sb.append(',');
442
443                        sb.append(" scope=\"");
444                        sb.append(scope.toString());
445                        sb.append('"');
446                }
447
448
449                return sb.toString();
450        }
451
452
453        /**
454         * Parses an OAuth 2.0 bearer token error from the specified HTTP
455         * response {@code WWW-Authenticate} header.
456         *
457         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
458         *                Must not be {@code null}.
459         *
460         * @return The bearer token error.
461         *
462         * @throws ParseException If the {@code WWW-Authenticate} header value 
463         *                        couldn't be parsed to a Bearer token error.
464         */
465        public static BearerTokenError parse(final String wwwAuth)
466                throws ParseException {
467
468                // We must have a WWW-Authenticate header set to Bearer .*
469                if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length()))
470                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer");
471                
472                Matcher m;
473                
474                // Parse optional realm
475                m = realmPattern.matcher(wwwAuth);
476                
477                String realm = null;
478                
479                if (m.find())
480                        realm = m.group(1);
481                
482                if (realm != null)
483                        realm = realm.replace("\\\"", "\""); // strip escaped double quotes
484                
485                
486                // Parse optional error 
487                String errorCode = null;
488                String errorDescription = null;
489                URI errorURI = null;
490
491                m = errorPattern.matcher(wwwAuth);
492                
493                if (m.find()) {
494                        
495                        // Error code: try group with double quotes, else group with no quotes
496                        errorCode = m.group(2) != null ? m.group(2) : m.group(3);
497                        
498                        if (! ErrorObject.isLegal(errorCode))
499                                errorCode = null; // found invalid chars
500
501                        // Parse optional error description
502                        m = errorDescriptionPattern.matcher(wwwAuth);
503
504                        if (m.find())
505                                errorDescription = m.group(1);
506
507                        
508                        // Parse optional error URI
509                        m = errorURIPattern.matcher(wwwAuth);
510                        
511                        if (m.find()) {
512                                try {
513                                        errorURI = new URI(m.group(1));
514                                } catch (URISyntaxException e) {
515                                        // ignore, URI is not required to construct error object
516                                }
517                        }
518                }
519
520
521                Scope scope = null;
522
523                m = scopePattern.matcher(wwwAuth);
524
525                if (m.find())
526                        scope = Scope.parse(m.group(1));
527                
528
529                return new BearerTokenError(errorCode, 
530                                            errorDescription, 
531                                            0, // HTTP status code not known
532                                            errorURI, 
533                                            realm, 
534                                            scope);
535        }
536}