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}