net.openid.appauth.AuthorizationService.java Source code

Java tutorial

Introduction

Here is the source code for net.openid.appauth.AuthorizationService.java

Source

/*
 * Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.openid.appauth;

import static net.openid.appauth.Preconditions.checkNotNull;

import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.customtabs.CustomTabsIntent;
import android.text.TextUtils;

import net.openid.appauth.AuthorizationException.GeneralErrors;
import net.openid.appauth.AuthorizationException.RegistrationRequestErrors;
import net.openid.appauth.AuthorizationException.TokenRequestErrors;

import net.openid.appauth.browser.BrowserDescriptor;
import net.openid.appauth.browser.BrowserSelector;
import net.openid.appauth.browser.CustomTabManager;
import net.openid.appauth.internal.Logger;
import net.openid.appauth.internal.UriUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.util.Map;

/**
 * Dispatches requests to an OAuth2 authorization service. Note that instances of this class
 * _must be manually disposed_ when no longer required, to avoid leaks
 * (see {@link #dispose()}.
 */
public class AuthorizationService {

    @VisibleForTesting
    Context mContext;

    @NonNull
    private final AppAuthConfiguration mClientConfiguration;

    @NonNull
    private final CustomTabManager mCustomTabManager;

    @Nullable
    private final BrowserDescriptor mBrowser;

    private boolean mDisposed = false;

    /**
     * Creates an AuthorizationService instance, using the
     * {@link AppAuthConfiguration#DEFAULT default configuration}. Note that
     * instances of this class must be manually disposed when no longer required, to avoid
     * leaks (see {@link #dispose()}.
     */
    public AuthorizationService(@NonNull Context context) {
        this(context, AppAuthConfiguration.DEFAULT);
    }

    /**
     * Creates an AuthorizationService instance, using the specified configuration. Note that
     * instances of this class must be manually disposed when no longer required, to avoid
     * leaks (see {@link #dispose()}.
     */
    public AuthorizationService(@NonNull Context context, @NonNull AppAuthConfiguration clientConfiguration) {
        this(context, clientConfiguration, BrowserSelector.select(context, clientConfiguration.getBrowserMatcher()),
                new CustomTabManager(context));
    }

    /**
     * Constructor that injects a url builder into the service for testing.
     */
    @VisibleForTesting
    AuthorizationService(@NonNull Context context, @NonNull AppAuthConfiguration clientConfiguration,
            @Nullable BrowserDescriptor browser, @NonNull CustomTabManager customTabManager) {
        mContext = checkNotNull(context);
        mClientConfiguration = clientConfiguration;
        mCustomTabManager = customTabManager;
        mBrowser = browser;

        if (browser != null && browser.useCustomTab) {
            mCustomTabManager.bind(browser.packageName);
        }
    }

    public CustomTabManager getCustomTabManager() {
        return mCustomTabManager;
    }

    /**
     * Creates a custom tab builder, that will use a tab session from an existing connection to
     * a web browser, if available.
     */
    public CustomTabsIntent.Builder createCustomTabsIntentBuilder(Uri... possibleUris) {
        checkNotDisposed();
        return mCustomTabManager.createTabBuilder(possibleUris);
    }

    /**
     * Sends an authorization request to the authorization service, using a
     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
     * if available, or a browser instance.
     * The parameters of this request are determined by both the authorization service
     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
     * If the user cancels the authorization request, the current activity will regain control.
     */
    public void performAuthorizationRequest(@NonNull AuthorizationRequest request,
            @NonNull PendingIntent completedIntent) {
        performAuthorizationRequest(request, completedIntent, null, createCustomTabsIntentBuilder().build());
    }

    /**
     * Sends an authorization request to the authorization service, using a
     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
     * if available, or a browser instance.
     * The parameters of this request are determined by both the authorization service
     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
     * If the user cancels the authorization request, the provided
     * {@link PendingIntent cancel PendingIntent} will be invoked.
     */
    public void performAuthorizationRequest(@NonNull AuthorizationRequest request,
            @NonNull PendingIntent completedIntent, @NonNull PendingIntent canceledIntent) {
        performAuthorizationRequest(request, completedIntent, canceledIntent,
                createCustomTabsIntentBuilder().build());
    }

