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