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 or token
039 * being invalid, or due to the access token having insufficient scope.
040 *
041 * <p>Standard DPoP access token errors:
042 *
043 * <ul>
044 *     <li>{@link #MISSING_TOKEN}
045 *     <li>{@link #INVALID_REQUEST}
046 *     <li>{@link #INVALID_TOKEN}
047 *     <li>{@link #INSUFFICIENT_SCOPE}
048 * </ul>
049 *
050 * <p>Example HTTP response:
051 *
052 * <pre>
053 * HTTP/1.1 401 Unauthorized
054 * WWW-Authenticate: DPoP realm="example.com",
055 *                   error="invalid_token",
056 *                   error_description="The access token expired"
057 * </pre>
058 *
059 * <p>Related specifications:
060 *
061 * <ul>
062 *     <li>OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer
063 *         (DPoP) (draft-ietf-oauth-dpop-03), section 7.1.
064 *     <li>Hypertext Transfer Protocol (HTTP/1.1): Authentication (RFC 7235),
065 *         section 4.1.
066 * </ul>
067 */
068@Immutable
069public class DPoPTokenError extends TokenSchemeError {
070        
071        
072        /**
073         * Regex pattern for matching the JWS algorithms parameter of a
074         * WWW-Authenticate header.
075         */
076        static final Pattern ALGS_PATTERN = Pattern.compile("algs=\"([^\"]+)");
077
078
079        /**
080         * The request does not contain an access token. No error code or
081         * description is specified for this error, just the HTTP status code
082         * is set to 401 (Unauthorized).
083         *
084         * <p>Example:
085         *
086         * <pre>
087         * HTTP/1.1 401 Unauthorized
088         * WWW-Authenticate: DPoP
089         * </pre>
090         */
091        public static final DPoPTokenError MISSING_TOKEN =
092                new DPoPTokenError(null, null, HTTPResponse.SC_UNAUTHORIZED);
093
094        /**
095         * The request is missing a required parameter, includes an unsupported
096         * parameter or parameter value, repeats the same parameter, uses more
097         * than one method for including an access token, or is otherwise
098         * malformed. The HTTP status code is set to 400 (Bad Request).
099         */
100        public static final DPoPTokenError INVALID_REQUEST =
101                new DPoPTokenError("invalid_request", "Invalid request",
102                                     HTTPResponse.SC_BAD_REQUEST);
103
104
105        /**
106         * The access token provided is expired, revoked, malformed, or invalid
107         * for other reasons.  The HTTP status code is set to 401
108         * (Unauthorized).
109         */
110        public static final DPoPTokenError INVALID_TOKEN =
111                new DPoPTokenError("invalid_token", "Invalid access token",
112                                     HTTPResponse.SC_UNAUTHORIZED);
113        
114        
115        /**
116         * The request requires higher privileges than provided by the access
117         * token. The HTTP status code is set to 403 (Forbidden).
118         */
119        public static final DPoPTokenError INSUFFICIENT_SCOPE =
120                new DPoPTokenError("insufficient_scope", "Insufficient scope",
121                                     HTTPResponse.SC_FORBIDDEN);
122        
123        
124        /**
125         * The acceptable JWS algorithms, {@code null} if not specified.
126         */
127        private final Set<JWSAlgorithm> jwsAlgs;
128        
129        
130        /**
131         * Creates a new OAuth 2.0 DPoP token error with the specified code
132         * and description.
133         *
134         * @param code        The error code, {@code null} if not specified.
135         * @param description The error description, {@code null} if not
136         *                    specified.
137         */
138        public DPoPTokenError(final String code, final String description) {
139        
140                this(code, description, 0, null, null, null);
141        }
142
143
144        /**
145         * Creates a new OAuth 2.0 DPoP token error with the specified code,
146         * description and HTTP status code.
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         * @param httpStatusCode The HTTP status code, zero if not specified.
152         */
153        public DPoPTokenError(final String code, final String description, final int httpStatusCode) {
154        
155                this(code, description, httpStatusCode, null, null, null);
156        }
157
158
159        /**
160         * Creates a new OAuth 2.0 DPoP token error with the specified code,
161         * description, HTTP status code, page URI, realm and scope.
162         *
163         * @param code           The error code, {@code null} if not specified.
164         * @param description    The error description, {@code null} if not
165         *                       specified.
166         * @param httpStatusCode The HTTP status code, zero if not specified.
167         * @param uri            The error page URI, {@code null} if not
168         *                       specified.
169         * @param realm          The realm, {@code null} if not specified.
170         * @param scope          The required scope, {@code null} if not
171         *                       specified.
172         */
173        public DPoPTokenError(final String code,
174                              final String description,
175                              final int httpStatusCode,
176                              final URI uri,
177                              final String realm,
178                              final Scope scope) {
179        
180                this(code, description, httpStatusCode, uri, realm, scope, null);
181        }
182
183
184        /**
185         * Creates a new OAuth 2.0 DPoP token error with the specified code,
186         * description, HTTP status code, page URI, realm and scope.
187         *
188         * @param code           The error code, {@code null} if not specified.
189         * @param description    The error description, {@code null} if not
190         *                       specified.
191         * @param httpStatusCode The HTTP status code, zero if not specified.
192         * @param uri            The error page URI, {@code null} if not
193         *                       specified.
194         * @param realm          The realm, {@code null} if not specified.
195         * @param scope          The required scope, {@code null} if not
196         *                       specified.
197         * @param jwsAlgs        The acceptable JWS algorithms, {@code null} if
198         *                       not specified.
199         */
200        public DPoPTokenError(final String code,
201                              final String description,
202                              final int httpStatusCode,
203                              final URI uri,
204                              final String realm,
205                              final Scope scope,
206                              final Set<JWSAlgorithm> jwsAlgs) {
207        
208                super(AccessTokenType.DPOP, code, description, httpStatusCode, uri, realm, scope);
209                
210                this.jwsAlgs = jwsAlgs;
211        }
212
213
214        @Override
215        public DPoPTokenError setDescription(final String description) {
216
217                return new DPoPTokenError(
218                        getCode(),
219                        description,
220                        getHTTPStatusCode(),
221                        getURI(),
222                        getRealm(),
223                        getScope(),
224                        getJWSAlgorithms()
225                );
226        }
227
228
229        @Override
230        public DPoPTokenError appendDescription(final String text) {
231
232                String newDescription;
233                if (getDescription() != null)
234                        newDescription = getDescription() + text;
235                else
236                        newDescription = text;
237
238                return new DPoPTokenError(
239                        getCode(),
240                        newDescription,
241                        getHTTPStatusCode(),
242                        getURI(),
243                        getRealm(),
244                        getScope(),
245                        getJWSAlgorithms()
246                );
247        }
248
249
250        @Override
251        public DPoPTokenError setHTTPStatusCode(final int httpStatusCode) {
252
253                return new DPoPTokenError(
254                        getCode(),
255                        getDescription(),
256                        httpStatusCode,
257                        getURI(),
258                        getRealm(),
259                        getScope(),
260                        getJWSAlgorithms()
261                );
262        }
263
264
265        @Override
266        public DPoPTokenError setURI(final URI uri) {
267
268                return new DPoPTokenError(
269                        getCode(),
270                        getDescription(),
271                        getHTTPStatusCode(),
272                        uri,
273                        getRealm(),
274                        getScope(),
275                        getJWSAlgorithms()
276                );
277        }
278
279
280        @Override
281        public DPoPTokenError setRealm(final String realm) {
282
283                return new DPoPTokenError(
284                        getCode(),
285                        getDescription(),
286                        getHTTPStatusCode(),
287                        getURI(),
288                        realm,
289                        getScope(),
290                        getJWSAlgorithms()
291                );
292        }
293
294
295        @Override
296        public DPoPTokenError setScope(final Scope scope) {
297
298                return new DPoPTokenError(
299                        getCode(),
300                        getDescription(),
301                        getHTTPStatusCode(),
302                        getURI(),
303                        getRealm(),
304                        scope,
305                        getJWSAlgorithms()
306                );
307        }
308        
309        
310        /**
311         * Returns the acceptable JWS algorithms.
312         *
313         * @return The acceptable JWS algorithms, {@code null} if not
314         *         specified.
315         */
316        public Set<JWSAlgorithm> getJWSAlgorithms() {
317                
318                return jwsAlgs;
319        }
320        
321        
322        /**
323         * Sets the acceptable JWS algorithms.
324         *
325         * @param jwsAlgs The acceptable JWS algorithms, {@code null} if not
326         *                specified.
327         *
328         * @return A copy of this error with the specified acceptable JWS
329         *         algorithms.
330         */
331        public DPoPTokenError setJWSAlgorithms(final Set<JWSAlgorithm> jwsAlgs) {
332                
333                return new DPoPTokenError(
334                        getCode(),
335                        getDescription(),
336                        getHTTPStatusCode(),
337                        getURI(),
338                        getRealm(),
339                        getScope(),
340                        jwsAlgs
341                );
342        }
343        
344        
345        /**
346         * Returns the {@code WWW-Authenticate} HTTP response header code for 
347         * this DPoP access token error response.
348         *
349         * <p>Example:
350         *
351         * <pre>
352         * DPoP realm="example.com", error="invalid_token", error_description="Invalid access token"
353         * </pre>
354         *
355         * @return The {@code Www-Authenticate} header value.
356         */
357        @Override
358        public String toWWWAuthenticateHeader() {
359
360                String header = super.toWWWAuthenticateHeader();
361                
362                if (CollectionUtils.isEmpty(getJWSAlgorithms())) {
363                        return header;
364                }
365                
366                StringBuilder sb = new StringBuilder(header);
367                
368                if (header.contains("=")) {
369                        sb.append(',');
370                }
371                
372                sb.append(" algs=\"");
373                
374                String delim = "";
375                for (JWSAlgorithm alg: getJWSAlgorithms()) {
376                        sb.append(delim);
377                        delim = " ";
378                        sb.append(alg.getName());
379                }
380                sb.append("\"");
381                
382                return sb.toString();
383        }
384
385
386        /**
387         * Parses an OAuth 2.0 DPoP token error from the specified HTTP
388         * response {@code WWW-Authenticate} header.
389         *
390         * @param wwwAuth The {@code WWW-Authenticate} header value to parse. 
391         *                Must not be {@code null}.
392         *
393         * @return The DPoP token error.
394         *
395         * @throws ParseException If the {@code WWW-Authenticate} header value 
396         *                        couldn't be parsed to a DPoP token error.
397         */
398        public static DPoPTokenError parse(final String wwwAuth)
399                throws ParseException {
400
401                TokenSchemeError genericError = TokenSchemeError.parse(wwwAuth, AccessTokenType.DPOP);
402                
403                Set<JWSAlgorithm> jwsAlgs = null;
404                
405                Matcher m = ALGS_PATTERN.matcher(wwwAuth);
406                
407                if (m.find()) {
408                        String algsString = m.group(1);
409                        jwsAlgs = new HashSet<>();
410                        for (String algName: algsString.split("\\s+")) {
411                                jwsAlgs.add(JWSAlgorithm.parse(algName));
412                        }
413                }
414                
415                return new DPoPTokenError(
416                        genericError.getCode(),
417                        genericError.getDescription(),
418                        genericError.getHTTPStatusCode(),
419                        genericError.getURI(),
420                        genericError.getRealm(),
421                        genericError.getScope(),
422                        jwsAlgs
423                );
424        }
425}