    /**
     * Sends an authorization request to the authorization service, using a
     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
     * The parameters of this request are determined by both the authorization service
     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
     * If the user cancels the authorization request, the current activity will regain control.
     *
     * @param customTabsIntent
     *     The intent that will be used to start the custom tab. It is recommended that this intent
     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
     */
    public void performAuthorizationRequest(@NonNull AuthorizationRequest request,
            @NonNull PendingIntent completedIntent, @NonNull CustomTabsIntent customTabsIntent) {
        performAuthorizationRequest(request, completedIntent, null, customTabsIntent);
    }

    /**
     * Sends an authorization request to the authorization service, using a
     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
     * The parameters of this request are determined by both the authorization service
     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
     * If the user cancels the authorization request, the provided
     * {@link PendingIntent cancel PendingIntent} will be invoked.
     *
     * @param customTabsIntent
     *     The intent that will be used to start the custom tab. It is recommended that this intent
     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
     *
     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
     *     perform the authorization flow.
     */
    public void performAuthorizationRequest(@NonNull AuthorizationRequest request,
            @NonNull PendingIntent completedIntent, @Nullable PendingIntent canceledIntent,
            @NonNull CustomTabsIntent customTabsIntent) {
        checkNotDisposed();

        if (mBrowser == null) {
            throw new ActivityNotFoundException();
        }

        Uri requestUri = request.toUri();
        Intent intent;
        if (mBrowser.useCustomTab) {
            intent = customTabsIntent.intent;
        } else {
            intent = new Intent(Intent.ACTION_VIEW);
        }
        intent.setPackage(mBrowser.packageName);
        intent.setData(requestUri);

        Logger.debug("Using %s as browser for auth, custom tab = %s", intent.getPackage(),
                mBrowser.useCustomTab.toString());
        intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.NO_TITLE);

