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