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