        Logger.debug("Initiating authorization request to %s", request.configuration.authorizationEndpoint);
        mContext.startActivity(AuthorizationManagementActivity.createStartIntent(mContext, request, intent,
                completedIntent, canceledIntent));
    }

    /**
     * Sends a request to the authorization service to exchange a code granted as part of an
     * authorization request for a token. The result of this request will be sent to the provided
     * callback handler.
     */
    public void performTokenRequest(@NonNull TokenRequest request, @NonNull TokenResponseCallback callback) {
        performTokenRequest(request, NoClientAuthentication.INSTANCE, callback);
    }

    /**
     * Sends a request to the authorization service to exchange a code granted as part of an
     * authorization request for a token. The result of this request will be sent to the provided
     * callback handler.
     */
    public void performTokenRequest(@NonNull TokenRequest request,
            @NonNull ClientAuthentication clientAuthentication, @NonNull TokenResponseCallback callback) {
        checkNotDisposed();
        Logger.debug("Initiating code exchange request to %s", request.configuration.tokenEndpoint);
        new TokenRequestTask(request, clientAuthentication, callback).execute();
    }

    /**
     * Sends a request to the authorization service to dynamically register a client.
     * The result of this request will be sent to the provided callback handler.
     */
    public void performRegistrationRequest(@NonNull RegistrationRequest request,
            @NonNull RegistrationResponseCallback callback) {
        checkNotDisposed();
        Logger.debug("Initiating dynamic client registration %s",
                request.configuration.registrationEndpoint.toString());
        new RegistrationRequestTask(request, callback).execute();
    }

    /**
     * Disposes state that will not normally be handled by garbage collection. This should be
     * called when the authorization service is no longer required, including when any owning
     * activity is paused or destroyed (i.e. in {@link android.app.Activity#onStop()}).
     */
    public void dispose() {
        if (mDisposed) {
            return;
        }
        mCustomTabManager.dispose();
        mDisposed = true;
    }

    private void checkNotDisposed() {
        if (mDisposed) {
            throw new IllegalStateException("Service has been disposed and rendered inoperable");
        }
    }

    private class TokenRequestTask extends AsyncTask<Void, Void, JSONObject> {
        private TokenRequest mRequest;
        private TokenResponseCallback mCallback;
        private ClientAuthentication mClientAuthentication;

        private AuthorizationException mException;

        TokenRequestTask(TokenRequest request, @NonNull ClientAuthentication clientAuthentication,
                TokenResponseCallback callback) {
            mRequest = request;
            mCallback = callback;
            mClientAuthentication = clientAuthentication;
        }

        @Override
        protected JSONObject doInBackground(Void... voids) {
            InputStream is = null;
            try {
                HttpURLConnection conn = mClientConfiguration.getConnectionBuilder()
                        .openConnection(mRequest.configuration.tokenEndpoint);
                conn.setRequestMethod("POST");
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                addJsonToAcceptHeader(conn);
                conn.setDoOutput(true);

                Map<String, String> headers = mClientAuthentication.getRequestHeaders(mRequest.clientId);
                if (headers != null) {
                    for (Map.Entry<String, String> header : headers.entrySet()) {
                        conn.setRequestProperty(header.getKey(), header.getValue());
                    }
                }

                Map<String, String> parameters = mRequest.getRequestParameters();
                Map<String, String> clientAuthParams = mClientAuthentication
                        .getRequestParameters(mRequest.clientId);
                if (clientAuthParams != null) {
                    parameters.putAll(clientAuthParams);
                }

                String queryData = UriUtil.formUrlEncode(parameters);
                conn.setRequestProperty("Content-Length", String.valueOf(queryData.length()));
                OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());

                wr.write(queryData);
                wr.flush();

                if (conn.getResponseCode() >= HttpURLConnection.HTTP_OK
                        && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) {
                    is = conn.getInputStream();
                } else {
                    is = conn.getErrorStream();
                }
                String response = Utils.readInputStream(is);
                return new JSONObject(response);
            } catch (IOException ex) {
                Logger.debugWithStack(ex, "Failed to complete exchange request");
                mException = AuthorizationException.fromTemplate(GeneralErrors.NETWORK_ERROR, ex);
            } catch (JSONException ex) {
                Logger.debugWithStack(ex, "Failed to complete exchange request");
                mException = AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, ex);
            } finally {
                Utils.closeQuietly(is);
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject json) {
            if (mException != null) {
                mCallback.onTokenRequestCompleted(null, mException);
                return;
            }

            if (json.has(AuthorizationException.PARAM_ERROR)) {
                AuthorizationException ex;
                try {
                    String error = json.getString(AuthorizationException.PARAM_ERROR);
                    ex = AuthorizationException.fromOAuthTemplate(TokenRequestErrors.byString(error), error,
                            json.optString(AuthorizationException.PARAM_ERROR_DESCRIPTION, null),
                            UriUtil.parseUriIfAvailable(json.optString(AuthorizationException.PARAM_ERROR_URI)));
                } catch (JSONException jsonEx) {
                    ex = AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, jsonEx);
                }
                mCallback.onTokenRequestCompleted(null, ex);
                return;
            }

            TokenResponse response;
            try {
                response = new TokenResponse.Builder(mRequest).fromResponseJson(json).build();
            } catch (JSONException jsonEx) {
                mCallback.onTokenRequestCompleted(null,
                        AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, jsonEx));
                return;
            }

            Logger.debug("Token exchange with %s completed", mRequest.configuration.tokenEndpoint);
            mCallback.onTokenRequestCompleted(response, null);
        }

        /**
         * GitHub will only return a spec-compliant response if JSON is explicitly defined
         * as an acceptable response type. As this is essentially harmless for all other
         * spec-compliant IDPs, we add this header if no existing Accept header has been set
         * by the connection builder.
         */
        private void addJsonToAcceptHeader(URLConnection conn) {
            if (TextUtils.isEmpty(conn.getRequestProperty("Accept"))) {
                conn.setRequestProperty("Accept", "application/json");
            }
        }
    }

    /**
     * Callback interface for token endpoint requests.
     * @see AuthorizationService#performTokenRequest
     */
    public interface TokenResponseCallback {
        /**
         * Invoked when the request completes successfully or fails.
         *
         * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure
         * occurred during the request. This can happen if a bad URI was provided, no connection
         * to the server could be established, or the response JSON was incomplete or incorrectly
         * formatted.
         *
         * @param response the retrieved token response, if successful; `null` otherwise.
         * @param ex a description of the failure, if one occurred: `null` otherwise.
         *
         * @see AuthorizationException.TokenRequestErrors
         */
        void onTokenRequestCompleted(@Nullable TokenResponse response, @Nullable AuthorizationException ex);
    }

    private class RegistrationRequestTask extends AsyncTask<Void, Void, JSONObject> {
        private RegistrationRequest mRequest;
        private RegistrationResponseCallback mCallback;

        private AuthorizationException mException;

        RegistrationRequestTask(RegistrationRequest request, RegistrationResponseCallback callback) {
            mRequest = request;
            mCallback = callback;
        }

        @Override
        protected JSONObject doInBackground(Void... voids) {
            InputStream is = null;
            String postData = mRequest.toJsonString();
            try {
                HttpURLConnection conn = mClientConfiguration.getConnectionBuilder()
                        .openConnection(mRequest.configuration.registrationEndpoint);
                conn.setRequestMethod("POST");
                conn.setRequestProperty("Content-Type", "application/json");
                conn.setDoOutput(true);
                conn.setRequestProperty("Content-Length", String.valueOf(postData.length()));
                OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
                wr.write(postData);
                wr.flush();

                is = conn.getInputStream();
                String response = Utils.readInputStream(is);
                return new JSONObject(response);
            } catch (IOException ex) {
                Logger.debugWithStack(ex, "Failed to complete registration request");
                mException = AuthorizationException.fromTemplate(GeneralErrors.NETWORK_ERROR, ex);
            } catch (JSONException ex) {
                Logger.debugWithStack(ex, "Failed to complete registration request");
                mException = AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, ex);
            } finally {
                Utils.closeQuietly(is);
            }
            return null;
        }

        @Override
        protected void onPostExecute(JSONObject json) {
            if (mException != null) {
                mCallback.onRegistrationRequestCompleted(null, mException);
                return;
            }

            if (json.has(AuthorizationException.PARAM_ERROR)) {
                AuthorizationException ex;
                try {
                    String error = json.getString(AuthorizationException.PARAM_ERROR);
                    ex = AuthorizationException.fromOAuthTemplate(RegistrationRequestErrors.byString(error), error,
                            json.getString(AuthorizationException.PARAM_ERROR_DESCRIPTION),
                            UriUtil.parseUriIfAvailable(json.getString(AuthorizationException.PARAM_ERROR_URI)));
                } catch (JSONException jsonEx) {
                    ex = AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, jsonEx);
                }
                mCallback.onRegistrationRequestCompleted(null, ex);
                return;
            }

            RegistrationResponse response;
            try {
                response = new RegistrationResponse.Builder(mRequest).fromResponseJson(json).build();
            } catch (JSONException jsonEx) {
                mCallback.onRegistrationRequestCompleted(null,
                        AuthorizationException.fromTemplate(GeneralErrors.JSON_DESERIALIZATION_ERROR, jsonEx));
                return;
            } catch (RegistrationResponse.MissingArgumentException ex) {
                Logger.errorWithStack(ex, "Malformed registration response");
                mException = AuthorizationException.fromTemplate(GeneralErrors.INVALID_REGISTRATION_RESPONSE, ex);
                return;
            }
            Logger.debug("Dynamic registration with %s completed", mRequest.configuration.registrationEndpoint);
            mCallback.onRegistrationRequestCompleted(response, null);
        }
    }

    /**
     * Callback interface for token endpoint requests.
     *
     * @see AuthorizationService#performTokenRequest
     */
    public interface RegistrationResponseCallback {
        /**
         * Invoked when the request completes successfully or fails.
         *
         * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure
         * occurred during the request. This can happen if an invalid URI was provided, no
         * connection to the server could be established, or the response JSON was incomplete or
         * incorrectly formatted.
         *
         * @param response the retrieved registration response, if successful; `null` otherwise.
         * @param ex a description of the failure, if one occurred: `null` otherwise.
         * @see AuthorizationException.RegistrationRequestErrors
         */
        void onRegistrationRequestCompleted(@Nullable RegistrationResponse response,
                @Nullable AuthorizationException ex);
    }
}