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.net.URISyntaxException; 023import java.util.*; 024 025import net.jcip.annotations.Immutable; 026 027import com.nimbusds.common.contenttype.ContentType; 028import com.nimbusds.oauth2.sdk.*; 029import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; 030import com.nimbusds.oauth2.sdk.http.HTTPRequest; 031import com.nimbusds.oauth2.sdk.id.ClientID; 032import com.nimbusds.oauth2.sdk.util.MapUtils; 033import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 034import com.nimbusds.oauth2.sdk.util.StringUtils; 035import com.nimbusds.oauth2.sdk.util.URLUtils; 036 037 038/** 039 * Device authorisation request. Used to start the authorization flow for 040 * browserless and input constraint devices. Supports custom request 041 * parameters. 042 * 043 * <p>Extending classes may define additional request parameters as well as 044 * enforce tighter requirements on the base parameters. 045 * 046 * <p>Example HTTP request: 047 * 048 * <pre> 049 * POST /device_authorization HTTP/1.1 050 * Host: server.example.com 051 * Content-Type: application/x-www-form-urlencoded 052 * 053 * client_id=459691054427 054 * </pre> 055 * 056 * <p>Related specifications: 057 * 058 * <ul> 059 * <li>OAuth 2.0 Device Authorization Grant (RFC 8628) 060 * </ul> 061 */ 062@Immutable 063public class DeviceAuthorizationRequest extends AbstractOptionallyIdentifiedRequest { 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("client_id"); 075 p.add("scope"); 076 077 REGISTERED_PARAMETER_NAMES = Collections.unmodifiableSet(p); 078 } 079 080 081 /** 082 * The scope (optional). 083 */ 084 private final Scope scope; 085 086 087 /** 088 * Custom parameters. 089 */ 090 private final Map<String, List<String>> customParams; 091 092 093 /** 094 * Builder for constructing authorisation requests. 095 */ 096 public static class Builder { 097 098 /** 099 * The endpoint URI (optional). 100 */ 101 private URI uri; 102 103 104 /** 105 * The client authentication (optional). 106 */ 107 private final ClientAuthentication clientAuth; 108 109 110 /** 111 * The client identifier (required if not authenticated). 112 */ 113 private final ClientID clientID; 114 115 116 /** 117 * The scope (optional). 118 */ 119 private Scope scope; 120 121 122 /** 123 * Custom parameters. 124 */ 125 private final Map<String, List<String>> customParams = new HashMap<>(); 126 127 128 /** 129 * Creates a new devize authorization request builder. 130 * 131 * @param clientID The client identifier. Corresponds to the {@code client_id} 132 * parameter. Must not be {@code null}. 133 */ 134 public Builder(final ClientID clientID) { 135 136 if (clientID == null) 137 throw new IllegalArgumentException("The client ID must not be null"); 138 139 this.clientID = clientID; 140 this.clientAuth = null; 141 } 142 143 144 /** 145 * Creates a new device authorization request builder for an 146 * authenticated request. 147 * 148 * @param clientAuth The client authentication. Must not be 149 * {@code null}. 150 */ 151 public Builder(final ClientAuthentication clientAuth) { 152 153 if (clientAuth == null) 154 throw new IllegalArgumentException("The client authentication must not be null"); 155 156 this.clientID = null; 157 this.clientAuth = clientAuth; 158 } 159 160 161 /** 162 * Creates a new device authorization request builder from the 163 * specified request. 164 * 165 * @param request The device authorization request. Must not be 166 * {@code null}. 167 */ 168 public Builder(final DeviceAuthorizationRequest request) { 169 170 uri = request.getEndpointURI(); 171 clientAuth = request.getClientAuthentication(); 172 scope = request.scope; 173 clientID = request.getClientID(); 174 customParams.putAll(request.getCustomParameters()); 175 } 176 177 178 /** 179 * Sets the scope. Corresponds to the optional {@code scope} 180 * parameter. 181 * 182 * @param scope The scope, {@code null} if not specified. 183 * 184 * @return This builder. 185 */ 186 public Builder scope(final Scope scope) { 187 188 this.scope = scope; 189 return this; 190 } 191 192 193 /** 194 * Sets a custom parameter. 195 * 196 * @param name The parameter name. Must not be {@code null}. 197 * @param values The parameter values, {@code null} if not 198 * specified. 199 * 200 * @return This builder. 201 */ 202 public Builder customParameter(final String name, final String... values) { 203 204 if (values == null || values.length == 0) { 205 customParams.remove(name); 206 } else { 207 customParams.put(name, Arrays.asList(values)); 208 } 209 210 return this; 211 } 212 213 214 /** 215 * Sets the URI of the endpoint (HTTP or HTTPS) for which the 216 * request is intended. 217 * 218 * @param uri The endpoint URI, {@code null} if not specified. 219 * 220 * @return This builder. 221 */ 222 public Builder endpointURI(final URI uri) { 223 224 this.uri = uri; 225 return this; 226 } 227 228 229 /** 230 * Builds a new device authorization request. 231 * 232 * @return The device authorization request. 233 */ 234 public DeviceAuthorizationRequest build() { 235 236 try { 237 if (clientAuth == null) { 238 return new DeviceAuthorizationRequest(uri, clientID, scope, customParams); 239 } else { 240 return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams); 241 } 242 } catch (IllegalArgumentException e) { 243 throw new IllegalStateException(e.getMessage(), e); 244 } 245 } 246 } 247 248 249 /** 250 * Creates a new minimal device authorization request. 251 * 252 * @param uri The URI of the device authorization endpoint. May be 253 * {@code null} if the {@link #toHTTPRequest} method 254 * will not be used. 255 * @param clientID The client identifier. Corresponds to the 256 * {@code client_id} parameter. Must not be 257 * {@code null}. 258 */ 259 public DeviceAuthorizationRequest(final URI uri, final ClientID clientID) { 260 261 this(uri, clientID, null, null); 262 } 263 264 265 /** 266 * Creates a new device authorization request. 267 * 268 * @param uri The URI of the device authorization endpoint. May be 269 * {@code null} if the {@link #toHTTPRequest} method 270 * will not be used. 271 * @param clientID The client identifier. Corresponds to the 272 * {@code client_id} parameter. Must not be 273 * {@code null}. 274 * @param scope The request scope. Corresponds to the optional 275 * {@code scope} parameter. {@code null} if not 276 * specified. 277 */ 278 public DeviceAuthorizationRequest(final URI uri, final ClientID clientID, final Scope scope) { 279 280 this(uri, clientID, scope, null); 281 } 282 283 284 /** 285 * Creates a new device authorization request with extension and custom 286 * parameters. 287 * 288 * @param uri The URI of the device authorization endpoint. 289 * May be {@code null} if the {@link #toHTTPRequest} 290 * method will not be used. 291 * @param clientID The client identifier. Corresponds to the 292 * {@code client_id} parameter. Must not be 293 * {@code null}. 294 * @param scope The request scope. Corresponds to the optional 295 * {@code scope} parameter. {@code null} if not 296 * specified. 297 * @param customParams Custom parameters, empty map or {@code null} if 298 * none. 299 */ 300 public DeviceAuthorizationRequest(final URI uri, 301 final ClientID clientID, 302 final Scope scope, 303 final Map<String, List<String>> customParams) { 304 305 super(uri, clientID); 306 307 if (clientID == null) 308 throw new IllegalArgumentException("The client ID must not be null"); 309 310 this.scope = scope; 311 312 if (MapUtils.isNotEmpty(customParams)) { 313 this.customParams = Collections.unmodifiableMap(customParams); 314 } else { 315 this.customParams = Collections.emptyMap(); 316 } 317 } 318 319 320 /** 321 * Creates a new authenticated device authorization request with 322 * extension and custom parameters. 323 * 324 * @param uri The URI of the device authorization endpoint. 325 * May be {@code null} if the {@link #toHTTPRequest} 326 * method will not be used. 327 * @param clientAuth The client authentication. Must not be 328 * {@code null}. 329 * @param scope The request scope. Corresponds to the optional 330 * {@code scope} parameter. {@code null} if not 331 * specified. 332 * @param customParams Custom parameters, empty map or {@code null} if 333 * none. 334 */ 335 public DeviceAuthorizationRequest(final URI uri, 336 final ClientAuthentication clientAuth, 337 final Scope scope, 338 final Map<String, List<String>> customParams) { 339 340 super(uri, clientAuth); 341 342 if (clientAuth == null) 343 throw new IllegalArgumentException("The client authentication must not be null"); 344 345 this.scope = scope; 346 347 if (MapUtils.isNotEmpty(customParams)) { 348 this.customParams = Collections.unmodifiableMap(customParams); 349 } else { 350 this.customParams = Collections.emptyMap(); 351 } 352 } 353 354 355 /** 356 * Returns the registered (standard) OAuth 2.0 device authorization 357 * request parameter names. 358 * 359 * @return The registered OAuth 2.0 device authorization request 360 * parameter names, as a unmodifiable set. 361 */ 362 public static Set<String> getRegisteredParameterNames() { 363 364 return REGISTERED_PARAMETER_NAMES; 365 } 366 367 368 /** 369 * Gets the scope. Corresponds to the optional {@code scope} parameter. 370 * 371 * @return The scope, {@code null} if not specified. 372 */ 373 public Scope getScope() { 374 375 return scope; 376 } 377 378 379 /** 380 * Returns the additional custom parameters. 381 * 382 * @return The additional custom parameters as a unmodifiable map, 383 * empty map if none. 384 */ 385 public Map<String, List<String>> getCustomParameters() { 386 387 return customParams; 388 } 389 390 391 /** 392 * Returns the specified custom parameter. 393 * 394 * @param name The parameter name. Must not be {@code null}. 395 * 396 * @return The parameter value(s), {@code null} if not specified. 397 */ 398 public List<String> getCustomParameter(final String name) { 399 400 return customParams.get(name); 401 } 402 403 404 /** 405 * Returns the matching HTTP request. 406 * 407 * @return The HTTP request. 408 */ 409 @Override 410 public HTTPRequest toHTTPRequest() { 411 412 if (getEndpointURI() == null) 413 throw new SerializeException("The endpoint URI is not specified"); 414 415 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI()); 416 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 417 418 if (getClientAuthentication() != null) { 419 getClientAuthentication().applyTo(httpRequest); 420 } 421 422 Map<String, List<String>> params = httpRequest.getQueryParameters(); 423 424 if (scope != null && !scope.isEmpty()) { 425 params.put("scope", Collections.singletonList(scope.toString())); 426 } 427 428 if (getClientID() != null) { 429 params.put("client_id", Collections.singletonList(getClientID().getValue())); 430 } 431 432 if (!getCustomParameters().isEmpty()) { 433 params.putAll(getCustomParameters()); 434 } 435 436 httpRequest.setQuery(URLUtils.serializeParameters(params)); 437 return httpRequest; 438 } 439 440 441 /** 442 * Parses an device authorization request from the specified HTTP 443 * request. 444 * 445 * <p>Example HTTP request (GET): 446 * 447 * <pre> 448 * POST /device_authorization HTTP/1.1 449 * Host: server.example.com 450 * Content-Type: application/x-www-form-urlencoded 451 * 452 * client_id=459691054427 453 * </pre> 454 * 455 * @param httpRequest The HTTP request. Must not be {@code null}. 456 * 457 * @return The device authorization request. 458 * 459 * @throws ParseException If the HTTP request couldn't be parsed to an 460 * device authorization request. 461 */ 462 public static DeviceAuthorizationRequest parse(final HTTPRequest httpRequest) throws ParseException { 463 464 // Only HTTP POST accepted 465 URI uri; 466 467 try { 468 uri = httpRequest.getURL().toURI(); 469 470 } catch (URISyntaxException e) { 471 472 throw new ParseException(e.getMessage(), e); 473 } 474 475 httpRequest.ensureMethod(HTTPRequest.Method.POST); 476 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 477 478 // Parse client authentication, if any 479 ClientAuthentication clientAuth; 480 481 try { 482 clientAuth = ClientAuthentication.parse(httpRequest); 483 } catch (ParseException e) { 484 throw new ParseException(e.getMessage(), 485 OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage())); 486 } 487 488 // No fragment! May use query component! 489 Map<String, List<String>> params = httpRequest.getQueryParameters(); 490 491 ClientID clientID; 492 String v; 493 494 if (clientAuth == null) { 495 // Parse mandatory client ID for unauthenticated requests 496 v = MultivaluedMapUtils.getFirstValue(params, "client_id"); 497 498 if (StringUtils.isBlank(v)) { 499 String msg = "Missing client_id parameter"; 500 throw new ParseException(msg, 501 OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 502 } 503 504 clientID = new ClientID(v); 505 } else { 506 clientID = null; 507 } 508 509 // Parse optional scope 510 v = MultivaluedMapUtils.getFirstValue(params, "scope"); 511 512 Scope scope = null; 513 514 if (StringUtils.isNotBlank(v)) 515 scope = Scope.parse(v); 516 517 // Parse custom parameters 518 Map<String, List<String>> customParams = null; 519 520 for (Map.Entry<String, List<String>> p : params.entrySet()) { 521 522 if (!REGISTERED_PARAMETER_NAMES.contains(p.getKey())) { 523 // We have a custom parameter 524 if (customParams == null) { 525 customParams = new HashMap<>(); 526 } 527 customParams.put(p.getKey(), p.getValue()); 528 } 529 } 530 531 if (clientAuth == null) { 532 return new DeviceAuthorizationRequest(uri, clientID, scope, customParams); 533 } else { 534 return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams); 535 } 536 } 537}