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