001package com.nimbusds.oauth2.sdk.token;
002
003
004import java.net.MalformedURLException;
005import java.net.URL;
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 * @author Vladimir Dzhuvinov
049 */
050@Immutable
051public 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}