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