001package com.nimbusds.oauth2.sdk.token;
002
003
004import java.net.URI;
005import java.net.URISyntaxException;
006import java.util.regex.Matcher;
007import java.util.regex.Pattern;
008
009import net.jcip.annotations.Immutable;
010
011import org.apache.commons.lang3.StringEscapeUtils;
012
013import com.nimbusds.oauth2.sdk.ErrorObject;
014import com.nimbusds.oauth2.sdk.ParseException;
015import com.nimbusds.oauth2.sdk.Scope;
016import com.nimbusds.oauth2.sdk.http.HTTPResponse;
017
018
019/**
020 * OAuth 2.0 bearer token error. Used to indicate that access to a resource 
021 * protected by a Bearer access token is denied, due to the request or token 
022 * being invalid, or due to the access token having insufficient scope.
023 *
024 * <p>Standard bearer access token errors:
025 *
026 * <ul>
027 *     <li>{@link #MISSING_TOKEN}
028 *     <li>{@link #INVALID_REQUEST}
029 *     <li>{@link #INVALID_TOKEN}
030 *     <li>{@link #INSUFFICIENT_SCOPE}
031 * </ul>
032 *
033 * <p>Example HTTP response:
034 *
035 * <pre>
036 * HTTP/1.1 401 Unauthorized
037 * WWW-Authenticate: Bearer realm="example.com",
038 *                   error="invalid_token",
039 *                   error_description="The access token expired"
040 * </pre>
041 *
042 * <p>Related specifications:
043 *
044 * <ul>
045 *     <li>OAuth 2.0 Bearer Token Usage (RFC 6750), section 3.1.
046 * </ul>
047 */
048@Immutable
049public class BearerTokenError extends ErrorObject {
050
051
052        /**
053         * The request does not contain an access token. No error code or
054         * description is specified for this error, just the HTTP status code
055         * is set to 401 (Unauthorized).
056         *
057         * <p>Example:
058         *
059         * <pre>
060         * HTTP/1.1 401 Unauthorized
061         * WWW-Authenticate: Bearer
062         * </pre>
063         */
064        public static final BearerTokenError MISSING_TOKEN =
065                new BearerTokenError(null, null, HTTPResponse.SC_UNAUTHORIZED);
066
067        /**
068         * The request is missing a required parameter, includes an unsupported
069         * parameter or parameter value, repeats the same parameter, uses more
070         * than one method for including an access token, or is otherwise 
071         * malformed. The HTTP status code is set to 400 (Bad Request).
072         */
073        public static final BearerTokenError INVALID_REQUEST = 
074                new BearerTokenError("invalid_request", "Invalid request", 
075                                     HTTPResponse.SC_BAD_REQUEST);
076
077
078        /**
079         * The access token provided is expired, revoked, malformed, or invalid
080         * for other reasons.  The HTTP status code is set to 401 
081         * (Unauthorized).
082         */
083        public static final BearerTokenError INVALID_TOKEN =
084                new BearerTokenError("invalid_token", "Invalid access token", 
085                                     HTTPResponse.SC_UNAUTHORIZED);
086        
087        
088        /**
089         * The request requires higher privileges than provided by the access 
090         * token. The HTTP status code is set to 403 (Forbidden).
091         */
092        public static final BearerTokenError INSUFFICIENT_SCOPE =
093                new BearerTokenError("insufficient_scope", "Insufficient scope", 
094                                     HTTPResponse.SC_FORBIDDEN);
095        
096        
097        /**
098         * Regex pattern for matching the realm parameter of a WWW-Authenticate 
099         * header.
100         */
101        private static final Pattern realmPattern = Pattern.compile("realm=\"([^\"]+)");
102
103        
104        /**
105         * Regex pattern for matching the error parameter of a WWW-Authenticate 
106         * header.
107         */
108        private static final Pattern errorPattern = Pattern.compile("error=\"([^\"]+)");
109
110
111        /**
112         * Regex pattern for matching the error description parameter of a 
113         * WWW-Authenticate header.
114         */
115        private static final Pattern errorDescriptionPattern = Pattern.compile("error_description=\"([^\"]+)\"");
116        
117        
118        /**
119         * Regex pattern for matching the error URI parameter of a 
120         * WWW-Authenticate header.
121         */
122        private static final Pattern errorURIPattern = Pattern.compile("error_uri=\"([^\"]+)\"");
123
124
125        /**
126         * Regex pattern for matching the scope parameter of a WWW-Authenticate 
127         * header.
128         */
129        private static final Pattern scopePattern = Pattern.compile("scope=\"([^\"]+)");
130        
131        
132        /**
133         * The realm, {@code null} if not specified.
134         */
135        private final String realm;
136
137
138        /**
139         * Required scope, {@code null} if not specified.
140         */
141        private final Scope scope;
142        
143        
144        /**
145         * Creates a new OAuth 2.0 bearer token error with the specified code
146         * and description.
147         *
148         * @param code        The error code, {@code null} if not specified.
149         * @param description The error description, {@code null} if not
150         *                    specified.
151         */
152        public BearerTokenError(final String code, final String description) {
153        
154                this(code, description, 0, null, null, null);
155        }
156
157
158        /**
159         * Creates a new OAuth 2.0 bearer token error with the specified code,
160         * description and HTTP status code.
161         *
162         * @param code           The error code, {@code null} if not specified.
163         * @param description    The error description, {@code null} if not
164         *                       specified.
165         * @param httpStatusCode The HTTP status code, zero if not specified.
166         */
167        public BearerTokenError(final String code, final String description, final int httpStatusCode) {
168        
169                this(code, description, httpStatusCode, null, null, null);
170        }
171
172
173        /**
174         * Creates a new OAuth 2.0 bearer token error with the specified code,
175         * description, HTTP status code, page URI, realm and scope.
176         *
177         * @param code           The error code, {@code null} if not specified.
178         * @param description    The error description, {@code null} if not
179         *                       specified.
180         * @param httpStatusCode The HTTP status code, zero if not specified.
181         * @param uri            The error page URI, {@code null} if not
182         *                       specified.
183         * @param realm          The realm, {@code null} if not specified.
184         * @param scope          The required scope, {@code null} if not 
185         *                       specified.
186         */
187        public BearerTokenError(final String code, 
188                                final String description, 
189                                final int httpStatusCode, 
190                                final URI uri,
191                                final String realm,
192                                final Scope scope) {
193        
194                super(code, description, httpStatusCode, uri);
195                this.realm = realm;
196                this.scope = scope;
197        }
198
199
200        @Override
201        public BearerTokenError setDescription(final String description) {
202
203                return new BearerTokenError(super.getCode(), description, super.getHTTPStatusCode(), super.getURI(), realm, scope);
204        }
205
206
207        @Override
208        public BearerTokenError appendDescription(final String text) {
209
210                String newDescription;
211
212                if (getDescription() != null)
213                        newDescription = getDescription() + text;
214                else
215                        newDescription = text;
216
217                return new BearerTokenError(super.getCode(), newDescription, super.getHTTPStatusCode(), super.getURI(), realm, scope);
218        }
219
220
221        @Override
222        public BearerTokenError setHTTPStatusCode(final int httpStatusCode) {
223
224                return new BearerTokenError(super.getCode(), super.getDescription(), httpStatusCode, super.getURI(), realm, scope);
225        }
226
227
228        @Override
229        public BearerTokenError setURI(final URI uri) {
230
231                return new BearerTokenError(super.getCode(), super.getDescription(), super.getHTTPStatusCode(), uri, realm, scope);
232        }
233        
234        
235        /**
236         * Gets the realm.
237         *
238         * @return The realm, {@code null} if not specified.
239         */
240        public String getRealm() {
241        
242                return realm;
243        }
244
245
246        /**
247         * Sets the realm.
248         *
249         * @param realm realm, {@code null} if not specified.
250         *
251         * @return A copy of this error with the specified realm.
252         */
253        public BearerTokenError setRealm(final String realm) {
254
255                return new BearerTokenError(getCode(), 
256                                            getDescription(), 
257                                            getHTTPStatusCode(), 
258                                            getURI(), 
259                                            realm, 
260                                            getScope());
261        }
262
263
264        /**
265         * Gets the required scope.
266         *
267         * @return The required scope, {@code null} if not specified.
268         */
269        public Scope getScope() {
270
271                return scope;
272        }
273
274
275        /**
276         * Sets the required scope.
277         *
278         * @param scope The required scope, {@code null} if not specified.
279         *
280         * @return A copy of this error with the specified required scope.
281         */
282        public BearerTokenError setScope(final Scope scope) {
283
284                return new BearerTokenError(getCode(),
285                                            getDescription(),
286                                            getHTTPStatusCode(),
287                                            getURI(),
288                                            getRealm(),
289                                            scope);
290        }
291
292
293        /**
294         * Returns the {@code WWW-Authenticate} HTTP response header code for 
295         * this bearer access token error response.
296         *
297         * <p>Example:
298         *
299         * <pre>
300         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
301         * </pre>
302         *
303         * @return The {@code Www-Authenticate} header value.
304         */
305        public String toWWWAuthenticateHeader() {
306
307                StringBuilder sb = new StringBuilder("Bearer");
308                
309                int numParams = 0;
310                
311                // Serialise realm
312                if (realm != null) {
313                        sb.append(" realm=\"");
314                        sb.append(StringEscapeUtils.escapeJava(realm));
315                        sb.append('"');
316                        
317                        numParams++;
318                }
319                
320                // Serialise error, error_description, error_uri
321                if (getCode() != null) {
322                        
323                        if (numParams > 0)
324                                sb.append(',');
325                        
326                        sb.append(" error=\"");
327                        sb.append(StringEscapeUtils.escapeJava(getCode()));
328                        sb.append('"');
329                        numParams++;
330                        
331                        if (getDescription() != null) {
332
333                                if (numParams > 0)
334                                        sb.append(',');
335
336                                sb.append(" error_description=\"");
337                                sb.append(StringEscapeUtils.escapeJava(getDescription()));
338                                sb.append('"');
339                                numParams++;
340                        }
341
342                        if (getURI() != null) {
343                
344                                if (numParams > 0)
345                                        sb.append(',');
346                                
347                                sb.append(" error_uri=\"");
348                                sb.append(StringEscapeUtils.escapeJava(getURI().toString()));
349                                sb.append('"');
350                                numParams++;
351                        }
352                }
353
354                // Serialise scope
355                if (scope != null) {
356
357                        if (numParams > 0)
358                                sb.append(',');
359
360                        sb.append(" scope=\"");
361                        sb.append(StringEscapeUtils.escapeJava(scope.toString()));
362                        sb.append('"');
363                }
364
365
366                return sb.toString();
367        }
368
369
370        /**
371         * Parses an OAuth 2.0 bearer token error from the specified HTTP
372         * response {@code WWW-Authenticate} header.
373         *
374         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
375         *                Must not be {@code null}.
376         *
377         * @throws ParseException If the {@code WWW-Authenticate} header value 
378         *                        couldn't be parsed to a Bearer token error.
379         */
380        public static BearerTokenError parse(final String wwwAuth)
381                throws ParseException {
382
383                // We must have a WWW-Authenticate header set to Bearer .*
384                if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length()))
385                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer");
386                
387                Matcher m;
388                
389                // Parse optional realm
390                m = realmPattern.matcher(wwwAuth);
391                
392                String realm = null;
393                
394                if (m.find())
395                        realm = m.group(1);
396                
397                
398                // Parse optional error 
399                String errorCode = null;
400                String errorDescription = null;
401                URI errorURI = null;
402
403                m = errorPattern.matcher(wwwAuth);
404                
405                if (m.find()) {
406
407                        errorCode = m.group(1);
408
409                        // Parse optional error description
410                        m = errorDescriptionPattern.matcher(wwwAuth);
411
412                        if (m.find())
413                                errorDescription = m.group(1);
414
415                        
416                        // Parse optional error URI
417                        m = errorURIPattern.matcher(wwwAuth);
418                        
419                        if (m.find()) {
420                        
421                                try {
422                                        errorURI = new URI(m.group(1));
423                                        
424                                } catch (URISyntaxException e) {
425                                
426                                        throw new ParseException("Invalid error URI: " + m.group(1), e);
427                                }
428                        }
429                }
430
431
432                Scope scope = null;
433
434                m = scopePattern.matcher(wwwAuth);
435
436                if (m.find())
437                        scope = Scope.parse(m.group(1));
438                
439
440                return new BearerTokenError(errorCode, 
441                                            errorDescription, 
442                                            0, // HTTP status code
443                                            errorURI, 
444                                            realm, 
445                                            scope);
446        }
447}