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