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