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;
019
020
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.*;
024
025import net.jcip.annotations.Immutable;
026
027import com.nimbusds.common.contenttype.ContentType;
028import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
029import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
030import com.nimbusds.oauth2.sdk.http.HTTPRequest;
031import com.nimbusds.oauth2.sdk.id.ClientID;
032import com.nimbusds.oauth2.sdk.token.RefreshToken;
033import com.nimbusds.oauth2.sdk.util.*;
034
035
036/**
037 * Token request. Used to obtain an
038 * {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an
039 * optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token}
040 * at the Token endpoint of the authorisation server. Supports custom request
041 * parameters.
042 *
043 * <p>Example token request with an authorisation code grant:
044 *
045 * <pre>
046 * POST /token HTTP/1.1
047 * Host: server.example.com
048 * Content-Type: application/x-www-form-URIencoded
049 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
050 *
051 * grant_type=authorization_code
052 * &amp;code=SplxlOBeZQQYbYS6WxSbIA
053 * &amp;redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
054 * </pre>
055 *
056 * <p>Related specifications:
057 *
058 * <ul>
059 *     <li>OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6.
060 *     <li>Resource Indicators for OAuth 2.0 (RFC 8707)
061 *     <li>OAuth 2.0 Incremental Authorization
062 *         (draft-ietf-oauth-incremental-authz-04)
063 * </ul>
064 */
065@Immutable
066public class TokenRequest extends AbstractOptionallyIdentifiedRequest {
067
068
069        /**
070         * The authorisation grant.
071         */
072        private final AuthorizationGrant authzGrant;
073
074
075        /**
076         * The requested scope, {@code null} if not specified.
077         */
078        private final Scope scope;
079        
080        
081        /**
082         * The resource URI(s), {@code null} if not specified.
083         */
084        private final List<URI> resources;
085        
086        
087        /**
088         * Existing refresh token for incremental authorisation of a public
089         * client, {@code null} if not specified.
090         */
091        private final RefreshToken existingGrant;
092
093
094        /**
095         * Custom request parameters.
096         */
097        private final Map<String,List<String>> customParams;
098
099        private static final Set<String> ALLOWED_REPEATED_PARAMS = new HashSet<>(Arrays.asList("resource", "audience"));
100
101
102        /**
103         * Creates a new token request with the specified client
104         * authentication.
105         *
106         * @param uri        The URI of the token endpoint. May be
107         *                   {@code null} if the {@link #toHTTPRequest} method
108         *                   will not be used.
109         * @param clientAuth The client authentication. Must not be
110         *                   {@code null}.
111         * @param authzGrant The authorisation grant. Must not be {@code null}.
112         * @param scope      The requested scope, {@code null} if not
113         *                   specified.
114         */
115        public TokenRequest(final URI uri,
116                            final ClientAuthentication clientAuth,
117                            final AuthorizationGrant authzGrant,
118                            final Scope scope) {
119
120                this(uri, clientAuth, authzGrant, scope, null, null);
121        }
122
123
124        /**
125         * Creates a new token request with the specified client
126         * authentication and extension and custom parameters.
127         *
128         * @param uri          The URI of the token endpoint. May be
129         *                     {@code null} if the {@link #toHTTPRequest}
130         *                     method will not be used.
131         * @param clientAuth   The client authentication. Must not be
132         *                     {@code null}.
133         * @param authzGrant   The authorisation grant. Must not be
134         *                     {@code null}.
135         * @param scope        The requested scope, {@code null} if not
136         *                     specified.
137         * @param resources    The resource URI(s), {@code null} if not
138         *                     specified.
139         * @param customParams Custom parameters to be included in the request
140         *                     body, empty map or {@code null} if none.
141         */
142        public TokenRequest(final URI uri,
143                            final ClientAuthentication clientAuth,
144                            final AuthorizationGrant authzGrant,
145                            final Scope scope,
146                            final List<URI> resources,
147                            final Map<String,List<String>> customParams) {
148
149                super(uri, clientAuth);
150
151                if (clientAuth == null)
152                        throw new IllegalArgumentException("The client authentication must not be null");
153
154                this.authzGrant = authzGrant;
155
156                this.scope = scope;
157                
158                if (resources != null) {
159                        for (URI resourceURI: resources) {
160                                if (! ResourceUtils.isValidResourceURI(resourceURI))
161                                        throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI);
162                        }
163                }
164                
165                this.resources = resources;
166                
167                this.existingGrant = null; // only for confidential client
168
169                if (MapUtils.isNotEmpty(customParams)) {
170                        this.customParams = customParams;
171                } else {
172                        this.customParams = Collections.emptyMap();
173                }
174        }
175
176
177        /**
178         * Creates a new token request with the specified client
179         * authentication.
180         *
181         * @param uri        The URI of the token endpoint. May be
182         *                   {@code null} if the {@link #toHTTPRequest} method
183         *                   will not be used.
184         * @param clientAuth The client authentication. Must not be
185         *                   {@code null}.
186         * @param authzGrant The authorisation grant. Must not be {@code null}.
187         */
188        public TokenRequest(final URI uri,
189                            final ClientAuthentication clientAuth,
190                            final AuthorizationGrant authzGrant) {
191
192                this(uri, clientAuth, authzGrant, null);
193        }
194
195
196        /**
197         * Creates a new token request, with no explicit client authentication
198         * (may be present in the grant depending on its type).
199         *
200         * @param uri        The URI of the token endpoint. May be
201         *                   {@code null} if the {@link #toHTTPRequest} method
202         *                   will not be used.
203         * @param clientID   The client identifier, {@code null} if not
204         *                   specified.
205         * @param authzGrant The authorisation grant. Must not be {@code null}.
206         * @param scope      The requested scope, {@code null} if not
207         *                   specified.
208         */
209        public TokenRequest(final URI uri,
210                            final ClientID clientID,
211                            final AuthorizationGrant authzGrant,
212                            final Scope scope) {
213
214                this(uri, clientID, authzGrant, scope, null, null,null);
215        }
216
217
218        /**
219         * Creates a new token request, with no explicit client authentication
220         * (may be present in the grant depending on its type) and extension
221         * and custom parameters.
222         *
223         * @param uri           The URI of the token endpoint. May be
224         *                      {@code null} if the {@link #toHTTPRequest}
225         *                      method will not be used.
226         * @param clientID      The client identifier, {@code null} if not
227         *                      specified.
228         * @param authzGrant    The authorisation grant. Must not be
229         *                      {@code null}.
230         * @param scope         The requested scope, {@code null} if not
231         *                      specified.
232         * @param resources     The resource URI(s), {@code null} if not
233         *                      specified.
234         * @param existingGrant Existing refresh token for incremental
235         *                      authorisation of a public client, {@code null}
236         *                      if not specified.
237         * @param customParams  Custom parameters to be included in the request
238         *                      body, empty map or {@code null} if none.
239         */
240        public TokenRequest(final URI uri,
241                            final ClientID clientID,
242                            final AuthorizationGrant authzGrant,
243                            final Scope scope,
244                            final List<URI> resources,
245                            final RefreshToken existingGrant,
246                            final Map<String,List<String>> customParams) {
247
248                super(uri, clientID);
249
250                if (authzGrant.getType().requiresClientAuthentication()) {
251                        throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires client authentication");
252                }
253
254                if (authzGrant.getType().requiresClientID() && clientID == null) {
255                        throw new IllegalArgumentException("The \"" + authzGrant.getType() + "\" grant type requires a \"client_id\" parameter");
256                }
257
258                this.authzGrant = authzGrant;
259
260                this.scope = scope;
261                
262                if (resources != null) {
263                        for (URI resourceURI: resources) {
264                                if (! ResourceUtils.isValidResourceURI(resourceURI))
265                                        throw new IllegalArgumentException("Resource URI must be absolute and with no query or fragment: " + resourceURI);
266                        }
267                }
268                
269                this.resources = resources;
270                
271                this.existingGrant = existingGrant;
272
273                if (MapUtils.isNotEmpty(customParams)) {
274                        this.customParams = customParams;
275                } else {
276                        this.customParams = Collections.emptyMap();
277                }
278        }
279
280
281        /**
282         * Creates a new token request, with no explicit client authentication
283         * (may be present in the grant depending on its type).
284         *
285         * @param uri        The URI of the token endpoint. May be
286         *                   {@code null} if the {@link #toHTTPRequest} method
287         *                   will not be used.
288         * @param clientID   The client identifier, {@code null} if not
289         *                   specified.
290         * @param authzGrant The authorisation grant. Must not be {@code null}.
291         */
292        public TokenRequest(final URI uri,
293                            final ClientID clientID,
294                            final AuthorizationGrant authzGrant) {
295
296                this(uri, clientID, authzGrant, null);
297        }
298
299
300        /**
301         * Creates a new token request, without client authentication and a
302         * specified client identifier.
303         *
304         * @param uri        The URI of the token endpoint. May be
305         *                   {@code null} if the {@link #toHTTPRequest} method
306         *                   will not be used.
307         * @param authzGrant The authorisation grant. Must not be {@code null}.
308         * @param scope      The requested scope, {@code null} if not
309         *                   specified.
310         */
311        public TokenRequest(final URI uri,
312                            final AuthorizationGrant authzGrant,
313                            final Scope scope) {
314
315                this(uri, (ClientID)null, authzGrant, scope);
316        }
317
318
319        /**
320         * Creates a new token request, without client authentication and a
321         * specified client identifier.
322         *
323         * @param uri        The URI of the token endpoint. May be
324         *                   {@code null} if the {@link #toHTTPRequest} method
325         *                   will not be used.
326         * @param authzGrant The authorisation grant. Must not be {@code null}.
327         */
328        public TokenRequest(final URI uri,
329                            final AuthorizationGrant authzGrant) {
330
331                this(uri, (ClientID)null, authzGrant, null);
332        }
333
334
335        /**
336         * Returns the authorisation grant.
337         *
338         * @return The authorisation grant.
339         */
340        public AuthorizationGrant getAuthorizationGrant() {
341
342                return authzGrant;
343        }
344
345
346        /**
347         * Returns the requested scope.
348         *
349         * @return The requested scope, {@code null} if not specified.
350         */
351        public Scope getScope() {
352
353                return scope;
354        }
355        
356        
357        /**
358         * Returns the resource server URI.
359         *
360         * @return The resource URI(s), {@code null} if not specified.
361         */
362        public List<URI> getResources() {
363                
364                return resources;
365        }
366        
367        
368        /**
369         * Returns the existing refresh token for incremental authorisation of
370         * a public client, {@code null} if not specified.
371         *
372         * @return The existing grant, {@code null} if not specified.
373         */
374        public RefreshToken getExistingGrant() {
375                
376                return existingGrant;
377        }
378
379
380        /**
381         * Returns the additional custom parameters included in the request
382         * body.
383         *
384         * <p>Example:
385         *
386         * <pre>
387         * resource=http://xxxxxx/PartyOData
388         * </pre>
389         *
390         * @return The additional custom parameters as a unmodifiable map,
391         *         empty map if none.
392         */
393        public Map<String,List<String>> getCustomParameters () {
394
395                return Collections.unmodifiableMap(customParams);
396        }
397
398
399        /**
400         * Returns the specified custom parameter included in the request body.
401         *
402         * @param name The parameter name. Must not be {@code null}.
403         *
404         * @return The parameter value(s), {@code null} if not specified.
405         */
406        public List<String> getCustomParameter(final String name) {
407
408                return customParams.get(name);
409        }
410
411
412        @Override
413        public HTTPRequest toHTTPRequest() {
414
415                if (getEndpointURI() == null)
416                        throw new SerializeException("The endpoint URI is not specified");
417
418                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
419                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
420
421                if (getClientAuthentication() != null) {
422                        getClientAuthentication().applyTo(httpRequest);
423                }
424
425                Map<String,List<String>> params = httpRequest.getQueryParameters();
426
427                params.putAll(authzGrant.toParameters());
428
429                if (scope != null && ! scope.isEmpty()) {
430                        params.put("scope", Collections.singletonList(scope.toString()));
431                }
432
433                if (getClientID() != null) {
434                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
435                }
436                
437                if (getResources() != null) {
438                        List<String> values = new LinkedList<>();
439                        for (URI uri: resources) {
440                                if (uri == null)
441                                        continue;
442                                values.add(uri.toString());
443                        }
444                        params.put("resource", values);
445                }
446                
447                if (getExistingGrant() != null) {
448                        params.put("existing_grant", Collections.singletonList(existingGrant.getValue()));
449                }
450
451                if (! getCustomParameters().isEmpty()) {
452                        params.putAll(getCustomParameters());
453                }
454
455                httpRequest.setQuery(URLUtils.serializeParameters(params));
456
457                return httpRequest;
458        }
459
460
461        /**
462         * Parses a token request from the specified HTTP request.
463         *
464         * @param httpRequest The HTTP request. Must not be {@code null}.
465         *
466         * @return The token request.
467         *
468         * @throws ParseException If the HTTP request couldn't be parsed to a
469         *                        token request.
470         */
471        public static TokenRequest parse(final HTTPRequest httpRequest)
472                throws ParseException {
473
474                // Only HTTP POST accepted
475                URI uri = httpRequest.getURI();
476                httpRequest.ensureMethod(HTTPRequest.Method.POST);
477                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
478
479                // Parse client authentication, if any
480                ClientAuthentication clientAuth;
481                
482                try {
483                        clientAuth = ClientAuthentication.parse(httpRequest);
484                } catch (ParseException e) {
485                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
486                }
487
488                // No fragment! May use query component!
489                Map<String,List<String>> params = httpRequest.getQueryParameters();
490                
491                Set<String> repeatParams = MultivaluedMapUtils.getKeysWithMoreThanOneValue(params, ALLOWED_REPEATED_PARAMS);
492                if (! repeatParams.isEmpty()) {
493                        String msg = "Parameter(s) present more than once: " + repeatParams;
494                        throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.setDescription(msg));
495                }
496                
497                // Multiple conflicting client auth methods (issue #203)?
498                if (clientAuth instanceof ClientSecretBasic) {
499                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
500                                String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
501                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
502                        }
503                }
504
505                // Parse grant
506                AuthorizationGrant grant = AuthorizationGrant.parse(params);
507
508                if (clientAuth == null && grant.getType().requiresClientAuthentication()) {
509                        String msg = "Missing client authentication";
510                        throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg));
511                }
512
513                // Parse client id
514                ClientID clientID = null;
515
516                if (clientAuth == null) {
517
518                        // Parse optional client ID
519                        String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
520
521                        if (clientIDString != null && ! clientIDString.trim().isEmpty())
522                                clientID = new ClientID(clientIDString);
523
524                        if (clientID == null && grant.getType().requiresClientID()) {
525                                String msg = "Missing required client_id parameter";
526                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
527                        }
528                }
529
530                // Parse optional scope
531                String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope");
532
533                Scope scope = null;
534
535                if (scopeValue != null) {
536                        scope = Scope.parse(scopeValue);
537                }
538                
539                // Parse resource URIs
540                List<URI> resources = null;
541                
542                List<String> vList = params.get("resource");
543                
544                if (vList != null) {
545                        
546                        resources = new LinkedList<>();
547                        
548                        for (String uriValue: vList) {
549                                
550                                if (uriValue == null)
551                                        continue;
552                                
553                                String errMsg = "Illegal resource parameter: Must be an absolute URI without a fragment: " + uriValue;
554                                
555                                URI resourceURI;
556                                try {
557                                        resourceURI = new URI(uriValue);
558                                } catch (URISyntaxException e) {
559                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
560                                }
561                                
562                                if (! ResourceUtils.isValidResourceURI(resourceURI)) {
563                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
564                                }
565                                
566                                resources.add(resourceURI);
567                        }
568                }
569                
570                String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant");
571                RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null;
572
573                // Parse custom parameters
574                Map<String,List<String>> customParams = new HashMap<>();
575
576                for (Map.Entry<String,List<String>> p: params.entrySet()) {
577
578                        if (p.getKey().equalsIgnoreCase("grant_type")) {
579                                continue; // skip
580                        }
581
582                        if (p.getKey().equalsIgnoreCase("client_id")) {
583                                continue; // skip
584                        }
585
586                        if (p.getKey().equalsIgnoreCase("client_secret")) {
587                                continue; // skip
588                        }
589
590                        if (p.getKey().equalsIgnoreCase("client_assertion_type")) {
591                                continue; // skip
592                        }
593
594                        if (p.getKey().equalsIgnoreCase("client_assertion")) {
595                                continue; // skip
596                        }
597
598                        if (p.getKey().equalsIgnoreCase("scope")) {
599                                continue; // skip
600                        }
601                        
602                        if (p.getKey().equalsIgnoreCase("resource")) {
603                                continue; // skip
604                        }
605                        
606                        if (p.getKey().equalsIgnoreCase("existing_grant"))
607                                continue; // skip
608
609                        if (! grant.getType().getRequestParameterNames().contains(p.getKey())) {
610                                // We have a custom (non-registered) parameter
611                                customParams.put(p.getKey(), p.getValue());
612                        }
613                }
614
615                if (clientAuth != null) {
616                        return new TokenRequest(uri, clientAuth, grant, scope, resources, customParams);
617                } else {
618                        // public client
619                        return new TokenRequest(uri, clientID, grant, scope, resources, existingGrant, customParams);
620                }
621        }
622}