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 authorization 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 uri;
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 devize authorization request builder.
128                 *
129                 * @param clientID The client identifier. Corresponds to the {@code client_id}
130                 *                 parameter. Must not be {@code null}.
131                 */
132                public Builder(final ClientID clientID) {
133
134                        if (clientID == null)
135                                throw new IllegalArgumentException("The client ID must not be null");
136
137                        this.clientID = clientID;
138                        this.clientAuth = null;
139                }
140
141
142                /**
143                 * Creates a new device authorization request builder for an
144                 * authenticated request.
145                 *
146                 * @param clientAuth The client authentication. Must not be
147                 *                   {@code null}.
148                 */
149                public Builder(final ClientAuthentication clientAuth) {
150
151                        if (clientAuth == null)
152                                throw new IllegalArgumentException("The client authentication must not be null");
153
154                        this.clientID = null;
155                        this.clientAuth = clientAuth;
156                }
157
158
159                /**
160                 * Creates a new device authorization request builder from the
161                 * specified request.
162                 *
163                 * @param request The device authorization request. Must not be
164                 *                {@code null}.
165                 */
166                public Builder(final DeviceAuthorizationRequest request) {
167
168                        uri = request.getEndpointURI();
169                        clientAuth = request.getClientAuthentication();
170                        scope = request.scope;
171                        clientID = request.getClientID();
172                        customParams.putAll(request.getCustomParameters());
173                }
174
175
176                /**
177                 * Sets the scope. Corresponds to the optional {@code scope}
178                 * parameter.
179                 *
180                 * @param scope The scope, {@code null} if not specified.
181                 *
182                 * @return This builder.
183                 */
184                public Builder scope(final Scope scope) {
185
186                        this.scope = scope;
187                        return this;
188                }
189
190
191                /**
192                 * Sets a custom parameter.
193                 *
194                 * @param name   The parameter name. Must not be {@code null}.
195                 * @param values The parameter values, {@code null} if not
196                 *               specified.
197                 *
198                 * @return This builder.
199                 */
200                public Builder customParameter(final String name, final String... values) {
201
202                        if (values == null || values.length == 0) {
203                                customParams.remove(name);
204                        } else {
205                                customParams.put(name, Arrays.asList(values));
206                        }
207
208                        return this;
209                }
210
211
212                /**
213                 * Sets the URI of the endpoint (HTTP or HTTPS) for which the
214                 * request is intended.
215                 *
216                 * @param uri The endpoint URI, {@code null} if not specified.
217                 *
218                 * @return This builder.
219                 */
220                public Builder endpointURI(final URI uri) {
221
222                        this.uri = uri;
223                        return this;
224                }
225
226
227                /**
228                 * Builds a new device authorization request.
229                 *
230                 * @return The device authorization request.
231                 */
232                public DeviceAuthorizationRequest build() {
233
234                        try {
235                                if (clientAuth == null) {
236                                        return new DeviceAuthorizationRequest(uri, clientID, scope, customParams);
237                                } else {
238                                        return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams);
239                                }
240                        } catch (IllegalArgumentException e) {
241                                throw new IllegalStateException(e.getMessage(), e);
242                        }
243                }
244        }
245
246
247        /**
248         * Creates a new minimal device authorization request.
249         *
250         * @param uri      The URI of the device authorization endpoint. May be
251         *                 {@code null} if the {@link #toHTTPRequest} method
252         *                 will not be used.
253         * @param clientID The client identifier. Corresponds to the
254         *                 {@code client_id} parameter. Must not be
255         *                 {@code null}.
256         */
257        public DeviceAuthorizationRequest(final URI uri, final ClientID clientID) {
258
259                this(uri, clientID, null, null);
260        }
261
262
263        /**
264         * Creates a new device authorization request.
265         *
266         * @param uri      The URI of the device authorization endpoint. May be
267         *                 {@code null} if the {@link #toHTTPRequest} method
268         *                 will not be used.
269         * @param clientID The client identifier. Corresponds to the
270         *                 {@code client_id} parameter. Must not be
271         *                 {@code null}.
272         * @param scope    The request scope. Corresponds to the optional
273         *                 {@code scope} parameter. {@code null} if not
274         *                 specified.
275         */
276        public DeviceAuthorizationRequest(final URI uri, final ClientID clientID, final Scope scope) {
277
278                this(uri, clientID, scope, null);
279        }
280
281
282        /**
283         * Creates a new device authorization request with extension and custom
284         * parameters.
285         *
286         * @param uri          The URI of the device authorization endpoint.
287         *                     May be {@code null} if the {@link #toHTTPRequest}
288         *                     method will not be used.
289         * @param clientID     The client identifier. Corresponds to the
290         *                     {@code client_id} parameter. Must not be
291         *                     {@code null}.
292         * @param scope        The request scope. Corresponds to the optional
293         *                     {@code scope} parameter. {@code null} if not
294         *                     specified.
295         * @param customParams Custom parameters, empty map or {@code null} if
296         *                     none.
297         */
298        public DeviceAuthorizationRequest(final URI uri,
299                                          final ClientID clientID,
300                                          final Scope scope,
301                                          final Map<String, List<String>> customParams) {
302
303                super(uri, clientID);
304
305                if (clientID == null)
306                        throw new IllegalArgumentException("The client ID must not be null");
307
308                this.scope = scope;
309
310                if (MapUtils.isNotEmpty(customParams)) {
311                        this.customParams = Collections.unmodifiableMap(customParams);
312                } else {
313                        this.customParams = Collections.emptyMap();
314                }
315        }
316
317
318        /**
319         * Creates a new authenticated device authorization request with
320         * extension and custom parameters.
321         *
322         * @param uri          The URI of the device authorization endpoint.
323         *                     May be {@code null} if the {@link #toHTTPRequest}
324         *                     method will not be used.
325         * @param clientAuth   The client authentication. Must not be
326         *                     {@code null}.
327         * @param scope        The request scope. Corresponds to the optional
328         *                     {@code scope} parameter. {@code null} if not
329         *                     specified.
330         * @param customParams Custom parameters, empty map or {@code null} if
331         *                     none.
332         */
333        public DeviceAuthorizationRequest(final URI uri,
334                                          final ClientAuthentication clientAuth,
335                                          final Scope scope,
336                                          final Map<String, List<String>> customParams) {
337
338                super(uri, clientAuth);
339
340                if (clientAuth == null)
341                        throw new IllegalArgumentException("The client authentication must not be null");
342
343                this.scope = scope;
344
345                if (MapUtils.isNotEmpty(customParams)) {
346                        this.customParams = Collections.unmodifiableMap(customParams);
347                } else {
348                        this.customParams = Collections.emptyMap();
349                }
350        }
351
352
353        /**
354         * Returns the registered (standard) OAuth 2.0 device authorization
355         * request parameter names.
356         *
357         * @return The registered OAuth 2.0 device authorization request
358         *         parameter names, as a unmodifiable set.
359         */
360        public static Set<String> getRegisteredParameterNames() {
361
362                return REGISTERED_PARAMETER_NAMES;
363        }
364
365
366        /**
367         * Gets the scope. Corresponds to the optional {@code scope} parameter.
368         *
369         * @return The scope, {@code null} if not specified.
370         */
371        public Scope getScope() {
372
373                return scope;
374        }
375
376
377        /**
378         * Returns the additional custom parameters.
379         *
380         * @return The additional custom parameters as a unmodifiable map,
381         *         empty map if none.
382         */
383        public Map<String, List<String>> getCustomParameters() {
384
385                return customParams;
386        }
387
388
389        /**
390         * Returns the specified custom parameter.
391         *
392         * @param name The parameter name. Must not be {@code null}.
393         *
394         * @return The parameter value(s), {@code null} if not specified.
395         */
396        public List<String> getCustomParameter(final String name) {
397
398                return customParams.get(name);
399        }
400
401
402        /**
403         * Returns the matching HTTP request.
404         *
405         * @return The HTTP request.
406         */
407        @Override
408        public HTTPRequest toHTTPRequest() {
409
410                if (getEndpointURI() == null)
411                        throw new SerializeException("The endpoint URI is not specified");
412
413                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
414                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
415
416                if (getClientAuthentication() != null) {
417                        getClientAuthentication().applyTo(httpRequest);
418                }
419
420                Map<String, List<String>> params;
421                try {
422                        params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters());
423                } catch (ParseException e) {
424                        throw new SerializeException(e.getMessage(), e);
425                }
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 (!getCustomParameters().isEmpty()) {
436                        params.putAll(getCustomParameters());
437                }
438
439                httpRequest.setBody(URLUtils.serializeParameters(params));
440                return httpRequest;
441        }
442
443
444        /**
445         * Parses an device authorization request from the specified HTTP
446         * request.
447         *
448         * <p>Example HTTP request (GET):
449         *
450         * <pre>
451         * POST /device_authorization HTTP/1.1
452         * Host: server.example.com
453         * Content-Type: application/x-www-form-urlencoded
454         *
455         * client_id=459691054427
456         * </pre>
457         *
458         * @param httpRequest The HTTP request. Must not be {@code null}.
459         *
460         * @return The device authorization request.
461         *
462         * @throws ParseException If the HTTP request couldn't be parsed to an
463         *                        device authorization request.
464         */
465        public static DeviceAuthorizationRequest parse(final HTTPRequest httpRequest) throws ParseException {
466
467                // Only HTTP POST accepted
468                URI uri = httpRequest.getURI();
469                httpRequest.ensureMethod(HTTPRequest.Method.POST);
470                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
471
472                // Parse client authentication, if any
473                ClientAuthentication clientAuth;
474                try {
475                        clientAuth = ClientAuthentication.parse(httpRequest);
476                } catch (ParseException e) {
477                        throw new ParseException(e.getMessage(),
478                                        OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage()));
479                }
480
481                Map<String, List<String>> params = httpRequest.getBodyAsFormParameters();
482
483                ClientID clientID;
484                String v;
485
486                if (clientAuth == null) {
487                        // Parse mandatory client ID for unauthenticated requests
488                        v = MultivaluedMapUtils.getFirstValue(params, "client_id");
489
490                        if (StringUtils.isBlank(v)) {
491                                String msg = "Missing client_id parameter";
492                                throw new ParseException(msg,
493                                                OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg));
494                        }
495
496                        clientID = new ClientID(v);
497                } else {
498                        clientID = null;
499                }
500
501                // Parse optional scope
502                v = MultivaluedMapUtils.getFirstValue(params, "scope");
503
504                Scope scope = null;
505
506                if (StringUtils.isNotBlank(v))
507                        scope = Scope.parse(v);
508
509                // Parse custom parameters
510                Map<String, List<String>> customParams = null;
511
512                for (Map.Entry<String, List<String>> p : params.entrySet()) {
513
514                        if (!REGISTERED_PARAMETER_NAMES.contains(p.getKey())) {
515                                // We have a custom parameter
516                                if (customParams == null) {
517                                        customParams = new HashMap<>();
518                                }
519                                customParams.put(p.getKey(), p.getValue());
520                        }
521                }
522
523                if (clientAuth == null) {
524                        return new DeviceAuthorizationRequest(uri, clientID, scope, customParams);
525                } else {
526                        return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams);
527                }
528        }
529}