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}