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