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