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 = httpRequest.getURI();
474                httpRequest.ensureMethod(HTTPRequest.Method.POST);
475                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
476
477                // Parse client authentication, if any
478                ClientAuthentication clientAuth;
479                
480                try {
481                        clientAuth = ClientAuthentication.parse(httpRequest);
482                } catch (ParseException e) {
483                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
484                }
485
486                // No fragment! May use query component!
487                Map<String,List<String>> params = httpRequest.getQueryParameters();
488                
489                Set<String> repeatParams = MultivaluedMapUtils.getKeysWithMoreThanOneValue(params, Collections.singleton("resource"));
490                if (! repeatParams.isEmpty()) {
491                        String msg = "Parameter(s) present more than once: " + repeatParams;
492                        throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.setDescription(msg));
493                }
494                
495                // Multiple conflicting client auth methods (issue #203)?
496                if (clientAuth instanceof ClientSecretBasic) {
497                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
498                                String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
499                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
500                        }
501                }
502
503                // Parse grant
504                AuthorizationGrant grant = AuthorizationGrant.parse(params);
505
506                if (clientAuth == null && grant.getType().requiresClientAuthentication()) {
507                        String msg = "Missing client authentication";
508                        throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg));
509                }
510
511                // Parse client id
512                ClientID clientID = null;
513
514                if (clientAuth == null) {
515
516                        // Parse optional client ID
517                        String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
518
519                        if (clientIDString != null && ! clientIDString.trim().isEmpty())
520                                clientID = new ClientID(clientIDString);
521
522                        if (clientID == null && grant.getType().requiresClientID()) {
523                                String msg = "Missing required client_id parameter";
524                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
525                        }
526                }
527
528                // Parse optional scope
529                String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope");
530
531                Scope scope = null;
532
533                if (scopeValue != null) {
534                        scope = Scope.parse(scopeValue);
535                }
536                
537                // Parse resource URIs
538                List<URI> resources = null;
539                
540                List<String> vList = params.get("resource");
541                
542                if (vList != null) {
543                        
544                        resources = new LinkedList<>();
545                        
546                        for (String uriValue: vList) {
547                                
548                                if (uriValue == null)
549                                        continue;
550                                
551                                String errMsg = "Illegal resource parameter: Must be an absolute URI and with no query or fragment: " + uriValue;
552                                
553                                URI resourceURI;
554                                try {
555                                        resourceURI = new URI(uriValue);
556                                } catch (URISyntaxException e) {
557                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
558                                }
559                                
560                                if (! ResourceUtils.isValidResourceURI(resourceURI)) {
561                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
562                                }
563                                
564                                resources.add(resourceURI);
565                        }
566                }
567                
568                String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant");
569                RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null;
570
571                // Parse custom parameters
572                Map<String,List<String>> customParams = new HashMap<>();
573
574                for (Map.Entry<String,List<String>> p: params.entrySet()) {
575
576                        if (p.getKey().equalsIgnoreCase("grant_type")) {
577                                continue; // skip
578                        }
579
580                        if (p.getKey().equalsIgnoreCase("client_id")) {
581                                continue; // skip
582                        }
583
584                        if (p.getKey().equalsIgnoreCase("client_secret")) {
585                                continue; // skip
586                        }
587
588                        if (p.getKey().equalsIgnoreCase("client_assertion_type")) {
589                                continue; // skip
590                        }
591
592                        if (p.getKey().equalsIgnoreCase("client_assertion")) {
593                                continue; // skip
594                        }
595
596                        if (p.getKey().equalsIgnoreCase("scope")) {
597                                continue; // skip
598                        }
599                        
600                        if (p.getKey().equalsIgnoreCase("resource")) {
601                                continue; // skip
602                        }
603                        
604                        if (p.getKey().equalsIgnoreCase("existing_grant"))
605                                continue; // skip
606
607                        if (! grant.getType().getRequestParameterNames().contains(p.getKey())) {
608                                // We have a custom (non-registered) parameter
609                                customParams.put(p.getKey(), p.getValue());
610                        }
611                }
612
613                if (clientAuth != null) {
614                        return new TokenRequest(uri, clientAuth, grant, scope, resources, customParams);
615                } else {
616                        // public client
617                        return new TokenRequest(uri, clientID, grant, scope, resources, existingGrant, customParams);
618                }
619        }
620}