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 java.net.URI;
022import java.net.URISyntaxException;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import com.nimbusds.oauth2.sdk.ErrorObject;
027import com.nimbusds.oauth2.sdk.ParseException;
028import com.nimbusds.oauth2.sdk.Scope;
029
030
031/**
032 * The base abstract class for token scheme errors. Concrete extending classes
033 * should be immutable.
034 */
035public abstract class TokenSchemeError extends ErrorObject {
036        
037        
038        /**
039         * Regex pattern for matching the realm parameter of a WWW-Authenticate
040         * header. Limits the realm string length to 256 chars to prevent
041         * potential stack overflow exception for very long strings due to
042         * recursive nature of regex.
043         */
044        static final Pattern REALM_PATTERN = Pattern.compile("realm=\"(([^\\\\\"]|\\\\.){0,256})\"");
045        
046        
047        /**
048         * Regex pattern for matching the error parameter of a WWW-Authenticate
049         * header. Double quoting is optional.
050         */
051        static final Pattern ERROR_PATTERN = Pattern.compile("error=(\"([\\w\\_-]+)\"|([\\w\\_-]+))");
052        
053        
054        /**
055         * Regex pattern for matching the error description parameter of a
056         * WWW-Authenticate header.
057         */
058        static final Pattern ERROR_DESCRIPTION_PATTERN = Pattern.compile("error_description=\"([^\"]+)\"");
059        
060        
061        /**
062         * Regex pattern for matching the error URI parameter of a
063         * WWW-Authenticate header.
064         */
065        static final Pattern ERROR_URI_PATTERN = Pattern.compile("error_uri=\"([^\"]+)\"");
066        
067        
068        /**
069         * Regex pattern for matching the scope parameter of a WWW-Authenticate
070         * header.
071         */
072        static final Pattern SCOPE_PATTERN = Pattern.compile("scope=\"([^\"]+)");
073        
074        
075        /**
076         * The token scheme.
077         */
078        private final AccessTokenType scheme;
079        
080        
081        /**
082         * The realm, {@code null} if not specified.
083         */
084        private final String realm;
085        
086        
087        /**
088         * Required scope, {@code null} if not specified.
089         */
090        private final Scope scope;
091        
092        
093        /**
094         * Returns {@code true} if the specified scope consists of valid
095         * characters. Values for the "scope" attributes must not include
096         * characters outside the [0x20, 0x21] | [0x23 - 0x5B] | [0x5D - 0x7E]
097         * range. See RFC 6750, section 3.
098         *
099         * @see ErrorObject#isLegal(String)
100         *
101         * @param scope The scope.
102         *
103         * @return {@code true} if the scope contains valid characters, else
104         *         {@code false}.
105         */
106        public static boolean isScopeWithValidChars(final Scope scope) {
107                
108                return ErrorObject.isLegal(scope.toString());
109        }
110        
111        
112        /**
113         * Creates a new token error with the specified code, description, HTTP
114         * status code, page URI, realm and scope.
115         *
116         * @param scheme         The token scheme. Must not be {@code null}.
117         * @param code           The error code, {@code null} if not specified.
118         * @param description    The error description, {@code null} if not
119         *                       specified.
120         * @param httpStatusCode The HTTP status code, zero if not specified.
121         * @param uri            The error page URI, {@code null} if not
122         *                       specified.
123         * @param realm          The realm, {@code null} if not specified.
124         * @param scope          The required scope, {@code null} if not
125         *                       specified.
126         */
127        protected TokenSchemeError(final AccessTokenType scheme,
128                                   final String code,
129                                   final String description,
130                                   final int httpStatusCode,
131                                   final URI uri,
132                                   final String realm,
133                                   final Scope scope) {
134                
135                super(code, description, httpStatusCode, uri);
136                
137                if (scheme == null) {
138                        throw new IllegalArgumentException("The token scheme must not be null");
139                }
140                this.scheme = scheme;
141                
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                        @Override
373                        public TokenSchemeError setDescription(String description) {
374                                return null;
375                        }
376                        
377                        
378                        @Override
379                        public TokenSchemeError appendDescription(String text) {
380                                return null;
381                        }
382                        
383                        
384                        @Override
385                        public TokenSchemeError setHTTPStatusCode(int httpStatusCode) {
386                                return null;
387                        }
388                        
389                        
390                        @Override
391                        public TokenSchemeError setURI(URI uri) {
392                                return null;
393                        }
394                        
395                        
396                        @Override
397                        public TokenSchemeError setRealm(String realm) {
398                                return null;
399                        }
400                        
401                        
402                        @Override
403                        public TokenSchemeError setScope(Scope scope) {
404                                return null;
405                        }
406                };
407        }
408}