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 #INVALID_DPOP_PROOF}
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-03), 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 acceptable JWS algorithms, {@code null} if not specified.
140         */
141        private final Set<JWSAlgorithm> jwsAlgs;
142        
143        
144        /**
145         * Creates a new OAuth 2.0 DPoP 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 DPoPTokenError(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 DPoP 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 DPoPTokenError(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 DPoP 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 DPoPTokenError(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                this(code, description, httpStatusCode, uri, realm, scope, null);
195        }
196
197
198        /**
199         * Creates a new OAuth 2.0 DPoP token error with the specified code,
200         * description, HTTP status code, page URI, realm and scope.
201         *
202         * @param code           The error code, {@code null} if not specified.
203         * @param description    The error description, {@code null} if not
204         *                       specified.
205         * @param httpStatusCode The HTTP status code, zero if not specified.
206         * @param uri            The error page URI, {@code null} if not
207         *                       specified.
208         * @param realm          The realm, {@code null} if not specified.
209         * @param scope          The required scope, {@code null} if not
210         *                       specified.
211         * @param jwsAlgs        The acceptable JWS algorithms, {@code null} if
212         *                       not specified.
213         */
214        public DPoPTokenError(final String code,
215                              final String description,
216                              final int httpStatusCode,
217                              final URI uri,
218                              final String realm,
219                              final Scope scope,
220                              final Set<JWSAlgorithm> jwsAlgs) {
221        
222                super(AccessTokenType.DPOP, code, description, httpStatusCode, uri, realm, scope);
223                
224                this.jwsAlgs = jwsAlgs;
225        }
226
227
228        @Override
229        public DPoPTokenError setDescription(final String description) {
230
231                return new DPoPTokenError(
232                        getCode(),
233                        description,
234                        getHTTPStatusCode(),
235                        getURI(),
236                        getRealm(),
237                        getScope(),
238                        getJWSAlgorithms()
239                );
240        }
241
242
243        @Override
244        public DPoPTokenError appendDescription(final String text) {
245
246                String newDescription;
247                if (getDescription() != null)
248                        newDescription = getDescription() + text;
249                else
250                        newDescription = text;
251
252                return new DPoPTokenError(
253                        getCode(),
254                        newDescription,
255                        getHTTPStatusCode(),
256                        getURI(),
257                        getRealm(),
258                        getScope(),
259                        getJWSAlgorithms()
260                );
261        }
262
263
264        @Override
265        public DPoPTokenError setHTTPStatusCode(final int httpStatusCode) {
266
267                return new DPoPTokenError(
268                        getCode(),
269                        getDescription(),
270                        httpStatusCode,
271                        getURI(),
272                        getRealm(),
273                        getScope(),
274                        getJWSAlgorithms()
275                );
276        }
277
278
279        @Override
280        public DPoPTokenError setURI(final URI uri) {
281
282                return new DPoPTokenError(
283                        getCode(),
284                        getDescription(),
285                        getHTTPStatusCode(),
286                        uri,
287                        getRealm(),
288                        getScope(),
289                        getJWSAlgorithms()
290                );
291        }
292
293
294        @Override
295        public DPoPTokenError setRealm(final String realm) {
296
297                return new DPoPTokenError(
298                        getCode(),
299                        getDescription(),
300                        getHTTPStatusCode(),
301                        getURI(),
302                        realm,
303                        getScope(),
304                        getJWSAlgorithms()
305                );
306        }
307
308
309        @Override
310        public DPoPTokenError setScope(final Scope scope) {
311
312                return new DPoPTokenError(
313                        getCode(),
314                        getDescription(),
315                        getHTTPStatusCode(),
316                        getURI(),
317                        getRealm(),
318                        scope,
319                        getJWSAlgorithms()
320                );
321        }
322        
323        
324        /**
325         * Returns the acceptable JWS algorithms.
326         *
327         * @return The acceptable JWS algorithms, {@code null} if not
328         *         specified.
329         */
330        public Set<JWSAlgorithm> getJWSAlgorithms() {
331                
332                return jwsAlgs;
333        }
334        
335        
336        /**
337         * Sets the acceptable JWS algorithms.
338         *
339         * @param jwsAlgs The acceptable JWS algorithms, {@code null} if not
340         *                specified.
341         *
342         * @return A copy of this error with the specified acceptable JWS
343         *         algorithms.
344         */
345        public DPoPTokenError setJWSAlgorithms(final Set<JWSAlgorithm> jwsAlgs) {
346                
347                return new DPoPTokenError(
348                        getCode(),
349                        getDescription(),
350                        getHTTPStatusCode(),
351                        getURI(),
352                        getRealm(),
353                        getScope(),
354                        jwsAlgs
355                );
356        }
357        
358        
359        /**
360         * Returns the {@code WWW-Authenticate} HTTP response header code for 
361         * this DPoP access token error response.
362         *
363         * <p>Example:
364         *
365         * <pre>
366         * DPoP realm="example.com", error="invalid_token", error_description="Invalid access token"
367         * </pre>
368         *
369         * @return The {@code Www-Authenticate} header value.
370         */
371        @Override
372        public String toWWWAuthenticateHeader() {
373
374                String header = super.toWWWAuthenticateHeader();
375                
376                if (CollectionUtils.isEmpty(getJWSAlgorithms())) {
377                        return header;
378                }
379                
380                StringBuilder sb = new StringBuilder(header);
381                
382                if (header.contains("=")) {
383                        sb.append(',');
384                }
385                
386                sb.append(" algs=\"");
387                
388                String delim = "";
389                for (JWSAlgorithm alg: getJWSAlgorithms()) {
390                        sb.append(delim);
391                        delim = " ";
392                        sb.append(alg.getName());
393                }
394                sb.append("\"");
395                
396                return sb.toString();
397        }
398
399
400        /**
401         * Parses an OAuth 2.0 DPoP token error from the specified HTTP
402         * response {@code WWW-Authenticate} header.
403         *
404         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
405         *                Must not be {@code null}.
406         *
407         * @return The DPoP token error.
408         *
409         * @throws ParseException If the {@code WWW-Authenticate} header value 
410         *                        couldn't be parsed to a DPoP token error.
411         */
412        public static DPoPTokenError parse(final String wwwAuth)
413                throws ParseException {
414
415                TokenSchemeError genericError = TokenSchemeError.parse(wwwAuth, AccessTokenType.DPOP);
416                
417                Set<JWSAlgorithm> jwsAlgs = null;
418                
419                Matcher m = ALGS_PATTERN.matcher(wwwAuth);
420                
421                if (m.find()) {
422                        String algsString = m.group(1);
423                        jwsAlgs = new HashSet<>();
424                        for (String algName: algsString.split("\\s+")) {
425                                jwsAlgs.add(JWSAlgorithm.parse(algName));
426                        }
427                }
428                
429                return new DPoPTokenError(
430                        genericError.getCode(),
431                        genericError.getDescription(),
432                        genericError.getHTTPStatusCode(),
433                        genericError.getURI(),
434                        genericError.getRealm(),
435                        genericError.getScope(),
436                        jwsAlgs
437                );
438        }
439}