001/* 002 * Copyright 2015 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.Preconditions.checkArgument; 018import static net.openid.appauth.Preconditions.checkNotNull; 019 020import android.net.Uri; 021import android.os.AsyncTask; 022import androidx.annotation.NonNull; 023import androidx.annotation.Nullable; 024 025import net.openid.appauth.AuthorizationException.GeneralErrors; 026import net.openid.appauth.connectivity.ConnectionBuilder; 027import net.openid.appauth.connectivity.DefaultConnectionBuilder; 028import net.openid.appauth.internal.Logger; 029import org.json.JSONException; 030import org.json.JSONObject; 031 032import java.io.IOException; 033import java.io.InputStream; 034import java.net.HttpURLConnection; 035 036/** 037 * Configuration details required to interact with an authorization service. 038 */ 039public class AuthorizationServiceConfiguration { 040 041 /** 042 * The standard base path for well-known resources on domains. 043 * 044 * @see "Defining Well-Known Uniform Resource Identifiers (RFC 5785) 045 * <https://tools.ietf.org/html/rfc5785>" 046 */ 047 public static final String WELL_KNOWN_PATH = 048 ".well-known"; 049 050 /** 051 * The standard resource under {@link #WELL_KNOWN_PATH .well-known} at which an OpenID Connect 052 * discovery document can be found under an issuer's base URI. 053 * 054 * @see "OpenID Connect discovery 1.0 055 * <https://openid.net/specs/openid-connect-discovery-1_0.html>" 056 */ 057 public static final String OPENID_CONFIGURATION_RESOURCE = 058 "openid-configuration"; 059 060 private static final String KEY_AUTHORIZATION_ENDPOINT = "authorizationEndpoint"; 061 private static final String KEY_TOKEN_ENDPOINT = "tokenEndpoint"; 062 private static final String KEY_REGISTRATION_ENDPOINT = "registrationEndpoint"; 063 private static final String KEY_DISCOVERY_DOC = "discoveryDoc"; 064 private static final String KEY_END_SESSION_ENPOINT = "endSessionEndpoint"; 065 066 /** 067 * The authorization service's endpoint. 068 */ 069 @NonNull 070 public final Uri authorizationEndpoint; 071 072 /** 073 * The authorization service's token exchange and refresh endpoint. 074 */ 075 @NonNull 076 public final Uri tokenEndpoint; 077 078 /** 079 * The end session service's endpoint; 080 */ 081 @Nullable 082 public final Uri endSessionEndpoint; 083 084 /** 085 * The authorization service's client registration endpoint. 086 */ 087 @Nullable 088 public final Uri registrationEndpoint; 089 090 091 /** 092 * The discovery document describing the service, if it is an OpenID Connect provider. 093 */ 094 @Nullable 095 public final AuthorizationServiceDiscovery discoveryDoc; 096 097 /** 098 * Creates a service configuration for a basic OAuth2 provider. 099 * @param authorizationEndpoint The 100 * [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1) 101 * for the service. 102 * @param tokenEndpoint The 103 * [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2) 104 * for the service. 105 */ 106 public AuthorizationServiceConfiguration( 107 @NonNull Uri authorizationEndpoint, 108 @NonNull Uri tokenEndpoint) { 109 this(authorizationEndpoint, tokenEndpoint, null); 110 } 111 112 /** 113 * Creates a service configuration for a basic OAuth2 provider. 114 * @param authorizationEndpoint The 115 * [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1) 116 * for the service. 117 * @param tokenEndpoint The 118 * [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2) 119 * for the service. 120 * @param registrationEndpoint The optional 121 * [client registration endpoint URI](https://tools.ietf.org/html/rfc7591#section-3) 122 */ 123 public AuthorizationServiceConfiguration( 124 @NonNull Uri authorizationEndpoint, 125 @NonNull Uri tokenEndpoint, 126 @Nullable Uri registrationEndpoint) { 127 this(authorizationEndpoint, tokenEndpoint, registrationEndpoint, null); 128 } 129 130 /** 131 * Creates a service configuration for a basic OAuth2 provider. 132 * @param authorizationEndpoint The 133 * [authorization endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.1) 134 * for the service. 135 * @param tokenEndpoint The 136 * [token endpoint URI](https://tools.ietf.org/html/rfc6749#section-3.2) 137 * for the service. 138 * @param registrationEndpoint The optional 139 * [client registration endpoint URI](https://tools.ietf.org/html/rfc7591#section-3) 140 * @param endSessionEndpoint The optional 141 * [end session endpoint URI](https://tools.ietf.org/html/rfc6749#section-2.2) 142 * for the service. 143 */ 144 public AuthorizationServiceConfiguration( 145 @NonNull Uri authorizationEndpoint, 146 @NonNull Uri tokenEndpoint, 147 @Nullable Uri registrationEndpoint, 148 @Nullable Uri endSessionEndpoint) { 149 this.authorizationEndpoint = checkNotNull(authorizationEndpoint); 150 this.tokenEndpoint = checkNotNull(tokenEndpoint); 151 this.registrationEndpoint = registrationEndpoint; 152 this.endSessionEndpoint = endSessionEndpoint; 153 this.discoveryDoc = null; 154 } 155 156 /** 157 * Creates an service configuration for an OpenID Connect provider, based on its 158 * {@link AuthorizationServiceDiscovery discovery document}. 159 * 160 * @param discoveryDoc The OpenID Connect discovery document which describes this service. 161 */ 162 public AuthorizationServiceConfiguration( 163 @NonNull AuthorizationServiceDiscovery discoveryDoc) { 164 checkNotNull(discoveryDoc, "docJson cannot be null"); 165 this.discoveryDoc = discoveryDoc; 166 this.authorizationEndpoint = discoveryDoc.getAuthorizationEndpoint(); 167 this.tokenEndpoint = discoveryDoc.getTokenEndpoint(); 168 this.registrationEndpoint = discoveryDoc.getRegistrationEndpoint(); 169 this.endSessionEndpoint = discoveryDoc.getEndSessionEndpoint(); 170 } 171 172 /** 173 * Converts the authorization service configuration to JSON for storage or transmission. 174 */ 175 @NonNull 176 public JSONObject toJson() { 177 JSONObject json = new JSONObject(); 178 JsonUtil.put(json, KEY_AUTHORIZATION_ENDPOINT, authorizationEndpoint.toString()); 179 JsonUtil.put(json, KEY_TOKEN_ENDPOINT, tokenEndpoint.toString()); 180 if (registrationEndpoint != null) { 181 JsonUtil.put(json, KEY_REGISTRATION_ENDPOINT, registrationEndpoint.toString()); 182 } 183 if (endSessionEndpoint != null) { 184 JsonUtil.put(json, KEY_END_SESSION_ENPOINT, endSessionEndpoint.toString()); 185 } 186 if (discoveryDoc != null) { 187 JsonUtil.put(json, KEY_DISCOVERY_DOC, discoveryDoc.docJson); 188 } 189 return json; 190 } 191 192 /** 193 * Converts the authorization service configuration to a JSON string for storage or 194 * transmission. 195 */ 196 public String toJsonString() { 197 return toJson().toString(); 198 } 199 200 /** 201 * Reads an Authorization service configuration from a JSON representation produced by the 202 * {@link #toJson()} method or some other equivalent producer. 203 * 204 * @throws JSONException if the provided JSON does not match the expected structure. 205 */ 206 @NonNull 207 public static AuthorizationServiceConfiguration fromJson(@NonNull JSONObject json) 208 throws JSONException { 209 checkNotNull(json, "json object cannot be null"); 210 211 if (json.has(KEY_DISCOVERY_DOC)) { 212 try { 213 AuthorizationServiceDiscovery discoveryDoc = 214 new AuthorizationServiceDiscovery(json.optJSONObject(KEY_DISCOVERY_DOC)); 215 return new AuthorizationServiceConfiguration(discoveryDoc); 216 } catch (AuthorizationServiceDiscovery.MissingArgumentException ex) { 217 throw new JSONException("Missing required field in discovery doc: " 218 + ex.getMissingField()); 219 } 220 } else { 221 checkArgument(json.has(KEY_AUTHORIZATION_ENDPOINT), "missing authorizationEndpoint"); 222 checkArgument(json.has(KEY_TOKEN_ENDPOINT), "missing tokenEndpoint"); 223 return new AuthorizationServiceConfiguration( 224 JsonUtil.getUri(json, KEY_AUTHORIZATION_ENDPOINT), 225 JsonUtil.getUri(json, KEY_TOKEN_ENDPOINT), 226 JsonUtil.getUriIfDefined(json, KEY_REGISTRATION_ENDPOINT), 227 JsonUtil.getUriIfDefined(json, KEY_END_SESSION_ENPOINT)); 228 } 229 } 230 231 /** 232 * Reads an Authorization service configuration from a JSON representation produced by the 233 * {@link #toJson()} method or some other equivalent producer. 234 * 235 * @throws JSONException if the provided JSON does not match the expected structure. 236 */ 237 public static AuthorizationServiceConfiguration fromJson(@NonNull String jsonStr) 238 throws JSONException { 239 checkNotNull(jsonStr, "json cannot be null"); 240 return AuthorizationServiceConfiguration.fromJson(new JSONObject(jsonStr)); 241 } 242 243 /** 244 * Fetch an AuthorizationServiceConfiguration from an OpenID Connect issuer URI. 245 * This method is equivalent to {@link #fetchFromUrl(Uri, RetrieveConfigurationCallback)}, 246 * but automatically appends the OpenID connect well-known configuration path to the 247 * URI. 248 * 249 * @param openIdConnectIssuerUri The issuer URI, e.g. "https://accounts.google.com" 250 * @param callback The callback to invoke upon completion. 251 * 252 * @see "OpenID Connect discovery 1.0 253 * <https://openid.net/specs/openid-connect-discovery-1_0.html>" 254 */ 255 public static void fetchFromIssuer(@NonNull Uri openIdConnectIssuerUri, 256 @NonNull RetrieveConfigurationCallback callback) { 257 fetchFromUrl(buildConfigurationUriFromIssuer(openIdConnectIssuerUri), callback); 258 } 259 260 /** 261 * Fetch an AuthorizationServiceConfiguration from an OpenID Connect issuer URI, using 262 * the {@link DefaultConnectionBuilder default connection builder}. 263 * This method is equivalent to {@link #fetchFromUrl(Uri, RetrieveConfigurationCallback, 264 * ConnectionBuilder)}, but automatically appends the OpenID connect well-known 265 * configuration path to the URI. 266 * 267 * @param openIdConnectIssuerUri The issuer URI, e.g. "https://accounts.google.com" 268 * @param connectionBuilder The connection builder that is used to establish a connection 269 * to the resource server. 270 * @param callback The callback to invoke upon completion. 271 * @see "OpenID Connect discovery 1.0 272 * <https://openid.net/specs/openid-connect-discovery-1_0.html>" 273 */ 274 public static void fetchFromIssuer(@NonNull Uri openIdConnectIssuerUri, 275 @NonNull RetrieveConfigurationCallback callback, 276 @NonNull ConnectionBuilder connectionBuilder) { 277 fetchFromUrl(buildConfigurationUriFromIssuer(openIdConnectIssuerUri), 278 callback, 279 connectionBuilder); 280 } 281 282 static Uri buildConfigurationUriFromIssuer(Uri openIdConnectIssuerUri) { 283 return openIdConnectIssuerUri.buildUpon() 284 .appendPath(WELL_KNOWN_PATH) 285 .appendPath(OPENID_CONFIGURATION_RESOURCE) 286 .build(); 287 } 288 289 /** 290 * Fetch a AuthorizationServiceConfiguration from an OpenID Connect discovery URI, using 291 * the {@link DefaultConnectionBuilder default connection builder}. 292 * 293 * @param openIdConnectDiscoveryUri The OpenID Connect discovery URI 294 * @param callback A callback to invoke upon completion 295 * 296 * @see "OpenID Connect discovery 1.0 297 * <https://openid.net/specs/openid-connect-discovery-1_0.html>" 298 */ 299 public static void fetchFromUrl(@NonNull Uri openIdConnectDiscoveryUri, 300 @NonNull RetrieveConfigurationCallback callback) { 301 fetchFromUrl(openIdConnectDiscoveryUri, 302 callback, 303 DefaultConnectionBuilder.INSTANCE); 304 } 305 306 /** 307 * Fetch a AuthorizationServiceConfiguration from an OpenID Connect discovery URI. 308 * 309 * @param openIdConnectDiscoveryUri The OpenID Connect discovery URI 310 * @param connectionBuilder The connection builder that is used to establish a connection 311 * to the resource server. 312 * @param callback A callback to invoke upon completion 313 * 314 * @see "OpenID Connect discovery 1.0 315 * <https://openid.net/specs/openid-connect-discovery-1_0.html>" 316 */ 317 public static void fetchFromUrl( 318 @NonNull Uri openIdConnectDiscoveryUri, 319 @NonNull RetrieveConfigurationCallback callback, 320 @NonNull ConnectionBuilder connectionBuilder) { 321 checkNotNull(openIdConnectDiscoveryUri, "openIDConnectDiscoveryUri cannot be null"); 322 checkNotNull(callback, "callback cannot be null"); 323 checkNotNull(connectionBuilder, "connectionBuilder must not be null"); 324 new ConfigurationRetrievalAsyncTask( 325 openIdConnectDiscoveryUri, 326 connectionBuilder, 327 callback) 328 .execute(); 329 } 330 331 /** 332 * Callback interface for configuration retrieval. 333 * @see AuthorizationServiceConfiguration#fetchFromUrl(Uri,RetrieveConfigurationCallback) 334 */ 335 public interface RetrieveConfigurationCallback { 336 /** 337 * Invoked when the retrieval of the discovery doc completes successfully or fails. 338 * 339 * <p>Exactly one of `serviceConfiguration` or `ex` will be non-null. If 340 * `serviceConfiguration` is `null`, a failure occurred during the request. This 341 * can happen if a bad URL was provided, no connection to the server could be established, 342 * or the retrieved JSON is incomplete or badly formatted. 343 * 344 * @param serviceConfiguration the service configuration that can be used to initialize 345 * the {@link AuthorizationService}, if retrieval was successful; `null` otherwise. 346 * @param ex the exception that caused an error. 347 */ 348 void onFetchConfigurationCompleted( 349 @Nullable AuthorizationServiceConfiguration serviceConfiguration, 350 @Nullable AuthorizationException ex); 351 } 352 353 /** 354 * ASyncTask that tries to retrieve the discover document and gives the callback with the 355 * values retrieved from the discovery document. In case of retrieval error, the exception 356 * is handed back to the callback. 357 */ 358 private static class ConfigurationRetrievalAsyncTask 359 extends AsyncTask<Void, Void, AuthorizationServiceConfiguration> { 360 361 private Uri mUri; 362 private ConnectionBuilder mConnectionBuilder; 363 private RetrieveConfigurationCallback mCallback; 364 private AuthorizationException mException; 365 366 ConfigurationRetrievalAsyncTask( 367 Uri uri, 368 ConnectionBuilder connectionBuilder, 369 RetrieveConfigurationCallback callback) { 370 mUri = uri; 371 mConnectionBuilder = connectionBuilder; 372 mCallback = callback; 373 mException = null; 374 } 375 376 @Override 377 protected AuthorizationServiceConfiguration doInBackground(Void... voids) { 378 InputStream is = null; 379 try { 380 HttpURLConnection conn = mConnectionBuilder.openConnection(mUri); 381 conn.setRequestMethod("GET"); 382 conn.setDoInput(true); 383 conn.connect(); 384 385 is = conn.getInputStream(); 386 JSONObject json = new JSONObject(Utils.readInputStream(is)); 387 388 AuthorizationServiceDiscovery discovery = 389 new AuthorizationServiceDiscovery(json); 390 return new AuthorizationServiceConfiguration(discovery); 391 } catch (IOException ex) { 392 Logger.errorWithStack(ex, "Network error when retrieving discovery document"); 393 mException = AuthorizationException.fromTemplate( 394 GeneralErrors.NETWORK_ERROR, 395 ex); 396 } catch (JSONException ex) { 397 Logger.errorWithStack(ex, "Error parsing discovery document"); 398 mException = AuthorizationException.fromTemplate( 399 GeneralErrors.JSON_DESERIALIZATION_ERROR, 400 ex); 401 } catch (AuthorizationServiceDiscovery.MissingArgumentException ex) { 402 Logger.errorWithStack(ex, "Malformed discovery document"); 403 mException = AuthorizationException.fromTemplate( 404 GeneralErrors.INVALID_DISCOVERY_DOCUMENT, 405 ex); 406 } finally { 407 Utils.closeQuietly(is); 408 } 409 return null; 410 } 411 412 @Override 413 protected void onPostExecute(AuthorizationServiceConfiguration configuration) { 414 if (mException != null) { 415 mCallback.onFetchConfigurationCompleted(null, mException); 416 } else { 417 mCallback.onFetchConfigurationCompleted(configuration, null); 418 } 419 } 420 } 421}