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 org.apache.commons.lang3.StringEscapeUtils;
029
030import com.nimbusds.oauth2.sdk.ErrorObject;
031import com.nimbusds.oauth2.sdk.ParseException;
032import com.nimbusds.oauth2.sdk.Scope;
033import com.nimbusds.oauth2.sdk.http.HTTPResponse;
034
035
036/**
037 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 
038 * protected by a Bearer access token is denied, due to the request or token 
039 * being invalid, or due to the access token having insufficient scope.
040 *
041 * <p>Standard bearer access token errors:
042 *
043 * <ul>
044 *     <li>{@link #MISSING_TOKEN}
045 *     <li>{@link #INVALID_REQUEST}
046 *     <li>{@link #INVALID_TOKEN}
047 *     <li>{@link #INSUFFICIENT_SCOPE}
048 * </ul>
049 *
050 * <p>Example HTTP response:
051 *
052 * <pre>
053 * HTTP/1.1 401 Unauthorized
054 * WWW-Authenticate: Bearer realm="example.com",
055 *                   error="invalid_token",
056 *                   error_description="The access token expired"
057 * </pre>
058 *
059 * <p>Related specifications:
060 *
061 * <ul>
062 *     <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.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         * Regex pattern for matching the realm parameter of a WWW-Authenticate 
116         * header.
117         */
118        private static final Pattern realmPattern = Pattern.compile("realm=\"([^\"]+)");
119
120        
121        /**
122         * Regex pattern for matching the error parameter of a WWW-Authenticate 
123         * header.
124         */
125        private static final Pattern errorPattern = Pattern.compile("error=\"([^\"]+)");
126
127
128        /**
129         * Regex pattern for matching the error description parameter of a 
130         * WWW-Authenticate header.
131         */
132        private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\"");
133        
134        
135        /**
136         * Regex pattern for matching the error URI parameter of a 
137         * WWW-Authenticate header.
138         */
139        private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\"");
140
141
142        /**
143         * Regex pattern for matching the scope parameter of a WWW-Authenticate 
144         * header.
145         */
146        private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)");
147        
148        
149        /**
150         * The realm, {@code null} if not specified.
151         */
152        private final String realm;
153
154
155        /**
156         * Required scope, {@code null} if not specified.
157         */
158        private final Scope scope;
159        
160        
161        /**
162         * Creates a new OAuth 2.0 bearer token error with the specified code
163         * and description.
164         *
165         * @param code        The error code, {@code null} if not specified.
166         * @param description The error description, {@code null} if not
167         *                    specified.
168         */
169        public BearerTokenError(final String code, final String description) {
170        
171                this(code, description, 0, null, null, null);
172        }
173
174
175        /**
176         * Creates a new OAuth 2.0 bearer token error with the specified code,
177         * description and HTTP status code.
178         *
179         * @param code           The error code, {@code null} if not specified.
180         * @param description    The error description, {@code null} if not
181         *                       specified.
182         * @param httpStatusCode The HTTP status code, zero if not specified.
183         */
184        public BearerTokenError(final String code, final String description, final int httpStatusCode) {
185        
186                this(code, description, httpStatusCode, null, null, null);
187        }
188
189
190        /**
191         * Creates a new OAuth 2.0 bearer token error with the specified code,
192         * description, HTTP status code, page URI, realm and scope.
193         *
194         * @param code           The error code, {@code null} if not specified.
195         * @param description    The error description, {@code null} if not
196         *                       specified.
197         * @param httpStatusCode The HTTP status code, zero if not specified.
198         * @param uri            The error page URI, {@code null} if not
199         *                       specified.
200         * @param realm          The realm, {@code null} if not specified.
201         * @param scope          The required scope, {@code null} if not 
202         *                       specified.
203         */
204        public BearerTokenError(final String code, 
205                                final String description, 
206                                final int httpStatusCode, 
207                                final URI uri,
208                                final String realm,
209                                final Scope scope) {
210        
211                super(code, description, httpStatusCode, uri);
212                this.realm = realm;
213                this.scope = scope;
214        }
215
216
217        @Override
218        public BearerTokenError setDescription(final String description) {
219
220                return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope);
221        }
222
223
224        @Override
225        public BearerTokenError appendDescription(final String text) {
226
227                String newDescription;
228
229                if (getDescription() != null)
230                        newDescription = getDescription() + text;
231                else
232                        newDescription = text;
233
234                return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope);
235        }
236
237
238        @Override
239        public BearerTokenError setHTTPStatusCode(final int httpStatusCode) {
240
241                return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope);
242        }
243
244
245        @Override
246        public BearerTokenError setURI(final URI uri) {
247
248                return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope);
249        }
250        
251        
252        /**
253         * Gets the realm.
254         *
255         * @return The realm, {@code null} if not specified.
256         */
257        public String getRealm() {
258        
259                return realm;
260        }
261
262
263        /**
264         * Sets the realm.
265         *
266         * @param realm realm, {@code null} if not specified.
267         *
268         * @return A copy of this error with the specified realm.
269         */
270        public BearerTokenError setRealm(final String realm) {
271
272                return new BearerTokenError(getCode(), 
273                                            getDescription(), 
274                                            getHTTPStatusCode(), 
275                                            getURI(), 
276                                            realm, 
277                                            getScope());
278        }
279
280
281        /**
282         * Gets the required scope.
283         *
284         * @return The required scope, {@code null} if not specified.
285         */
286        public Scope getScope() {
287
288                return scope;
289        }
290
291
292        /**
293         * Sets the required scope.
294         *
295         * @param scope The required scope, {@code null} if not specified.
296         *
297         * @return A copy of this error with the specified required scope.
298         */
299        public BearerTokenError setScope(final Scope scope) {
300
301                return new BearerTokenError(getCode(),
302                                            getDescription(),
303                                            getHTTPStatusCode(),
304                                            getURI(),
305                                            getRealm(),
306                                            scope);
307        }
308
309
310        /**
311         * Returns the {@code WWW-Authenticate} HTTP response header code for 
312         * this bearer access token error response.
313         *
314         * <p>Example:
315         *
316         * <pre>
317         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
318         * </pre>
319         *
320         * @return The {@code Www-Authenticate} header value.
321         */
322        public String toWWWAuthenticateHeader() {
323
324                StringBuilder sb = new StringBuilder("Bearer");
325                
326                int numParams = 0;
327                
328                // Serialise realm
329                if (realm != null) {
330                        sb.append(" realm=\"");
331                        sb.append(StringEscapeUtils.escapeJava(realm));
332                        sb.append('"');
333                        
334                        numParams++;
335                }
336                
337                // Serialise error, error_description, error_uri
338                if (getCode() != null) {
339                        
340                        if (numParams > 0)
341                                sb.append(',');
342                        
343                        sb.append(" error=\"");
344                        sb.append(StringEscapeUtils.escapeJava(getCode()));
345                        sb.append('"');
346                        numParams++;
347                        
348                        if (getDescription() != null) {
349
350                                if (numParams > 0)
351                                        sb.append(',');
352
353                                sb.append(" error_description=\"");
354                                sb.append(StringEscapeUtils.escapeJava(getDescription()));
355                                sb.append('"');
356                                numParams++;
357                        }
358
359                        if (getURI() != null) {
360                
361                                if (numParams > 0)
362                                        sb.append(',');
363                                
364                                sb.append(" error_uri=\"");
365                                sb.append(StringEscapeUtils.escapeJava(getURI().toString()));
366                                sb.append('"');
367                                numParams++;
368                        }
369                }
370
371                // Serialise scope
372                if (scope != null) {
373
374                        if (numParams > 0)
375                                sb.append(',');
376
377                        sb.append(" scope=\"");
378                        sb.append(StringEscapeUtils.escapeJava(scope.toString()));
379                        sb.append('"');
380                }
381
382
383                return sb.toString();
384        }
385
386
387        /**
388         * Parses an OAuth 2.0 bearer token error from the specified HTTP
389         * response {@code WWW-Authenticate} header.
390         *
391         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
392         *                Must not be {@code null}.
393         *
394         * @throws ParseException If the {@code WWW-Authenticate} header value 
395         *                        couldn't be parsed to a Bearer token error.
396         */
397        public static BearerTokenError parse(final String wwwAuth)
398                throws ParseException {
399
400                // We must have a WWW-Authenticate header set to Bearer .*
401                if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length()))
402                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer");
403                
404                Matcher m;
405                
406                // Parse optional realm
407                m = realmPattern.matcher(wwwAuth);
408                
409                String realm = null;
410                
411                if (m.find())
412                        realm = m.group(1);
413                
414                
415                // Parse optional error 
416                String errorCode = null;
417                String errorDescription = null;
418                URI errorURI = null;
419
420                m = errorPattern.matcher(wwwAuth);
421                
422                if (m.find()) {
423
424                        errorCode = m.group(1);
425
426                        // Parse optional error description
427                        m = errorDescriptionPattern.matcher(wwwAuth);
428
429                        if (m.find())
430                                errorDescription = m.group(1);
431
432                        
433                        // Parse optional error URI
434                        m = errorURIPattern.matcher(wwwAuth);
435                        
436                        if (m.find()) {
437                        
438                                try {
439                                        errorURI = new URI(m.group(1));
440                                        
441                                } catch (URISyntaxException e) {
442                                
443                                        throw new ParseException("Invalid error URI: " + m.group(1), e);
444                                }
445                        }
446                }
447
448
449                Scope scope = null;
450
451                m = scopePattern.matcher(wwwAuth);
452
453                if (m.find())
454                        scope = Scope.parse(m.group(1));
455                
456
457                return new BearerTokenError(errorCode, 
458                                            errorDescription, 
459                                            0, // HTTP status code
460                                            errorURI, 
461                                            realm, 
462                                            scope);
463        }
464}