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}