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;
025import net.minidev.json.JSONObject;
026
027import com.nimbusds.common.contenttype.ContentType;
028import com.nimbusds.oauth2.sdk.ParseException;
029import com.nimbusds.oauth2.sdk.SuccessResponse;
030import com.nimbusds.oauth2.sdk.http.HTTPResponse;
031import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
032
033
034/**
035 * A device authorization response from the device authorization endpoint.
036 *
037 * <p>
038 * Example HTTP response:
039 *
040 * <pre>
041 * HTTP/1.1 200 OK
042 * Content-Type: application/json;charset=UTF-8
043 * Cache-Control: no-store
044 * Pragma: no-cache
045 *
046 * {
047 *   "device_code"               : "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
048 *   "user_code"                 : "WDJB-MJHT",
049 *   "verification_uri"          : "https://example.com/device",
050 *   "verification_uri_complete" : "https://example.com/device?user_code=WDJB-MJHT",
051 *   "expires_in"                : 1800,
052 *   "interval"                  : 5
053 * }
054 * </pre>
055 *
056 * <p>Related specifications:
057 *
058 * <ul>
059 *     <li>OAuth 2.0 Device Authorization Grant (RFC 8628), section 3.2.
060 * </ul>
061 */
062@Immutable
063public class DeviceAuthorizationSuccessResponse extends DeviceAuthorizationResponse implements SuccessResponse {
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("device_code");
075                p.add("user_code");
076                p.add("verification_uri");
077                p.add("verification_uri_complete");
078                p.add("expires_in");
079                p.add("interval");
080
081                REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p);
082        }
083
084
085        /**
086         * The device verification code.
087         */
088        private final DeviceCode deviceCode;
089
090
091        /**
092         * The end-user verification code.
093         */
094        private final UserCode userCode;
095
096
097        /**
098         * The end-user verification URI on the authorization server. The URI
099         * should be and easy to remember as end-users will be asked to
100         * manually type it into their user-agent.
101         */
102        private final URI verificationURI;
103
104
105        /**
106         * Optional. A verification URI that includes the "user_code" (or other
107         * information with the same function as the "user_code"), designed for
108         * non-textual transmission.
109         */
110        private final URI verificationURIComplete;
111
112
113        /**
114         * The lifetime in seconds of the "device_code" and "user_code".
115         */
116        private final long lifetime;
117
118
119        /**
120         * Optional. The minimum amount of time in seconds that the client
121         * SHOULD wait between polling requests to the token endpoint. If no
122         * value is provided, clients MUST use 5 as the default.
123         */
124        private final long interval;
125
126
127        /**
128         * Optional custom parameters.
129         */
130        private final Map<String, Object> customParams;
131
132
133        /**
134         * Creates a new device authorization success response.
135         *
136         * @param deviceCode      The device verification code. Must not be
137         *                        {@code null}.
138         * @param userCode        The user verification code. Must not be
139         *                        {@code null}.
140         * @param verificationURI The end-user verification URI on the
141         *                        authorization server. Must not be
142         *                        {@code null}.
143         * @param lifetime        The lifetime in seconds of the "device_code"
144         *                        and "user_code".
145         */
146        public DeviceAuthorizationSuccessResponse(final DeviceCode deviceCode,
147                                                  final UserCode userCode,
148                                                  final URI verificationURI,
149                                                  final long lifetime) {
150
151                this(deviceCode, userCode, verificationURI, null, lifetime, 5, null);
152        }
153
154
155        /**
156         * Creates a new device authorization success response.
157         *
158         * @param deviceCode              The device verification code. Must
159         *                                not be {@code null}.
160         * @param userCode                The user verification code. Must not
161         *                                be {@code null}.
162         * @param verificationURI         The end-user verification URI on the
163         *                                authorization server. Must not be
164         *                                {@code null}.
165         * @param verificationURIComplete The end-user verification URI on the
166         *                                authorization server that includes
167         *                                the user_code. Can be {@code null}.
168         * @param lifetime                The lifetime in seconds of the
169         *                                "device_code" and "user_code". Must
170         *                                be greater than {@code 0}.
171         * @param interval                The minimum amount of time in seconds
172         *                                that the client SHOULD wait between
173         *                                polling requests to the token
174         *                                endpoint.
175         * @param customParams            Optional custom parameters,
176         *                                {@code null} if none.
177         */
178        public DeviceAuthorizationSuccessResponse(final DeviceCode deviceCode,
179                                                  final UserCode userCode,
180                                                  final URI verificationURI,
181                                                  final URI verificationURIComplete,
182                                                  final long lifetime,
183                                                  final long interval,
184                                                  final Map<String, Object> customParams) {
185
186                if (deviceCode == null)
187                        throw new IllegalArgumentException("The device_code must not be null");
188
189                this.deviceCode = deviceCode;
190
191                if (userCode == null)
192                        throw new IllegalArgumentException("The user_code must not be null");
193
194                this.userCode = userCode;
195
196                if (verificationURI == null)
197                        throw new IllegalArgumentException("The verification_uri must not be null");
198
199                this.verificationURI = verificationURI;
200
201                this.verificationURIComplete = verificationURIComplete;
202
203                if (lifetime <= 0)
204                        throw new IllegalArgumentException("The lifetime must be greater than 0");
205
206                this.lifetime = lifetime;
207                this.interval = interval;
208                this.customParams = customParams;
209        }
210
211
212        /**
213         * Returns the registered (standard) OAuth 2.0 device authorization
214         * response parameter names.
215         *
216         * @return The registered OAuth 2.0 device authorization response
217         *         parameter names, as a unmodifiable set.
218         */
219        public static Set<String> getRegisteredParameterNames() {
220
221                return REGISTERED_PARAMETER_NAMES;
222        }
223
224
225        @Override
226        public boolean indicatesSuccess() {
227
228                return true;
229        }
230
231
232        /**
233         * Returns the device verification code.
234         * 
235         * @return The device verification code.
236         */
237        public DeviceCode getDeviceCode() {
238
239                return deviceCode;
240        }
241
242
243        /**
244         * Returns the end-user verification code.
245         * 
246         * @return The end-user verification code.
247         */
248        public UserCode getUserCode() {
249
250                return userCode;
251        }
252
253
254        /**
255         * Returns the end-user verification URI on the authorization server.
256         * 
257         * @return The end-user verification URI on the authorization server.
258         */
259        public URI getVerificationURI() {
260
261                return verificationURI;
262        }
263
264
265        /**
266         * @see #getVerificationURI()
267         */
268        @Deprecated
269        public URI getVerificationUri() {
270
271                return getVerificationURI();
272        }
273
274
275        /**
276         * Returns the end-user verification URI that includes the user_code.
277         * 
278         * @return The end-user verification URI that includes the user_code,
279         *         or {@code null} if not specified.
280         */
281        public URI getVerificationURIComplete() {
282
283                return verificationURIComplete;
284        }
285
286
287        /**
288         * @see #getVerificationURIComplete()
289         */
290        @Deprecated
291        public URI getVerificationUriComplete() {
292
293                return getVerificationURIComplete();
294        }
295
296
297        /**
298         * Returns the lifetime in seconds of the "device_code" and "user_code".
299         * 
300         * @return The lifetime in seconds of the "device_code" and "user_code".
301         */
302        public long getLifetime() {
303
304                return lifetime;
305        }
306
307
308        /**
309         * Returns the minimum amount of time in seconds that the client SHOULD
310         * wait between polling requests to the token endpoint.
311         * 
312         * @return The minimum amount of time in seconds that the client SHOULD
313         *         wait between polling requests to the token endpoint.
314         */
315        public long getInterval() {
316
317                return interval;
318        }
319
320
321        /**
322         * Returns the custom parameters.
323         *
324         * @return The custom parameters, as a unmodifiable map, empty map if
325         *         none.
326         */
327        public Map<String, Object> getCustomParameters() {
328
329                if (customParams == null)
330                        return Collections.emptyMap();
331
332                return Collections.unmodifiableMap(customParams);
333        }
334
335
336        /**
337         * Returns a JSON object representation of this device authorization
338         * response.
339         *
340         * <p>
341         * Example JSON object:
342         *
343         * <pre>
344         * {
345         *   "device_code"               : "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
346         *   "user_code"                 : "WDJB-MJHT",
347         *   "verification_uri"          : "https://example.com/device",
348         *   "verification_uri_complete" : "https://example.com/device?user_code=WDJB-MJHT",
349         *   "expires_in"                : 1800,
350         *   "interval"                  : 5
351         * }
352         * </pre>
353         *
354         * @return The JSON object.
355         */
356        public JSONObject toJSONObject() {
357
358                JSONObject o = new JSONObject();
359                o.put("device_code", getDeviceCode());
360                o.put("user_code", getUserCode());
361                o.put("verification_uri", getVerificationURI().toString());
362
363                if (getVerificationURIComplete() != null)
364                        o.put("verification_uri_complete", getVerificationURIComplete().toString());
365
366                o.put("expires_in", getLifetime());
367
368                if (getInterval() > 0)
369                        o.put("interval", getInterval());
370
371                if (customParams != null)
372                        o.putAll(customParams);
373
374                return o;
375        }
376
377
378        @Override
379        public HTTPResponse toHTTPResponse() {
380
381                HTTPResponse httpResponse = new HTTPResponse(HTTPResponse.SC_OK);
382
383                httpResponse.setEntityContentType(ContentType.APPLICATION_JSON);
384                httpResponse.setCacheControl("no-store");
385                httpResponse.setPragma("no-cache");
386
387                httpResponse.setContent(toJSONObject().toString());
388
389                return httpResponse;
390        }
391
392
393        /**
394         * Parses an device authorization response from the specified JSON
395         * object.
396         *
397         * @param jsonObject The JSON object to parse. Must not be {@code null}.
398         *
399         * @return The device authorization response.
400         *
401         * @throws ParseException If the JSON object couldn't be parsed to a
402         *                        device authorization response.
403         */
404        public static DeviceAuthorizationSuccessResponse parse(final JSONObject jsonObject) throws ParseException {
405
406                DeviceCode deviceCode = new DeviceCode(JSONObjectUtils.getString(jsonObject, "device_code"));
407                UserCode userCode = new UserCode(JSONObjectUtils.getString(jsonObject, "user_code"));
408                URI verificationURI = JSONObjectUtils.getURI(jsonObject, "verification_uri");
409                URI verificationURIComplete = JSONObjectUtils.getURI(jsonObject, "verification_uri_complete", null);
410
411                // Parse lifetime
412                long lifetime;
413                if (jsonObject.get("expires_in") instanceof Number) {
414
415                        lifetime = JSONObjectUtils.getLong(jsonObject, "expires_in");
416                } else {
417                        String lifetimeStr = JSONObjectUtils.getString(jsonObject, "expires_in");
418
419                        try {
420                                lifetime = Long.parseLong(lifetimeStr);
421
422                        } catch (NumberFormatException e) {
423
424                                throw new ParseException("Invalid expires_in parameter, must be integer");
425                        }
426                }
427
428                // Parse lifetime
429                long interval = 5;
430                if (jsonObject.containsKey("interval")) {
431                        if (jsonObject.get("interval") instanceof Number) {
432
433                                interval = JSONObjectUtils.getLong(jsonObject, "interval");
434                        } else {
435                                String intervalStr = JSONObjectUtils.getString(jsonObject, "interval");
436
437                                try {
438                                        interval = Long.parseLong(intervalStr);
439
440                                } catch (NumberFormatException e) {
441
442                                        throw new ParseException("Invalid interval parameter, must be integer");
443                                }
444                        }
445                }
446
447                // Determine the custom param names
448                Set<String> customParamNames = new HashSet<>(jsonObject.keySet());
449                customParamNames.removeAll(getRegisteredParameterNames());
450
451                Map<String, Object> customParams = null;
452
453                if (!customParamNames.isEmpty()) {
454
455                        customParams = new LinkedHashMap<>();
456
457                        for (String name : customParamNames) {
458                                customParams.put(name, jsonObject.get(name));
459                        }
460                }
461
462                return new DeviceAuthorizationSuccessResponse(deviceCode, userCode, verificationURI,
463                                verificationURIComplete, lifetime, interval, customParams);
464        }
465
466
467        /**
468         * Parses an device authorization response from the specified HTTP
469         * response.
470         *
471         * @param httpResponse The HTTP response. Must not be {@code null}.
472         *
473         * @return The device authorization response.
474         *
475         * @throws ParseException If the HTTP response couldn't be parsed to a
476         *                        device authorization response.
477         */
478        public static DeviceAuthorizationSuccessResponse parse(final HTTPResponse httpResponse) throws ParseException {
479
480                httpResponse.ensureStatusCode(HTTPResponse.SC_OK);
481                JSONObject jsonObject = httpResponse.getContentAsJSONObject();
482                return parse(jsonObject);
483        }
484}