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.device;
019
020
021import com.nimbusds.common.contenttype.ContentType;
022import com.nimbusds.oauth2.sdk.*;
023import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
024import com.nimbusds.oauth2.sdk.http.HTTPRequest;
025import com.nimbusds.oauth2.sdk.id.ClientID;
026import com.nimbusds.oauth2.sdk.util.MapUtils;
027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
028import com.nimbusds.oauth2.sdk.util.StringUtils;
029import com.nimbusds.oauth2.sdk.util.URLUtils;
030import net.jcip.annotations.Immutable;
031
032import java.net.URI;
033import java.util.*;
034
035
036/**
037 * Device authorisation request. Used to start the authorisation flow for
038 * browserless and input constraint devices. Supports custom request
039 * parameters.
040 *
041 * <p>Extending classes may define additional request parameters as well as
042 * enforce tighter requirements on the base parameters.
043 *
044 * <p>Example HTTP request:
045 *
046 * <pre>
047 * POST /device_authorization HTTP/1.1
048 * Host: server.example.com
049 * Content-Type: application/x-www-form-urlencoded
050 *
051 * client_id=459691054427
052 * </pre>
053 *
054 * <p>Related specifications:
055 *
056 * <ul>
057 *     <li>OAuth 2.0 Device Authorization Grant (RFC 8628)
058 * </ul>
059 */
060@Immutable
061public class DeviceAuthorizationRequest extends AbstractOptionallyIdentifiedRequest {
062
063        
064        /**
065         * The registered parameter names.
066         */
067        private static final Set<String> REGISTERED_PARAMETER_NAMES;
068
069        static {
070                Set<String> p = new HashSet<>();
071
072                p.add("client_id");
073                p.add("scope");
074
075                REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p);
076        }
077
078
079        /**
080         * The scope (optional).
081         */
082        private final Scope scope;
083
084
085        /**
086         * Custom parameters.
087         */
088        private final Map<String, List<String>> customParams;
089
090
091        /**
092         * Builder for constructing authorisation requests.
093         */
094        public static class Builder {
095
096                /**
097                 * The endpoint URI (optional).
098                 */
099                private URI endpoint;
100
101
102                /**
103                 * The client authentication (optional).
104                 */
105                private final ClientAuthentication clientAuth;
106
107
108                /**
109                 * The client identifier (required if not authenticated).
110                 */
111                private final ClientID clientID;
112
113
114                /**
115                 * The scope (optional).
116                 */
117                private Scope scope;
118
119
120                /**
121                 * Custom parameters.
122                 */
123                private final Map<String, List<String>> customParams = new HashMap<>();
124
125
126                /**
127                 * Creates a new device authorization request builder.
128                 *
129                 * @param clientID The client identifier. Corresponds to the
130                 *                 {@code client_id} parameter. Must not be
131                 *                 {@code null}.
132                 */
133                public Builder(final ClientID clientID) {
134                        this.clientID = Objects.requireNonNull(clientID);
135                        this.clientAuth = null;
136                }
137
138
139                /**
140                 * Creates a new device authorization request builder for an
141                 * authenticated request.
142                 *
143                 * @param clientAuth The client authentication. Must not be
144                 *                   {@code null}.
145                 */
146                public Builder(final ClientAuthentication clientAuth) {
147                        this.clientID = null;
148                        this.clientAuth = Objects.requireNonNull(clientAuth);
149                }
150
151
152                /**
153                 * Creates a new device authorization request builder from the
154                 * specified request.
155                 *
156                 * @param request The device authorization request. Must not be
157                 *                {@code null}.
158                 */
159                public Builder(final DeviceAuthorizationRequest request) {
160
161                        endpoint = request.getEndpointURI();
162                        clientAuth = request.getClientAuthentication();
163                        scope = request.scope;
164                        clientID = request.getClientID();
165                        customParams.putAll(request.getCustomParameters());
166                }
167
168
169                /**
170                 * Sets the scope. Corresponds to the optional {@code scope}
171                 * parameter.
172                 *
173                 * @param scope The scope, {@code null} if not specified.
174                 *
175                 * @return This builder.
176                 */
177                public Builder scope(final Scope scope) {
178
179                        this.scope = scope;
180                        return this;
181                }
182
183
184                /**
185                 * Sets a custom parameter.
186                 *
187                 * @param name   The parameter name. Must not be {@code null}.
188                 * @param values The parameter values, {@code null} if not
189                 *               specified.
190                 *
191                 * @return This builder.
192                 */
193                public Builder customParameter(final String name, final String... values) {
194
195                        if (values == null || values.length == 0) {
196                                customParams.remove(name);
197                        } else {
198                                customParams.put(name, Arrays.asList(values));
199                        }
200
201                        return this;
202                }
203
204
205                /**
206                 * Sets the URI of the device authorisation endpoint.
207                 *
208                 * @param endpoint The URI of the device authorisation
209                 *                 endpoint. May be {@code null} if the request
210                 *                 is not going to be serialised.
211                 *
212                 * @return This builder.
213                 */
214                public Builder endpointURI(final URI endpoint) {
215
216                        this.endpoint = endpoint;
217                        return this;
218                }
219
220
221                /**
222                 * Builds a new device authorization request.
223                 *
224                 * @return The device authorization request.
225                 */
226                public DeviceAuthorizationRequest build() {
227
228                        try {
229                                if (clientAuth == null) {
230                                        return new DeviceAuthorizationRequest(endpoint, clientID, scope, customParams);
231                                } else {
232                                        return new DeviceAuthorizationRequest(endpoint, clientAuth, scope, customParams);
233                                }
234                        } catch (IllegalArgumentException e) {
235                                throw new IllegalStateException(e.getMessage(), e);
236                        }
237                }
238        }
239
240
241        /**
242         * Creates a new minimal device authorization request.
243         *
244         * @param endpoint The URI of the device authorization endpoint. May be
245         *                 {@code null} if the {@link #toHTTPRequest} method
246         *                 is not going to be used.
247         * @param clientID The client identifier. Corresponds to the
248         *                 {@code client_id} parameter. Must not be
249         *                 {@code null}.
250         */
251        public DeviceAuthorizationRequest(final URI endpoint, final ClientID clientID) {
252
253                this(endpoint, clientID, null, null);
254        }
255
256
257        /**
258         * Creates a new device authorization request.
259         *
260         * @param endpoint The URI of the device authorization endpoint. May be
261         *                 {@code null} if the {@link #toHTTPRequest} method
262         *                 is not going to be used.
263         * @param clientID The client identifier. Corresponds to the
264         *                 {@code client_id} parameter. Must not be
265         *                 {@code null}.
266         * @param scope    The request scope. Corresponds to the optional
267         *                 {@code scope} parameter. {@code null} if not
268         *                 specified.
269         */
270        public DeviceAuthorizationRequest(final URI endpoint, final ClientID clientID, final Scope scope) {
271
272                this(endpoint, clientID, scope, null);
273        }
274
275
276        /**
277         * Creates a new device authorization request with extension and custom
278         * parameters.
279         *
280         * @param endpoint     The URI of the device authorization endpoint.
281         *                     May be {@code null} if the {@link #toHTTPRequest}
282         *                     method is not going to be used.
283         * @param clientID     The client identifier. Corresponds to the
284         *                     {@code client_id} parameter. Must not be
285         *                     {@code null}.
286         * @param scope        The request scope. Corresponds to the optional
287         *                     {@code scope} parameter. {@code null} if not
288         *                     specified.
289         * @param customParams Custom parameters, empty map or {@code null} if
290         *                     none.
291         */
292        public DeviceAuthorizationRequest(final URI endpoint,
293                                          final ClientID clientID,
294                                          final Scope scope,
295                                          final Map<String, List<String>> customParams) {
296
297                super(endpoint, Objects.requireNonNull(clientID));
298
299                this.scope = scope;
300
301                if (MapUtils.isNotEmpty(customParams)) {
302                        this.customParams = Collections.unmodifiableMap(customParams);
303                } else {
304                        this.customParams = Collections.emptyMap();
305                }
306        }
307
308
309        /**
310         * Creates a new authenticated device authorization request with
311         * extension and custom parameters.
312         *
313         * @param uri          The URI of the device authorization endpoint.
314         *                     May be {@code null} if the {@link #toHTTPRequest}
315         *                     method will not be used.
316         * @param clientAuth   The client authentication. Must not be
317         *                     {@code null}.
318         * @param scope        The request scope. Corresponds to the optional
319         *                     {@code scope} parameter. {@code null} if not
320         *                     specified.
321         * @param customParams Custom parameters, empty map or {@code null} if
322         *                     none.
323         */
324        public DeviceAuthorizationRequest(final URI uri,
325                                          final ClientAuthentication clientAuth,
326                                          final Scope scope,
327                                          final Map<String, List<String>> customParams) {
328
329                super(uri, Objects.requireNonNull(clientAuth));
330
331                this.scope = scope;
332
333                if (MapUtils.isNotEmpty(customParams)) {
334                        this.customParams = Collections.unmodifiableMap(customParams);
335                } else {
336                        this.customParams = Collections.emptyMap();
337                }
338        }
339
340
341        /**
342         * Returns the registered (standard) OAuth 2.0 device authorization
343         * request parameter names.
344         *
345         * @return The registered OAuth 2.0 device authorization request
346         *         parameter names, as an unmodifiable set.
347         */
348        public static Set<String> getRegisteredParameterNames() {
349
350                return REGISTERED_PARAMETER_NAMES;
351        }
352
353
354        /**
355         * Gets the scope. Corresponds to the optional {@code scope} parameter.
356         *
357         * @return The scope, {@code null} if not specified.
358         */
359        public Scope getScope() {
360
361                return scope;
362        }
363
364
365        /**
366         * Returns the additional custom parameters.
367         *
368         * @return The additional custom parameters as an unmodifiable map,
369         *         empty map if none.
370         */
371        public Map<String, List<String>> getCustomParameters() {
372
373                return customParams;
374        }
375
376
377        /**
378         * Returns the specified custom parameter.
379         *
380         * @param name The parameter name. Must not be {@code null}.
381         *
382         * @return The parameter value(s), {@code null} if not specified.
383         */
384        public List<String> getCustomParameter(final String name) {
385
386                return customParams.get(name);
387        }
388
389
390        /**
391         * Returns the matching HTTP request.
392         *
393         * @return The HTTP request.
394         */
395        @Override
396        public HTTPRequest toHTTPRequest() {
397
398                if (getEndpointURI() == null)
399                        throw new SerializeException("The endpoint URI is not specified");
400
401                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
402                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
403                httpRequest.setAccept(ContentType.APPLICATION_JSON.getType()); // iss #451
404
405                if (getClientAuthentication() != null) {
406                        getClientAuthentication().applyTo(httpRequest);
407                }
408
409                Map<String, List<String>> params;
410                try {
411                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
412                } catch (ParseException e) {
413                        throw new SerializeException(e.getMessage(), e);
414                }
415
416                if (scope != null && !scope.isEmpty()) {
417                        params.put("scope", Collections.singletonList(scope.toString()));
418                }
419
420                if (getClientID() != null) {
421                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
422                }
423
424                if (!getCustomParameters().isEmpty()) {
425                        params.putAll(getCustomParameters());
426                }
427
428                httpRequest.setBody(URLUtils.serializeParameters(params));
429                return httpRequest;
430        }
431
432
433        /**
434         * Parses a device authorization request from the specified HTTP
435         * request.
436         *
437         * <p>Example HTTP request (GET):
438         *
439         * <pre>
440         * POST /device_authorization HTTP/1.1
441         * Host: server.example.com
442         * Content-Type: application/x-www-form-urlencoded
443         *
444         * client_id=459691054427
445         * </pre>
446         *
447         * @param httpRequest The HTTP request. Must not be {@code null}.
448         *
449         * @return The device authorization request.
450         *
451         * @throws ParseException If the HTTP request couldn't be parsed to an
452         *                        device authorization request.
453         */
454        public static DeviceAuthorizationRequest parse(final HTTPRequest httpRequest) throws ParseException {
455
456                // Only HTTP POST accepted
457                URI uri = httpRequest.getURI();
458                httpRequest.ensureMethod(HTTPRequest.Method.POST);
459                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
460
461                // Parse client authentication, if any
462                ClientAuthentication clientAuth;
463                try {
464                        clientAuth = ClientAuthentication.parse(httpRequest);
465                } catch (ParseException e) {
466                        throw new ParseException(e.getMessage(),
467                                        OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
468                }
469
470                Map<String, List<String>> params = httpRequest.getBodyAsFormParameters();
471
472                ClientID clientID;
473                String v;
474
475                if (clientAuth == null) {
476                        // Parse mandatory client ID for unauthenticated requests
477                        v = MultivaluedMapUtils.getFirstValue(params, "client_id");
478
479                        if (StringUtils.isBlank(v)) {
480                                String msg = "Missing client_id parameter";
481                                throw new ParseException(msg,
482                                                OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
483                        }
484
485                        clientID = new ClientID(v);
486                } else {
487                        clientID = null;
488                }
489
490                // Parse optional scope
491                v = MultivaluedMapUtils.getFirstValue(params, "scope");
492
493                Scope scope = null;
494
495                if (StringUtils.isNotBlank(v))
496                        scope = Scope.parse(v);
497
498                // Parse custom parameters
499                Map<String, List<String>> customParams = null;
500
501                for (Map.Entry<String, List<String>> p : params.entrySet()) {
502
503                        if (!REGISTERED_PARAMETER_NAMES.contains(p.getKey())) {
504                                // We have a custom parameter
505                                if (customParams == null) {
506                                        customParams = new HashMap<>();
507                                }
508                                customParams.put(p.getKey(), p.getValue());
509                        }
510                }
511
512                if (clientAuth == null) {
513                        return new DeviceAuthorizationRequest(uri, clientID, scope, customParams);
514                } else {
515                        return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams);
516                }
517        }
518}