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 authorisation 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 endpoint; 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 device authorization request builder. 128 * 129 * @param clientID The client identifier. Corresponds to the 130 * {@code client_id} parameter. Must not be 131 * {@code null}. 132 */ 133 public Builder(final ClientID clientID) { 134 this.clientID = Objects.requireNonNull(clientID); 135 this.clientAuth = null; 136 } 137 138 139 /** 140 * Creates a new device authorization request builder for an 141 * authenticated request. 142 * 143 * @param clientAuth The client authentication. Must not be 144 * {@code null}. 145 */ 146 public Builder(final ClientAuthentication clientAuth) { 147 this.clientID = null; 148 this.clientAuth = Objects.requireNonNull(clientAuth); 149 } 150 151 152 /** 153 * Creates a new device authorization request builder from the 154 * specified request. 155 * 156 * @param request The device authorization request. Must not be 157 * {@code null}. 158 */ 159 public Builder(final DeviceAuthorizationRequest request) { 160 161 endpoint = request.getEndpointURI(); 162 clientAuth = request.getClientAuthentication(); 163 scope = request.scope; 164 clientID = request.getClientID(); 165 customParams.putAll(request.getCustomParameters()); 166 } 167 168 169 /** 170 * Sets the scope. Corresponds to the optional {@code scope} 171 * parameter. 172 * 173 * @param scope The scope, {@code null} if not specified. 174 * 175 * @return This builder. 176 */ 177 public Builder scope(final Scope scope) { 178 179 this.scope = scope; 180 return this; 181 } 182 183 184 /** 185 * Sets a custom parameter. 186 * 187 * @param name The parameter name. Must not be {@code null}. 188 * @param values The parameter values, {@code null} if not 189 * specified. 190 * 191 * @return This builder. 192 */ 193 public Builder customParameter(final String name, final String... values) { 194 195 if (values == null || values.length == 0) { 196 customParams.remove(name); 197 } else { 198 customParams.put(name, Arrays.asList(values)); 199 } 200 201 return this; 202 } 203 204 205 /** 206 * Sets the URI of the device authorisation endpoint. 207 * 208 * @param endpoint The URI of the device authorisation 209 * endpoint. May be {@code null} if the request 210 * is not going to be serialised. 211 * 212 * @return This builder. 213 */ 214 public Builder endpointURI(final URI endpoint) { 215 216 this.endpoint = endpoint; 217 return this; 218 } 219 220 221 /** 222 * Builds a new device authorization request. 223 * 224 * @return The device authorization request. 225 */ 226 public DeviceAuthorizationRequest build() { 227 228 try { 229 if (clientAuth == null) { 230 return new DeviceAuthorizationRequest(endpoint, clientID, scope, customParams); 231 } else { 232 return new DeviceAuthorizationRequest(endpoint, clientAuth, scope, customParams); 233 } 234 } catch (IllegalArgumentException e) { 235 throw new IllegalStateException(e.getMessage(), e); 236 } 237 } 238 } 239 240 241 /** 242 * Creates a new minimal device authorization request. 243 * 244 * @param endpoint The URI of the device authorization endpoint. May be 245 * {@code null} if the {@link #toHTTPRequest} method 246 * is not going to be used. 247 * @param clientID The client identifier. Corresponds to the 248 * {@code client_id} parameter. Must not be 249 * {@code null}. 250 */ 251 public DeviceAuthorizationRequest(final URI endpoint, final ClientID clientID) { 252 253 this(endpoint, clientID, null, null); 254 } 255 256 257 /** 258 * Creates a new device authorization request. 259 * 260 * @param endpoint The URI of the device authorization endpoint. May be 261 * {@code null} if the {@link #toHTTPRequest} method 262 * is not going to be used. 263 * @param clientID The client identifier. Corresponds to the 264 * {@code client_id} parameter. Must not be 265 * {@code null}. 266 * @param scope The request scope. Corresponds to the optional 267 * {@code scope} parameter. {@code null} if not 268 * specified. 269 */ 270 public DeviceAuthorizationRequest(final URI endpoint, final ClientID clientID, final Scope scope) { 271 272 this(endpoint, clientID, scope, null); 273 } 274 275 276 /** 277 * Creates a new device authorization request with extension and custom 278 * parameters. 279 * 280 * @param endpoint The URI of the device authorization endpoint. 281 * May be {@code null} if the {@link #toHTTPRequest} 282 * method is not going to be used. 283 * @param clientID The client identifier. Corresponds to the 284 * {@code client_id} parameter. Must not be 285 * {@code null}. 286 * @param scope The request scope. Corresponds to the optional 287 * {@code scope} parameter. {@code null} if not 288 * specified. 289 * @param customParams Custom parameters, empty map or {@code null} if 290 * none. 291 */ 292 public DeviceAuthorizationRequest(final URI endpoint, 293 final ClientID clientID, 294 final Scope scope, 295 final Map<String, List<String>> customParams) { 296 297 super(endpoint, Objects.requireNonNull(clientID)); 298 299 this.scope = scope; 300 301 if (MapUtils.isNotEmpty(customParams)) { 302 this.customParams = Collections.unmodifiableMap(customParams); 303 } else { 304 this.customParams = Collections.emptyMap(); 305 } 306 } 307 308 309 /** 310 * Creates a new authenticated device authorization request with 311 * extension and custom parameters. 312 * 313 * @param uri The URI of the device authorization endpoint. 314 * May be {@code null} if the {@link #toHTTPRequest} 315 * method will not be used. 316 * @param clientAuth The client authentication. Must not be 317 * {@code null}. 318 * @param scope The request scope. Corresponds to the optional 319 * {@code scope} parameter. {@code null} if not 320 * specified. 321 * @param customParams Custom parameters, empty map or {@code null} if 322 * none. 323 */ 324 public DeviceAuthorizationRequest(final URI uri, 325 final ClientAuthentication clientAuth, 326 final Scope scope, 327 final Map<String, List<String>> customParams) { 328 329 super(uri, Objects.requireNonNull(clientAuth)); 330 331 this.scope = scope; 332 333 if (MapUtils.isNotEmpty(customParams)) { 334 this.customParams = Collections.unmodifiableMap(customParams); 335 } else { 336 this.customParams = Collections.emptyMap(); 337 } 338 } 339 340 341 /** 342 * Returns the registered (standard) OAuth 2.0 device authorization 343 * request parameter names. 344 * 345 * @return The registered OAuth 2.0 device authorization request 346 * parameter names, as an unmodifiable set. 347 */ 348 public static Set<String> getRegisteredParameterNames() { 349 350 return REGISTERED_PARAMETER_NAMES; 351 } 352 353 354 /** 355 * Gets the scope. Corresponds to the optional {@code scope} parameter. 356 * 357 * @return The scope, {@code null} if not specified. 358 */ 359 public Scope getScope() { 360 361 return scope; 362 } 363 364 365 /** 366 * Returns the additional custom parameters. 367 * 368 * @return The additional custom parameters as an unmodifiable map, 369 * empty map if none. 370 */ 371 public Map<String, List<String>> getCustomParameters() { 372 373 return customParams; 374 } 375 376 377 /** 378 * Returns the specified custom parameter. 379 * 380 * @param name The parameter name. Must not be {@code null}. 381 * 382 * @return The parameter value(s), {@code null} if not specified. 383 */ 384 public List<String> getCustomParameter(final String name) { 385 386 return customParams.get(name); 387 } 388 389 390 /** 391 * Returns the matching HTTP request. 392 * 393 * @return The HTTP request. 394 */ 395 @Override 396 public HTTPRequest toHTTPRequest() { 397 398 if (getEndpointURI() == null) 399 throw new SerializeException("The endpoint URI is not specified"); 400 401 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI()); 402 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 403 httpRequest.setAccept(ContentType.APPLICATION_JSON.getType()); // iss #451 404 405 if (getClientAuthentication() != null) { 406 getClientAuthentication().applyTo(httpRequest); 407 } 408 409 Map<String, List<String>> params; 410 try { 411 params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters()); 412 } catch (ParseException e) { 413 throw new SerializeException(e.getMessage(), e); 414 } 415 416 if (scope != null && !scope.isEmpty()) { 417 params.put("scope", Collections.singletonList(scope.toString())); 418 } 419 420 if (getClientID() != null) { 421 params.put("client_id", Collections.singletonList(getClientID().getValue())); 422 } 423 424 if (!getCustomParameters().isEmpty()) { 425 params.putAll(getCustomParameters()); 426 } 427 428 httpRequest.setBody(URLUtils.serializeParameters(params)); 429 return httpRequest; 430 } 431 432 433 /** 434 * Parses a device authorization request from the specified HTTP 435 * request. 436 * 437 * <p>Example HTTP request (GET): 438 * 439 * <pre> 440 * POST /device_authorization HTTP/1.1 441 * Host: server.example.com 442 * Content-Type: application/x-www-form-urlencoded 443 * 444 * client_id=459691054427 445 * </pre> 446 * 447 * @param httpRequest The HTTP request. Must not be {@code null}. 448 * 449 * @return The device authorization request. 450 * 451 * @throws ParseException If the HTTP request couldn't be parsed to an 452 * device authorization request. 453 */ 454 public static DeviceAuthorizationRequest parse(final HTTPRequest httpRequest) throws ParseException { 455 456 // Only HTTP POST accepted 457 URI uri = httpRequest.getURI(); 458 httpRequest.ensureMethod(HTTPRequest.Method.POST); 459 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 460 461 // Parse client authentication, if any 462 ClientAuthentication clientAuth; 463 try { 464 clientAuth = ClientAuthentication.parse(httpRequest); 465 } catch (ParseException e) { 466 throw new ParseException(e.getMessage(), 467 OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage())); 468 } 469 470 Map<String, List<String>> params = httpRequest.getBodyAsFormParameters(); 471 472 ClientID clientID; 473 String v; 474 475 if (clientAuth == null) { 476 // Parse mandatory client ID for unauthenticated requests 477 v = MultivaluedMapUtils.getFirstValue(params, "client_id"); 478 479 if (StringUtils.isBlank(v)) { 480 String msg = "Missing client_id parameter"; 481 throw new ParseException(msg, 482 OAuth2Error.INVALID_REQUEST.appendDescription(": " + msg)); 483 } 484 485 clientID = new ClientID(v); 486 } else { 487 clientID = null; 488 } 489 490 // Parse optional scope 491 v = MultivaluedMapUtils.getFirstValue(params, "scope"); 492 493 Scope scope = null; 494 495 if (StringUtils.isNotBlank(v)) 496 scope = Scope.parse(v); 497 498 // Parse custom parameters 499 Map<String, List<String>> customParams = null; 500 501 for (Map.Entry<String, List<String>> p : params.entrySet()) { 502 503 if (!REGISTERED_PARAMETER_NAMES.contains(p.getKey())) { 504 // We have a custom parameter 505 if (customParams == null) { 506 customParams = new HashMap<>(); 507 } 508 customParams.put(p.getKey(), p.getValue()); 509 } 510 } 511 512 if (clientAuth == null) { 513 return new DeviceAuthorizationRequest(uri, clientID, scope, customParams); 514 } else { 515 return new DeviceAuthorizationRequest(uri, clientAuth, scope, customParams); 516 } 517 } 518}