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}