001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2021, 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.oauth2.sdk.ErrorObject;
022import com.nimbusds.oauth2.sdk.ParseException;
023import com.nimbusds.oauth2.sdk.Scope;
024
025import java.net.URI;
026import java.net.URISyntaxException;
027import java.util.Objects;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031
032/**
033 * The base abstract class for token scheme errors. Concrete extending classes
034 * should be immutable.
035 */
036public abstract class TokenSchemeError extends ErrorObject {
037
038
039        private static final long serialVersionUID = -1132784406578139418L;
040
041
042        /**
043         * Regex pattern for matching the realm parameter of a WWW-Authenticate
044         * header. Limits the realm string length to 256 chars to prevent
045         * potential stack overflow exception for very long strings due to
046         * recursive nature of regex.
047         */
048        static final Pattern REALM_PATTERN = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\"");
049
050
051        /**
052         * Regex pattern for matching the error parameter of a WWW-Authenticate
053         * header. Double quoting is optional.
054         */
055        static final Pattern ERROR_PATTERN = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))");
056
057
058        /**
059         * Regex pattern for matching the error description parameter of a
060         * WWW-Authenticate header.
061         */
062        static final Pattern ERROR_DESCRIPTION_PATTERN = Pattern.compile("error_description=\"([^\"]+)\"");
063
064
065        /**
066         * Regex pattern for matching the error URI parameter of a
067         * WWW-Authenticate header.
068         */
069        static final Pattern ERROR_URI_PATTERN = Pattern.compile("error_uri=\"([^\"]+)\"");
070
071
072        /**
073         * Regex pattern for matching the scope parameter of a WWW-Authenticate
074         * header.
075         */
076        static final Pattern SCOPE_PATTERN = Pattern.compile("scope=\"([^\"]+)");
077
078
079        /**
080         * The token scheme.
081         */
082        private final AccessTokenType scheme;
083
084
085        /**
086         * The realm, {@code null} if not specified.
087         */
088        private final String realm;
089
090
091        /**
092         * Required scope, {@code null} if not specified.
093         */
094        private final Scope scope;
095
096
097        /**
098         * Returns {@code true} if the specified scope consists of valid
099         * characters. Values for the "scope" attributes must not include
100         * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E]
101         * range. See RFC 6750, section 3.
102         *
103         * @see ErrorObject#isLegal(String)
104         *
105         * @param scope The scope.
106         *
107         * @return {@code true} if the scope contains valid characters, else
108         *         {@code false}.
109         */
110        public static boolean isScopeWithValidChars(final Scope scope) {
111
112                return ErrorObject.isLegal(scope.toString());
113        }
114
115
116        /**
117         * Creates a new token error with the specified code, description, HTTP
118         * status code, page URI, realm and scope.
119         *
120         * @param scheme         The token scheme. Must not be {@code null}.
121         * @param code           The error code, {@code null} if not specified.
122         * @param description    The error description, {@code null} if not
123         *                       specified.
124         * @param httpStatusCode The HTTP status code, zero if not specified.
125         * @param uri            The error page URI, {@code null} if not
126         *                       specified.
127         * @param realm          The realm, {@code null} if not specified.
128         * @param scope          The required scope, {@code null} if not
129         *                       specified.
130         */
131        protected TokenSchemeError(final AccessTokenType scheme,
132                                   final String code,
133                                   final String description,
134                                   final int httpStatusCode,
135                                   final URI uri,
136                                   final String realm,
137                                   final Scope scope) {
138
139                super(code, description, httpStatusCode, uri);
140
141                this.scheme = Objects.requireNonNull(scheme);
142                this.realm = realm;
143                this.scope = scope;
144
145                if (scope != null && ! isScopeWithValidChars(scope)) {
146                        throw new IllegalArgumentException("The scope contains illegal characters, see RFC 6750, section 3");
147                }
148        }
149
150
151        /**
152         * Returns the token scheme.
153         *
154         * @return The token scheme.
155         */
156        public AccessTokenType getScheme() {
157
158                return scheme;
159        }
160
161
162        /**
163         * Returns the realm.
164         *
165         * @return The realm, {@code null} if not specified.
166         */
167        public String getRealm() {
168
169                return realm;
170        }
171
172
173        /**
174         * Returns the required scope.
175         *
176         * @return The required scope, {@code null} if not specified.
177         */
178        public Scope getScope() {
179
180                return scope;
181        }
182
183
184        @Override
185        public abstract TokenSchemeError setDescription(final String description);
186
187
188        @Override
189        public abstract TokenSchemeError appendDescription(final String text);
190
191
192        @Override
193        public abstract TokenSchemeError setHTTPStatusCode(final int httpStatusCode);
194
195
196        @Override
197        public abstract TokenSchemeError setURI(final URI uri);
198
199
200        /**
201         * Sets the realm.
202         *
203         * @param realm realm, {@code null} if not specified.
204         *
205         * @return A copy of this error with the specified realm.
206         */
207        public abstract TokenSchemeError setRealm(final String realm);
208
209
210        /**
211         * Sets the required scope.
212         *
213         * @param scope The required scope, {@code null} if not specified.
214         *
215         * @return A copy of this error with the specified required scope.
216         */
217        public abstract TokenSchemeError setScope(final Scope scope);
218
219
220        /**
221         * Returns the {@code WWW-Authenticate} HTTP response header code for
222         * this token scheme error.
223         *
224         * <p>Example:
225         *
226         * <pre>
227         * Bearer realm="example.com", error="invalid_token", error_description="Invalid access token"
228         * </pre>
229         *
230         * @return The {@code Www-Authenticate} header value.
231         */
232        public String toWWWAuthenticateHeader() {
233
234                StringBuilder sb = new StringBuilder(getScheme().getValue());
235
236                int numParams = 0;
237
238                // Serialise realm, may contain double quotes
239                if (getRealm() != null) {
240                        sb.append(" realm=\"");
241                        sb.append(getRealm().replaceAll("\"","\\\\\""));
242                        sb.append('"');
243
244                        numParams++;
245                }
246
247                // Serialise error, error_description, error_uri
248                if (getCode() != null) {
249
250                        if (numParams > 0)
251                                sb.append(',');
252
253                        sb.append(" error=\"");
254                        sb.append(getCode());
255                        sb.append('"');
256                        numParams++;
257
258                        if (getDescription() != null) {
259                                // Output description only if code is present
260                                sb.append(',');
261                                sb.append(" error_description=\"");
262                                sb.append(getDescription());
263                                sb.append('"');
264                                numParams++;
265                        }
266
267                        if (getURI() != null) {
268                                // Output description only if code is present
269                                sb.append(',');
270                                sb.append(" error_uri=\"");
271                                sb.append(getURI().toString()); // double quotes always escaped in URI representation
272                                sb.append('"');
273                                numParams++;
274                        }
275                }
276
277                // Serialise scope
278                if (getScope() != null) {
279
280                        if (numParams > 0)
281                                sb.append(',');
282
283                        sb.append(" scope=\"");
284                        sb.append(getScope().toString());
285                        sb.append('"');
286                }
287
288                return sb.toString();
289        }
290
291
292        /**
293         * Parses an OAuth 2.0 generic token scheme error from the specified
294         * HTTP response {@code WWW-Authenticate} header.
295         *
296         * @param wwwAuth The {@code WWW-Authenticate} header value to parse.
297         *                Must not be {@code null}.
298         * @param scheme  The token scheme. Must not be {@code null}.
299         *
300         * @return The generic token scheme error.
301         *
302         * @throws ParseException If the {@code WWW-Authenticate} header value
303         *                        couldn't be parsed to a generic token scheme
304         *                        error.
305         */
306        static TokenSchemeError parse(final String wwwAuth,
307                                      final AccessTokenType scheme)
308                throws ParseException {
309
310                // We must have a WWW-Authenticate header set to <Scheme> .*
311                if (! wwwAuth.regionMatches(true, 0, scheme.getValue(), 0, scheme.getValue().length()))
312                        throw new ParseException("WWW-Authenticate scheme must be OAuth 2.0 DPoP");
313
314                Matcher m;
315
316                // Parse optional realm
317                m = REALM_PATTERN.matcher(wwwAuth);
318
319                String realm = null;
320
321                if (m.find())
322                        realm = m.group(1);
323
324                if (realm != null)
325                        realm = realm.replace("\\\"", "\""); // strip escaped double quotes
326
327
328                // Parse optional error
329                String errorCode = null;
330                String errorDescription = null;
331                URI errorURI = null;
332
333                m = ERROR_PATTERN.matcher(wwwAuth);
334
335                if (m.find()) {
336
337                        // Error code: try group with double quotes, else group with no quotes
338                        errorCode = m.group(2) != null ? m.group(2) : m.group(3);
339
340                        if (! ErrorObject.isLegal(errorCode))
341                                errorCode = null; // found invalid chars
342
343                        // Parse optional error description
344                        m = ERROR_DESCRIPTION_PATTERN.matcher(wwwAuth);
345
346                        if (m.find())
347                                errorDescription = m.group(1);
348
349
350                        // Parse optional error URI
351                        m = ERROR_URI_PATTERN.matcher(wwwAuth);
352
353                        if (m.find()) {
354                                try {
355                                        errorURI = new URI(m.group(1));
356                                } catch (URISyntaxException e) {
357                                        // ignore, URI is not required to construct error object
358                                }
359                        }
360                }
361
362
363                Scope scope = null;
364
365                m = SCOPE_PATTERN.matcher(wwwAuth);
366
367                if (m.find())
368                        scope = Scope.parse(m.group(1));
369
370
371                return new TokenSchemeError(AccessTokenType.UNKNOWN, errorCode, errorDescription, 0, errorURI, realm, scope) {
372
373                        private static final long serialVersionUID = -1629382220440634919L;
374
375
376                        @Override
377                        public TokenSchemeError setDescription(String description) {
378                                return null;
379                        }
380
381
382                        @Override
383                        public TokenSchemeError appendDescription(String text) {
384                                return null;
385                        }
386
387
388                        @Override
389                        public TokenSchemeError setHTTPStatusCode(int httpStatusCode) {
390                                return null;
391                        }
392
393
394                        @Override
395                        public TokenSchemeError setURI(URI uri) {
396                                return null;
397                        }
398
399
400                        @Override
401                        public TokenSchemeError setRealm(String realm) {
402                                return null;
403                        }
404
405
406                        @Override
407                        public TokenSchemeError setScope(Scope scope) {
408                                return null;
409                        }
410                };
411        }
412}