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        /**
201         * Gets the realm.
202         *
203         * @return The realm, {@code null} if not specified.
204         */
205        public String getRealm() {
206        
207                return realm;
208        }
209
210
211        /**
212         * Sets the realm.
213         *
214         * @param realm realm, {@code null} if not specified.
215         *
216         * @return A copy of this error with the specified realm.
217         */
218        public BearerTokenError setRealm(final String realm) {
219
220                return new BearerTokenError(getCode(), 
221                                            getDescription(), 
222                                            getHTTPStatusCode(), 
223                                            getURI(), 
224                                            realm, 
225                                            getScope());
226        }
227
228
229        /**
230         * Gets the required scope.
231         *
232         * @return The required scope, {@code null} if not specified.
233         */
234        public Scope getScope() {
235
236                return scope;
237        }
238
239
240        /**
241         * Sets the required scope.
242         *
243         * @param scope The required scope, {@code null} if not specified.
244         *
245         * @return A copy of this error with the specified required scope.
246         */
247        public BearerTokenError setScope(final Scope scope) {
248
249                return new BearerTokenError(getCode(),
250                                            getDescription(),
251                                            getHTTPStatusCode(),
252                                            getURI(),
253                                            getRealm(),
254                                            scope);
255        }
256
257
258        /**
259         * Returns the {@code WWW-Authenticate} HTTP response header code for 
260         * this bearer access token error response.
261         *
262         * <p>Example:
263         *
264         * <pre>
265         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
266         * </pre>
267         *
268         * @return The {@code Www-Authenticate} header value.
269         */
270        public String toWWWAuthenticateHeader() {
271
272                StringBuilder sb = new StringBuilder("Bearer");
273                
274                int numParams = 0;
275                
276                // Serialise realm
277                if (realm != null) {
278                        sb.append(" realm=\"");
279                        sb.append(StringEscapeUtils.escapeJava(realm));
280                        sb.append('"');
281                        
282                        numParams++;
283                }
284                
285                // Serialise error, error_description, error_uri
286                if (getCode() != null) {
287                        
288                        if (numParams > 0)
289                                sb.append(',');
290                        
291                        sb.append(" error=\"");
292                        sb.append(StringEscapeUtils.escapeJava(getCode()));
293                        sb.append('"');
294                        numParams++;
295                        
296                        if (getDescription() != null) {
297
298                                if (numParams > 0)
299                                        sb.append(',');
300
301                                sb.append(" error_description=\"");
302                                sb.append(StringEscapeUtils.escapeJava(getDescription()));
303                                sb.append('"');
304                                numParams++;
305                        }
306
307                        if (getURI() != null) {
308                
309                                if (numParams > 0)
310                                        sb.append(',');
311                                
312                                sb.append(" error_uri=\"");
313                                sb.append(StringEscapeUtils.escapeJava(getURI().toString()));
314                                sb.append('"');
315                                numParams++;
316                        }
317                }
318
319                // Serialise scope
320                if (scope != null) {
321
322                        if (numParams > 0)
323                                sb.append(',');
324
325                        sb.append(" scope=\"");
326                        sb.append(StringEscapeUtils.escapeJava(scope.toString()));
327                        sb.append('"');
328                }
329
330
331                return sb.toString();
332        }
333
334
335        /**
336         * Parses an OAuth 2.0 bearer token error from the specified HTTP
337         * response {@code WWW-Authenticate} header.
338         *
339         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
340         *                Must not be {@code null}.
341         *
342         * @throws ParseException If the {@code WWW-Authenticate} header value 
343         *                        couldn't be parsed to a Bearer token error.
344         */
345        public static BearerTokenError parse(final String wwwAuth)
346                throws ParseException {
347
348                // We must have a WWW-Authenticate header set to Bearer .*
349                if (! wwwAuth.regionMatches(true, 0, "Bearer", 0, "Bearer".length()))
350                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 Bearer");
351                
352                Matcher m;
353                
354                // Parse optional realm
355                m = realmPattern.matcher(wwwAuth);
356                
357                String realm = null;
358                
359                if (m.find())
360                        realm = m.group(1);
361                
362                
363                // Parse optional error 
364                String errorCode = null;
365                String errorDescription = null;
366                URI errorURI = null;
367
368                m = errorPattern.matcher(wwwAuth);
369                
370                if (m.find()) {
371
372                        errorCode = m.group(1);
373
374                        // Parse optional error description
375                        m = errorDescriptionPattern.matcher(wwwAuth);
376
377                        if (m.find())
378                                errorDescription = m.group(1);
379
380                        
381                        // Parse optional error URI
382                        m = errorURIPattern.matcher(wwwAuth);
383                        
384                        if (m.find()) {
385                        
386                                try {
387                                        errorURI = new URI(m.group(1));
388                                        
389                                } catch (URISyntaxException e) {
390                                
391                                        throw new ParseException("Invalid error URI: " + m.group(1), e);
392                                }
393                        }
394                }
395
396
397                Scope scope = null;
398
399                m = scopePattern.matcher(wwwAuth);
400
401                if (m.find())
402                        scope = Scope.parse(m.group(1));
403                
404
405                return new BearerTokenError(errorCode, 
406                                            errorDescription, 
407                                            0, // HTTP status code
408                                            errorURI, 
409                                            realm, 
410                                            scope);
411        }
412}