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