001/* 002 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 005 * in compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the 010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 011 * express or implied. See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package net.openid.appauth; 016 017import static net.openid.appauth.AdditionalParamsProcessor.checkAdditionalParams; 018import static net.openid.appauth.AdditionalParamsProcessor.extractAdditionalParams; 019import static net.openid.appauth.Preconditions.checkNotEmpty; 020import static net.openid.appauth.Preconditions.checkNotNull; 021 022import android.net.Uri; 023import androidx.annotation.NonNull; 024import androidx.annotation.Nullable; 025import androidx.annotation.VisibleForTesting; 026 027import org.json.JSONException; 028import org.json.JSONObject; 029 030import java.util.Arrays; 031import java.util.Collections; 032import java.util.HashSet; 033import java.util.Map; 034import java.util.Set; 035import java.util.concurrent.TimeUnit; 036 037public class RegistrationResponse { 038 static final String PARAM_CLIENT_ID = "client_id"; 039 static final String PARAM_CLIENT_SECRET = "client_secret"; 040 static final String PARAM_CLIENT_SECRET_EXPIRES_AT = "client_secret_expires_at"; 041 static final String PARAM_REGISTRATION_ACCESS_TOKEN = "registration_access_token"; 042 static final String PARAM_REGISTRATION_CLIENT_URI = "registration_client_uri"; 043 static final String PARAM_CLIENT_ID_ISSUED_AT = "client_id_issued_at"; 044 static final String PARAM_TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method"; 045 046 static final String KEY_REQUEST = "request"; 047 static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters"; 048 049 private static final Set<String> BUILT_IN_PARAMS = new HashSet<>(Arrays.asList( 050 PARAM_CLIENT_ID, 051 PARAM_CLIENT_SECRET, 052 PARAM_CLIENT_SECRET_EXPIRES_AT, 053 PARAM_REGISTRATION_ACCESS_TOKEN, 054 PARAM_REGISTRATION_CLIENT_URI, 055 PARAM_CLIENT_ID_ISSUED_AT, 056 PARAM_TOKEN_ENDPOINT_AUTH_METHOD 057 )); 058 059 /** 060 * The registration request associated with this response. 061 */ 062 @NonNull 063 public final RegistrationRequest request; 064 065 /** 066 * The registered client identifier. 067 * 068 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4 069 * <https://tools.ietf.org/html/rfc6749#section-4>" 070 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 <https://tools.ietf.org/html/rfc6749#section-4.1.1>" 071 */ 072 @NonNull 073 public final String clientId; 074 075 /** 076 * Timestamp of when the client identifier was issued, if provided. 077 * 078 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 079 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 080 */ 081 @Nullable 082 public final Long clientIdIssuedAt; 083 084 /** 085 * The client secret, which is part of the client credentials, if provided. 086 * 087 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 088 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 089 */ 090 @Nullable 091 public final String clientSecret; 092 093 /** 094 * Timestamp of when the client credentials expires, if provided. 095 * 096 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 097 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 098 */ 099 @Nullable 100 public final Long clientSecretExpiresAt; 101 102 /** 103 * Client registration access token that can be used for subsequent operations upon the client 104 * registration. 105 * 106 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 107 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 108 */ 109 @Nullable 110 public final String registrationAccessToken; 111 112 /** 113 * Location of the client configuration endpoint, if provided. 114 * 115 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 116 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 117 */ 118 @Nullable 119 public final Uri registrationClientUri; 120 121 /** 122 * Client authentication method to use at the token endpoint, if provided. 123 * 124 * @see "OpenID Connect Core 1.0, Section 9 125 * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9>" 126 */ 127 @Nullable 128 public final String tokenEndpointAuthMethod; 129 130 /** 131 * Additional, non-standard parameters in the response. 132 */ 133 @NonNull 134 public final Map<String, String> additionalParameters; 135 136 /** 137 * Thrown when a mandatory property is missing from the registration response. 138 */ 139 public static class MissingArgumentException extends Exception { 140 private String mMissingField; 141 142 /** 143 * Indicates that the specified mandatory field is missing from the registration response. 144 */ 145 public MissingArgumentException(String field) { 146 super("Missing mandatory registration field: " + field); 147 mMissingField = field; 148 } 149 150 public String getMissingField() { 151 return mMissingField; 152 } 153 } 154 155 public static final class Builder { 156 @NonNull 157 private RegistrationRequest mRequest; 158 @NonNull 159 private String mClientId; 160 161 @Nullable 162 private Long mClientIdIssuedAt; 163 @Nullable 164 private String mClientSecret; 165 @Nullable 166 private Long mClientSecretExpiresAt; 167 @Nullable 168 private String mRegistrationAccessToken; 169 @Nullable 170 private Uri mRegistrationClientUri; 171 @Nullable 172 private String mTokenEndpointAuthMethod; 173 174 @NonNull 175 private Map<String, String> mAdditionalParameters = Collections.emptyMap(); 176 177 /** 178 * Creates a token response associated with the specified request. 179 */ 180 public Builder(@NonNull RegistrationRequest request) { 181 setRequest(request); 182 } 183 184 /** 185 * Specifies the request associated with this response. Must not be null. 186 */ 187 @NonNull 188 public Builder setRequest(@NonNull RegistrationRequest request) { 189 mRequest = checkNotNull(request, "request cannot be null"); 190 return this; 191 } 192 193 /** 194 * Specifies the client identifier. 195 * 196 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4 197 * <https://tools.ietf.org/html/rfc6749#section-4>" 198 * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 4.1.1 199 * <https://tools.ietf.org/html/rfc6749#section-4.1.1>" 200 */ 201 public Builder setClientId(@NonNull String clientId) { 202 checkNotEmpty(clientId, "client ID cannot be null or empty"); 203 mClientId = clientId; 204 return this; 205 } 206 207 /** 208 * Specifies the timestamp for when the client identifier was issued. 209 * 210 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 211 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 212 */ 213 public Builder setClientIdIssuedAt(@Nullable Long clientIdIssuedAt) { 214 mClientIdIssuedAt = clientIdIssuedAt; 215 return this; 216 } 217 218 /** 219 * Specifies the client secret. 220 * 221 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 222 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 223 */ 224 public Builder setClientSecret(@Nullable String clientSecret) { 225 mClientSecret = clientSecret; 226 return this; 227 } 228 229 /** 230 * Specifies the expiration time of the client secret. 231 * 232 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 233 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 234 */ 235 public Builder setClientSecretExpiresAt(@Nullable Long clientSecretExpiresAt) { 236 mClientSecretExpiresAt = clientSecretExpiresAt; 237 return this; 238 } 239 240 /** 241 * Specifies the registration access token. 242 * 243 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 244 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 245 */ 246 public Builder setRegistrationAccessToken(@Nullable String registrationAccessToken) { 247 mRegistrationAccessToken = registrationAccessToken; 248 return this; 249 } 250 251 /** 252 * Specifies the client authentication method to use at the token endpoint. 253 */ 254 public Builder setTokenEndpointAuthMethod(@Nullable String tokenEndpointAuthMethod) { 255 mTokenEndpointAuthMethod = tokenEndpointAuthMethod; 256 return this; 257 } 258 259 /** 260 * Specifies the client configuration endpoint. 261 * 262 * @see "OpenID Connect Dynamic Client Registration 1.0, Section 3.2 263 * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3.2>" 264 */ 265 public Builder setRegistrationClientUri(@Nullable Uri registrationClientUri) { 266 mRegistrationClientUri = registrationClientUri; 267 return this; 268 } 269 270 /** 271 * Specifies the additional, non-standard parameters received as part of the response. 272 */ 273 public Builder setAdditionalParameters(Map<String, String> additionalParameters) { 274 mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS); 275 return this; 276 } 277 278 /** 279 * Creates the token response instance. 280 */ 281 public RegistrationResponse build() { 282 return new RegistrationResponse( 283 mRequest, 284 mClientId, 285 mClientIdIssuedAt, 286 mClientSecret, 287 mClientSecretExpiresAt, 288 mRegistrationAccessToken, 289 mRegistrationClientUri, 290 mTokenEndpointAuthMethod, 291 mAdditionalParameters); 292 } 293 294 /** 295 * Extracts registration response fields from a JSON string. 296 * 297 * @throws JSONException if the JSON is malformed or has incorrect value types for fields. 298 * @throws MissingArgumentException if the JSON is missing fields required by the 299 * specification. 300 */ 301 @NonNull 302 public Builder fromResponseJsonString(@NonNull String jsonStr) 303 throws JSONException, MissingArgumentException { 304 checkNotEmpty(jsonStr, "json cannot be null or empty"); 305 return fromResponseJson(new JSONObject(jsonStr)); 306 } 307 308 /** 309 * Extracts token response fields from a JSON object. 310 * 311 * @throws JSONException if the JSON is malformed or has incorrect value types for fields. 312 * @throws MissingArgumentException if the JSON is missing fields required by the 313 * specification. 314 */ 315 @NonNull 316 public Builder fromResponseJson(@NonNull JSONObject json) 317 throws JSONException, MissingArgumentException { 318 setClientId(JsonUtil.getString(json, PARAM_CLIENT_ID)); 319 setClientIdIssuedAt(JsonUtil.getLongIfDefined(json, PARAM_CLIENT_ID_ISSUED_AT)); 320 321 if (json.has(PARAM_CLIENT_SECRET)) { 322 if (!json.has(PARAM_CLIENT_SECRET_EXPIRES_AT)) { 323 /* 324 * From OpenID Connect Dynamic Client Registration, Section 3.2: 325 * client_secret_expires_at: "REQUIRED if client_secret is issued" 326 */ 327 throw new MissingArgumentException(PARAM_CLIENT_SECRET_EXPIRES_AT); 328 } 329 setClientSecret(json.getString(PARAM_CLIENT_SECRET)); 330 setClientSecretExpiresAt(json.getLong(PARAM_CLIENT_SECRET_EXPIRES_AT)); 331 } 332 333 if (json.has(PARAM_REGISTRATION_ACCESS_TOKEN) 334 != json.has(PARAM_REGISTRATION_CLIENT_URI)) { 335 /* 336 * From OpenID Connect Dynamic Client Registration, Section 3.2: 337 * "Implementations MUST either return both a Client Configuration Endpoint and a 338 * Registration Access Token or neither of them." 339 */ 340 String missingParameter = json.has(PARAM_REGISTRATION_ACCESS_TOKEN) 341 ? PARAM_REGISTRATION_CLIENT_URI : PARAM_REGISTRATION_ACCESS_TOKEN; 342 throw new MissingArgumentException(missingParameter); 343 } 344 345 setRegistrationAccessToken(JsonUtil.getStringIfDefined(json, 346 PARAM_REGISTRATION_ACCESS_TOKEN)); 347 setRegistrationClientUri(JsonUtil.getUriIfDefined(json, PARAM_REGISTRATION_CLIENT_URI)); 348 setTokenEndpointAuthMethod(JsonUtil.getStringIfDefined(json, 349 PARAM_TOKEN_ENDPOINT_AUTH_METHOD)); 350 351 setAdditionalParameters(extractAdditionalParams(json, BUILT_IN_PARAMS)); 352 return this; 353 } 354 } 355 356 private RegistrationResponse( 357 @NonNull RegistrationRequest request, 358 @NonNull String clientId, 359 @Nullable Long clientIdIssuedAt, 360 @Nullable String clientSecret, 361 @Nullable Long clientSecretExpiresAt, 362 @Nullable String registrationAccessToken, 363 @Nullable Uri registrationClientUri, 364 @Nullable String tokenEndpointAuthMethod, 365 @NonNull Map<String, String> additionalParameters) { 366 this.request = request; 367 this.clientId = clientId; 368 this.clientIdIssuedAt = clientIdIssuedAt; 369 this.clientSecret = clientSecret; 370 this.clientSecretExpiresAt = clientSecretExpiresAt; 371 this.registrationAccessToken = registrationAccessToken; 372 this.registrationClientUri = registrationClientUri; 373 this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; 374 this.additionalParameters = additionalParameters; 375 } 376 377 /** 378 * Reads a registration response JSON string received from an authorization server, 379 * and associates it with the provided request. 380 * 381 * @throws JSONException if the JSON is malformed or missing required fields. 382 * @throws MissingArgumentException if the JSON is missing fields required by the specification. 383 */ 384 @NonNull 385 public static RegistrationResponse fromJson( 386 @NonNull RegistrationRequest request, @NonNull String jsonStr) 387 throws JSONException, MissingArgumentException { 388 checkNotEmpty(jsonStr, "jsonStr cannot be null or empty"); 389 return fromJson(request, new JSONObject(jsonStr)); 390 } 391 392 /** 393 * Reads a registration response JSON object received from an authorization server, 394 * and associates it with the provided request. 395 * 396 * @throws JSONException if the JSON is malformed or missing required fields. 397 * @throws MissingArgumentException if the JSON is missing fields required by the specification. 398 */ 399 @NonNull 400 public static RegistrationResponse fromJson( 401 @NonNull RegistrationRequest request, 402 @NonNull JSONObject json) 403 throws JSONException, MissingArgumentException { 404 checkNotNull(request, "registration request cannot be null"); 405 return new RegistrationResponse.Builder(request) 406 .fromResponseJson(json) 407 .build(); 408 } 409 410 /** 411 * Produces a JSON representation of the registration response for persistent storage or 412 * local transmission (e.g. between activities). 413 */ 414 @NonNull 415 public JSONObject jsonSerialize() { 416 JSONObject json = new JSONObject(); 417 JsonUtil.put(json, KEY_REQUEST, request.jsonSerialize()); 418 JsonUtil.put(json, PARAM_CLIENT_ID, clientId); 419 JsonUtil.putIfNotNull(json, PARAM_CLIENT_ID_ISSUED_AT, clientIdIssuedAt); 420 JsonUtil.putIfNotNull(json, PARAM_CLIENT_SECRET, clientSecret); 421 JsonUtil.putIfNotNull(json, PARAM_CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt); 422 JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_ACCESS_TOKEN, registrationAccessToken); 423 JsonUtil.putIfNotNull(json, PARAM_REGISTRATION_CLIENT_URI, registrationClientUri); 424 JsonUtil.putIfNotNull(json, PARAM_TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthMethod); 425 JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS, 426 JsonUtil.mapToJsonObject(additionalParameters)); 427 return json; 428 } 429 430 /** 431 * Produces a JSON string representation of the registration response for persistent storage or 432 * local transmission (e.g. between activities). This method is just a convenience wrapper 433 * for {@link #jsonSerialize()}, converting the JSON object to its string form. 434 */ 435 @NonNull 436 public String jsonSerializeString() { 437 return jsonSerialize().toString(); 438 } 439 440 /** 441 * Reads a registration response from a JSON string representation produced by 442 * {@link #jsonSerialize()}. 443 * 444 * @throws JSONException if the provided JSON does not match the expected structure. 445 */ 446 public static RegistrationResponse jsonDeserialize(@NonNull JSONObject json) 447 throws JSONException { 448 checkNotNull(json, "json cannot be null"); 449 if (!json.has(KEY_REQUEST)) { 450 throw new IllegalArgumentException("registration request not found in JSON"); 451 } 452 453 return new RegistrationResponse( 454 RegistrationRequest.jsonDeserialize(json.getJSONObject(KEY_REQUEST)), 455 JsonUtil.getString(json, PARAM_CLIENT_ID), 456 JsonUtil.getLongIfDefined(json, PARAM_CLIENT_ID_ISSUED_AT), 457 JsonUtil.getStringIfDefined(json, PARAM_CLIENT_SECRET), 458 JsonUtil.getLongIfDefined(json, PARAM_CLIENT_SECRET_EXPIRES_AT), 459 JsonUtil.getStringIfDefined(json, PARAM_REGISTRATION_ACCESS_TOKEN), 460 JsonUtil.getUriIfDefined(json, PARAM_REGISTRATION_CLIENT_URI), 461 JsonUtil.getStringIfDefined(json, PARAM_TOKEN_ENDPOINT_AUTH_METHOD), 462 JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS)); 463 } 464 465 /** 466 * Reads a registration response from a JSON string representation produced by 467 * {@link #jsonSerializeString()}. This method is just a convenience wrapper for 468 * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form. 469 * 470 * @throws JSONException if the provided JSON does not match the expected structure. 471 */ 472 @NonNull 473 public static RegistrationResponse jsonDeserialize(@NonNull String jsonStr) 474 throws JSONException { 475 checkNotEmpty(jsonStr, "jsonStr cannot be null or empty"); 476 return jsonDeserialize(new JSONObject(jsonStr)); 477 } 478 479 /** 480 * Determines whether the returned access token has expired. 481 */ 482 public boolean hasClientSecretExpired() { 483 return hasClientSecretExpired(SystemClock.INSTANCE); 484 } 485 486 @VisibleForTesting 487 boolean hasClientSecretExpired(@NonNull Clock clock) { 488 Long now = TimeUnit.MILLISECONDS.toSeconds(checkNotNull(clock).getCurrentTimeMillis()); 489 return clientSecretExpiresAt != null && now > clientSecretExpiresAt; 490 491 } 492}