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