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