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