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.MalformedURLException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.util.*;
026
027import net.jcip.annotations.Immutable;
028
029import com.nimbusds.common.contenttype.ContentType;
030import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
031import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
032import com.nimbusds.oauth2.sdk.http.HTTPRequest;
033import com.nimbusds.oauth2.sdk.id.ClientID;
034import com.nimbusds.oauth2.sdk.token.RefreshToken;
035import com.nimbusds.oauth2.sdk.util.*;
036
037
038/**
039 * Token request. Used to obtain an
040 * {@link com.nimbusds.oauth2.sdk.token.AccessToken access token} and an
041 * optional {@link com.nimbusds.oauth2.sdk.token.RefreshToken refresh token}
042 * at the Token endpoint of the authorisation server. Supports custom request
043 * parameters.
044 *
045 * <p>Example token request with an authorisation code grant:
046 *
047 * <pre>
048 * POST /token HTTP/1.1
049 * Host: server.example.com
050 * Content-Type: application/x-www-form-URIencoded
051 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
052 *
053 * grant_type=authorization_code
054 * &amp;code=SplxlOBeZQQYbYS6WxSbIA
055 * &amp;redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
056 * </pre>
057 *
058 * <p>Related specifications:
059 *
060 * <ul>
061 *     <li>OAuth 2.0 (RFC 6749), sections 4.1.3, 4.3.2, 4.4.2 and 6.
062 *     <li>Resource Indicators for OAuth 2.0 (RFC 8707)
063 *     <li>OAuth 2.0 Incremental Authorization
064 *         (draft-ietf-oauth-incremental-authz-00)
065 * </ul>
066 */
067@Immutable
068public class TokenRequest extends AbstractOptionallyIdentifiedRequest {
069
070
071        /**
072         * The authorisation grant.
073         */
074        private final AuthorizationGrant authzGrant;
075
076
077        /**
078         * The requested scope, {@code null} if not specified.
079         */
080        private final Scope scope;
081        
082        
083        /**
084         * The resource URI(s), {@code null} if not specified.
085         */
086        private final List<URI> resources;
087        
088        
089        /**
090         * Existing refresh token for incremental authorisation of a public
091         * client, {@code null} if not specified.
092         */
093        private final RefreshToken existingGrant;
094
095
096        /**
097         * Custom request parameters.
098         */
099        private final Map<String,List<String>> customParams;
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                URL url;
419
420                try {
421                        url = getEndpointURI().toURL();
422
423                } catch (MalformedURLException e) {
424
425                        throw new SerializeException(e.getMessage(), e);
426                }
427
428                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, url);
429                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
430
431                if (getClientAuthentication() != null) {
432                        getClientAuthentication().applyTo(httpRequest);
433                }
434
435                Map<String,List<String>> params = httpRequest.getQueryParameters();
436
437                params.putAll(authzGrant.toParameters());
438
439                if (scope != null && ! scope.isEmpty()) {
440                        params.put("scope", Collections.singletonList(scope.toString()));
441                }
442
443                if (getClientID() != null) {
444                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
445                }
446                
447                if (getResources() != null) {
448                        List<String> values = new LinkedList<>();
449                        for (URI uri: resources) {
450                                if (uri == null)
451                                        continue;
452                                values.add(uri.toString());
453                        }
454                        params.put("resource", values);
455                }
456                
457                if (getExistingGrant() != null) {
458                        params.put("existing_grant", Collections.singletonList(existingGrant.getValue()));
459                }
460
461                if (! getCustomParameters().isEmpty()) {
462                        params.putAll(getCustomParameters());
463                }
464
465                httpRequest.setQuery(URLUtils.serializeParameters(params));
466
467                return httpRequest;
468        }
469
470
471        /**
472         * Parses a token request from the specified HTTP request.
473         *
474         * @param httpRequest The HTTP request. Must not be {@code null}.
475         *
476         * @return The token request.
477         *
478         * @throws ParseException If the HTTP request couldn't be parsed to a
479         *                        token request.
480         */
481        public static TokenRequest parse(final HTTPRequest httpRequest)
482                throws ParseException {
483
484                // Only HTTP POST accepted
485                URI uri;
486
487                try {
488                        uri = httpRequest.getURL().toURI();
489
490                } catch (URISyntaxException e) {
491
492                        throw new ParseException(e.getMessage(), e);
493                }
494
495                httpRequest.ensureMethod(HTTPRequest.Method.POST);
496                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
497
498                // Parse client authentication, if any
499                ClientAuthentication clientAuth;
500                
501                try {
502                        clientAuth = ClientAuthentication.parse(httpRequest);
503                } catch (ParseException e) {
504                        throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
505                }
506
507                // No fragment! May use query component!
508                Map<String,List<String>> params = httpRequest.getQueryParameters();
509                
510                // Multiple conflicting client auth methods (issue #203)?
511                if (clientAuth instanceof ClientSecretBasic) {
512                        if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion")) || StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"))) {
513                                String msg = "Multiple conflicting client authentication methods found: Basic and JWT assertion";
514                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
515                        }
516                }
517
518                // Parse grant
519                AuthorizationGrant grant = AuthorizationGrant.parse(params);
520
521                if (clientAuth == null && grant.getType().requiresClientAuthentication()) {
522                        String msg = "Missing client authentication";
523                        throw new ParseException(msg, OAuth2Error.INVALID_CLIENT.appendDescription(": " + msg));
524                }
525
526                // Parse client id
527                ClientID clientID = null;
528
529                if (clientAuth == null) {
530
531                        // Parse optional client ID
532                        String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
533
534                        if (clientIDString != null && ! clientIDString.trim().isEmpty())
535                                clientID = new ClientID(clientIDString);
536
537                        if (clientID == null && grant.getType().requiresClientID()) {
538                                String msg = "Missing required \"client_id\" parameter";
539                                throw new ParseException(msg, OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
540                        }
541                }
542
543                // Parse optional scope
544                String scopeValue = MultivaluedMapUtils.getFirstValue(params, "scope");
545
546                Scope scope = null;
547
548                if (scopeValue != null) {
549                        scope = Scope.parse(scopeValue);
550                }
551                
552                // Parse resource URIs
553                List<URI> resources = null;
554                
555                List<String> vList = params.get("resource");
556                
557                if (vList != null) {
558                        
559                        resources = new LinkedList<>();
560                        
561                        for (String uriValue: vList) {
562                                
563                                if (uriValue == null)
564                                        continue;
565                                
566                                String errMsg = "Invalid \"resource\" parameter: Must be an absolute URI and with no query or fragment: " + uriValue;
567                                
568                                URI resourceURI;
569                                try {
570                                        resourceURI = new URI(uriValue);
571                                } catch (URISyntaxException e) {
572                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
573                                }
574                                
575                                if (! ResourceUtils.isValidResourceURI(resourceURI)) {
576                                        throw new ParseException(errMsg, OAuth2Error.INVALID_RESOURCE.setDescription(errMsg));
577                                }
578                                
579                                resources.add(resourceURI);
580                        }
581                }
582                
583                String rt = MultivaluedMapUtils.getFirstValue(params, "existing_grant");
584                RefreshToken existingGrant = StringUtils.isNotBlank(rt) ? new RefreshToken(rt) : null;
585
586                // Parse custom parameters
587                Map<String,List<String>> customParams = new HashMap<>();
588
589                for (Map.Entry<String,List<String>> p: params.entrySet()) {
590
591                        if (p.getKey().equalsIgnoreCase("grant_type")) {
592                                continue; // skip
593                        }
594
595                        if (p.getKey().equalsIgnoreCase("client_id")) {
596                                continue; // skip
597                        }
598
599                        if (p.getKey().equalsIgnoreCase("client_secret")) {
600                                continue; // skip
601                        }
602
603                        if (p.getKey().equalsIgnoreCase("client_assertion_type")) {
604                                continue; // skip
605                        }
606
607                        if (p.getKey().equalsIgnoreCase("client_assertion")) {
608                                continue; // skip
609                        }
610
611                        if (p.getKey().equalsIgnoreCase("scope")) {
612                                continue; // skip
613                        }
614                        
615                        if (p.getKey().equalsIgnoreCase("resource")) {
616                                continue; // skip
617                        }
618                        
619                        if (p.getKey().equalsIgnoreCase("existing_grant"))
620                                continue; // skip
621
622                        if (! grant.getType().getRequestParameterNames().contains(p.getKey())) {
623                                // We have a custom (non-registered) parameter
624                                customParams.put(p.getKey(), p.getValue());
625                        }
626                }
627
628                if (clientAuth != null) {
629                        return new TokenRequest(uri, clientAuth, grant, scope, resources, customParams);
630                } else {
631                        // public client
632                        return new TokenRequest(uri, clientID, grant, scope, resources, existingGrant, customParams);
633                }
634        }
635}