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