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