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.checkNotNull;
018
019import android.annotation.TargetApi;
020import android.app.Activity;
021import android.app.PendingIntent;
022import android.content.ActivityNotFoundException;
023import android.content.Context;
024import android.content.ContextWrapper;
025import android.content.Intent;
026import android.net.Uri;
027import android.os.AsyncTask;
028import android.os.Build;
029import android.text.TextUtils;
030import androidx.annotation.NonNull;
031import androidx.annotation.Nullable;
032import androidx.annotation.VisibleForTesting;
033import androidx.browser.customtabs.CustomTabsIntent;
034
035import net.openid.appauth.AuthorizationException.GeneralErrors;
036import net.openid.appauth.AuthorizationException.RegistrationRequestErrors;
037import net.openid.appauth.AuthorizationException.TokenRequestErrors;
038import net.openid.appauth.IdToken.IdTokenException;
039import net.openid.appauth.browser.BrowserDescriptor;
040import net.openid.appauth.browser.BrowserSelector;
041import net.openid.appauth.browser.CustomTabManager;
042import net.openid.appauth.connectivity.ConnectionBuilder;
043import net.openid.appauth.internal.Logger;
044import net.openid.appauth.internal.UriUtil;
045import org.json.JSONException;
046import org.json.JSONObject;
047
048import java.io.IOException;
049import java.io.InputStream;
050import java.io.OutputStreamWriter;
051import java.net.HttpURLConnection;
052import java.net.URLConnection;
053import java.util.Map;
054
055
056/**
057 * Dispatches requests to an OAuth2 authorization service. Note that instances of this class
058 * _must be manually disposed_ when no longer required, to avoid leaks
059 * (see {@link #dispose()}.
060 */
061public class AuthorizationService {
062
063    @VisibleForTesting
064    Context mContext;
065
066    @NonNull
067    private final AppAuthConfiguration mClientConfiguration;
068
069    @NonNull
070    private final CustomTabManager mCustomTabManager;
071
072    @Nullable
073    private final BrowserDescriptor mBrowser;
074
075    private boolean mDisposed = false;
076
077    /**
078     * Creates an AuthorizationService instance, using the
079     * {@link AppAuthConfiguration#DEFAULT default configuration}. Note that
080     * instances of this class must be manually disposed when no longer required, to avoid
081     * leaks (see {@link #dispose()}.
082     */
083    public AuthorizationService(@NonNull Context context) {
084        this(context, AppAuthConfiguration.DEFAULT);
085    }
086
087    /**
088     * Creates an AuthorizationService instance, using the specified configuration. Note that
089     * instances of this class must be manually disposed when no longer required, to avoid
090     * leaks (see {@link #dispose()}.
091     */
092    public AuthorizationService(
093            @NonNull Context context,
094            @NonNull AppAuthConfiguration clientConfiguration) {
095        this(context,
096                clientConfiguration,
097                BrowserSelector.select(
098                        context,
099                        clientConfiguration.getBrowserMatcher()),
100                new CustomTabManager(context));
101    }
102
103    /**
104     * Constructor that injects a url builder into the service for testing.
105     */
106    @VisibleForTesting
107    AuthorizationService(@NonNull Context context,
108                         @NonNull AppAuthConfiguration clientConfiguration,
109                         @Nullable BrowserDescriptor browser,
110                         @NonNull CustomTabManager customTabManager) {
111        mContext = checkNotNull(context);
112        mClientConfiguration = clientConfiguration;
113        mCustomTabManager = customTabManager;
114        mBrowser = browser;
115
116        if (browser != null && browser.useCustomTab) {
117            mCustomTabManager.bind(browser.packageName);
118        }
119    }
120
121    public CustomTabManager getCustomTabManager() {
122        return mCustomTabManager;
123    }
124
125    /**
126     * Returns the BrowserDescriptor of the chosen browser.
127     * Can for example be used to set the browsers package name to a CustomTabsIntent.
128     */
129    public BrowserDescriptor getBrowserDescriptor() {
130        return mBrowser;
131    }
132
133    /**
134     * Creates a custom tab builder, that will use a tab session from an existing connection to
135     * a web browser, if available.
136     */
137    public CustomTabsIntent.Builder createCustomTabsIntentBuilder(Uri... possibleUris) {
138        checkNotDisposed();
139        return mCustomTabManager.createTabBuilder(possibleUris);
140    }
141
142    /**
143     * Sends an authorization request to the authorization service, using a
144     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
145     * if available, or a browser instance.
146     * The parameters of this request are determined by both the authorization service
147     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
148     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
149     * If the user cancels the authorization request, the current activity will regain control.
150     */
151    public void performAuthorizationRequest(
152            @NonNull AuthorizationRequest request,
153            @NonNull PendingIntent completedIntent) {
154        performAuthorizationRequest(
155                request,
156                completedIntent,
157                null,
158                createCustomTabsIntentBuilder().build());
159    }
160
161    /**
162     * Sends an authorization request to the authorization service, using a
163     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
164     * if available, or a browser instance.
165     * The parameters of this request are determined by both the authorization service
166     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
167     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
168     * If the user cancels the authorization request, the provided
169     * {@link PendingIntent cancel PendingIntent} will be invoked.
170     */
171    public void performAuthorizationRequest(
172            @NonNull AuthorizationRequest request,
173            @NonNull PendingIntent completedIntent,
174            @NonNull PendingIntent canceledIntent) {
175        performAuthorizationRequest(
176                request,
177                completedIntent,
178                canceledIntent,
179                createCustomTabsIntentBuilder().build());
180    }
181
182    /**
183     * Sends an authorization request to the authorization service, using a
184     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
185     * The parameters of this request are determined by both the authorization service
186     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
187     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
188     * If the user cancels the authorization request, the current activity will regain control.
189     *
190     * @param customTabsIntent
191     *     The intent that will be used to start the custom tab. It is recommended that this intent
192     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
193     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
194     */
195    public void performAuthorizationRequest(
196            @NonNull AuthorizationRequest request,
197            @NonNull PendingIntent completedIntent,
198            @NonNull CustomTabsIntent customTabsIntent) {
199        performAuthorizationRequest(
200                request,
201                completedIntent,
202                null,
203                customTabsIntent);
204    }
205
206    /**
207     * Sends an authorization request to the authorization service, using a
208     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
209     * The parameters of this request are determined by both the authorization service
210     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
211     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
212     * If the user cancels the authorization request, the provided
213     * {@link PendingIntent cancel PendingIntent} will be invoked.
214     *
215     * @param customTabsIntent
216     *     The intent that will be used to start the custom tab. It is recommended that this intent
217     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
218     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
219     *
220     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
221     *     perform the authorization flow.
222     */
223    public void performAuthorizationRequest(
224            @NonNull AuthorizationRequest request,
225            @NonNull PendingIntent completedIntent,
226            @Nullable PendingIntent canceledIntent,
227            @NonNull CustomTabsIntent customTabsIntent) {
228        performAuthManagementRequest(
229                request,
230                completedIntent,
231                canceledIntent,
232                customTabsIntent);
233    }
234
235    /**
236     * Sends an end session request to the authorization service, using a
237     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
238     * if available, or a browser instance.
239     * The parameters of this request are determined by both the authorization service
240     * configuration and the provided {@link EndSessionRequest request object}. Upon completion
241     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
242     * If the user cancels the authorization request, the current activity will regain control.
243     */
244    public void performEndSessionRequest(
245            @NonNull EndSessionRequest request,
246            @NonNull PendingIntent completedIntent) {
247        performEndSessionRequest(
248                request,
249                completedIntent,
250                null,
251                createCustomTabsIntentBuilder().build());
252    }
253
254    /**
255     * Sends an end session request to the authorization service, using a
256     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs)
257     * if available, or a browser instance.
258     * The parameters of this request are determined by both the authorization service
259     * configuration and the provided {@link EndSessionRequest request object}. Upon completion
260     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
261     * If the user cancels the authorization request, the provided
262     * {@link PendingIntent cancel PendingIntent} will be invoked.
263     */
264    public void performEndSessionRequest(
265            @NonNull EndSessionRequest request,
266            @NonNull PendingIntent completedIntent,
267            @NonNull PendingIntent canceledIntent) {
268        performEndSessionRequest(
269                request,
270                completedIntent,
271                canceledIntent,
272                createCustomTabsIntentBuilder().build());
273    }
274
275    /**
276     * Sends an end session request to the authorization service, using a
277     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
278     * The parameters of this request are determined by both the authorization service
279     * configuration and the provided {@link EndSessionRequest request object}. Upon completion
280     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
281     * If the user cancels the authorization request, the current activity will regain control.
282     *
283     * @param customTabsIntent
284     *     The intent that will be used to start the custom tab. It is recommended that this intent
285     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
286     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
287     */
288    public void performEndSessionRequest(
289            @NonNull EndSessionRequest request,
290            @NonNull PendingIntent completedIntent,
291            @NonNull CustomTabsIntent customTabsIntent) {
292        performEndSessionRequest(
293                request,
294                completedIntent,
295                null,
296                customTabsIntent);
297    }
298
299    /**
300     * Sends an end session request to the authorization service, using a
301     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
302     * The parameters of this request are determined by both the authorization service
303     * configuration and the provided {@link EndSessionRequest request object}. Upon completion
304     * of this request, the provided {@link PendingIntent completion PendingIntent} will be invoked.
305     * If the user cancels the authorization request, the provided
306     * {@link PendingIntent cancel PendingIntent} will be invoked.
307     *
308     * @param customTabsIntent
309     *     The intent that will be used to start the custom tab. It is recommended that this intent
310     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
311     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
312     *
313     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
314     *     perform the authorization flow.
315     */
316    public void performEndSessionRequest(
317            @NonNull EndSessionRequest request,
318            @NonNull PendingIntent completedIntent,
319            @Nullable PendingIntent canceledIntent,
320            @NonNull CustomTabsIntent customTabsIntent) {
321        performAuthManagementRequest(
322                request,
323                completedIntent,
324                canceledIntent,
325                customTabsIntent);
326    }
327
328    private void performAuthManagementRequest(
329            @NonNull AuthorizationManagementRequest request,
330            @NonNull PendingIntent completedIntent,
331            @Nullable PendingIntent canceledIntent,
332            @NonNull CustomTabsIntent customTabsIntent) {
333
334        checkNotDisposed();
335        checkNotNull(request);
336        checkNotNull(completedIntent);
337        checkNotNull(customTabsIntent);
338
339        Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent);
340        Intent startIntent = AuthorizationManagementActivity.createStartIntent(
341                mContext,
342                request,
343                authIntent,
344                completedIntent,
345                canceledIntent);
346
347        // Calling start activity from outside an activity requires FLAG_ACTIVITY_NEW_TASK.
348        if (!isActivity(mContext)) {
349            startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
350        }
351        mContext.startActivity(startIntent);
352    }
353
354    private boolean isActivity(Context context) {
355        while (context instanceof ContextWrapper) {
356            if (context instanceof Activity) {
357                return true;
358            }
359            context = ((ContextWrapper) context).getBaseContext();
360        }
361        return false;
362    }
363
364    /**
365     * Constructs an intent that encapsulates the provided request and custom tabs intent,
366     * and is intended to be launched via {@link Activity#startActivityForResult}.
367     * The parameters of this request are determined by both the authorization service
368     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
369     * of this request, the activity that gets launched will call {@link Activity#setResult} with
370     * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion
371     * information. If the user presses the back button or closes the browser tab, the launched
372     * activity will call {@link Activity#setResult} with
373     * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that
374     * {@link Activity#RESULT_OK} indicates the authorization request completed,
375     * not necessarily that it was a successful authorization.
376     *
377     * @param customTabsIntent
378     *     The intent that will be used to start the custom tab. It is recommended that this intent
379     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
380     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
381     *
382     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
383     *     perform the authorization flow.
384     */
385    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
386    public Intent getAuthorizationRequestIntent(
387            @NonNull AuthorizationRequest request,
388            @NonNull CustomTabsIntent customTabsIntent) {
389
390        Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent);
391        return AuthorizationManagementActivity.createStartForResultIntent(
392                mContext,
393                request,
394                authIntent);
395    }
396
397    /**
398     * Constructs an intent that encapsulates the provided request and a default custom tabs intent,
399     * and is intended to be launched via {@link Activity#startActivityForResult}
400     * When started, the intent launches an {@link Activity} that sends an authorization request
401     * to the authorization service, using a
402     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
403     * The parameters of this request are determined by both the authorization service
404     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
405     * of this request, the activity that gets launched will call {@link Activity#setResult} with
406     * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion
407     * information. If the user presses the back button or closes the browser tab, the launched
408     * activity will call {@link Activity#setResult} with
409     * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that
410     * {@link Activity#RESULT_OK} indicates the authorization request completed,
411     * not necessarily that it was a successful authorization.
412     *
413     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
414     *     perform the authorization flow.
415     */
416    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
417    public Intent getAuthorizationRequestIntent(
418            @NonNull AuthorizationRequest request) {
419        return getAuthorizationRequestIntent(request, createCustomTabsIntentBuilder().build());
420    }
421
422    /**
423     * Constructs an intent that encapsulates the provided request and custom tabs intent,
424     * and is intended to be launched via {@link Activity#startActivityForResult}.
425     * The parameters of this request are determined by both the authorization service
426     * configuration and the provided {@link AuthorizationRequest request object}. Upon completion
427     * of this request, the activity that gets launched will call {@link Activity#setResult} with
428     * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion
429     * information. If the user presses the back button or closes the browser tab, the launched
430     * activity will call {@link Activity#setResult} with
431     * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that
432     * {@link Activity#RESULT_OK} indicates the authorization request completed,
433     * not necessarily that it was a successful authorization.
434     *
435     * @param customTabsIntent
436     *     The intent that will be used to start the custom tab. It is recommended that this intent
437     *     be created with the help of {@link #createCustomTabsIntentBuilder(Uri[])}, which will
438     *     ensure that a warmed-up version of the browser will be used, minimizing latency.
439     *
440     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
441     *     perform the authorization flow.
442     */
443    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
444    public Intent getEndSessionRequestIntent(
445            @NonNull EndSessionRequest request,
446            @NonNull CustomTabsIntent customTabsIntent) {
447
448        Intent authIntent = prepareAuthorizationRequestIntent(request, customTabsIntent);
449        return AuthorizationManagementActivity.createStartForResultIntent(
450            mContext,
451            request,
452            authIntent);
453    }
454
455    /**
456     * Constructs an intent that encapsulates the provided request and a default custom tabs intent,
457     * and is intended to be launched via {@link Activity#startActivityForResult}
458     * When started, the intent launches an {@link Activity} that sends an authorization request
459     * to the authorization service, using a
460     * [custom tab](https://developer.chrome.com/multidevice/android/customtabs).
461     * The parameters of this request are determined by both the authorization service
462     * configuration and the provided {@link EndSessionRequest request object}. Upon completion
463     * of this request, the activity that gets launched will call {@link Activity#setResult} with
464     * {@link Activity#RESULT_OK} and an {@link Intent} containing authorization completion
465     * information. If the user presses the back button or closes the browser tab, the launched
466     * activity will call {@link Activity#setResult} with
467     * {@link Activity#RESULT_CANCELED} without a data {@link Intent}. Note that
468     * {@link Activity#RESULT_OK} indicates the authorization request completed,
469     * not necessarily that it was a successful authorization.
470     *
471     * @throws android.content.ActivityNotFoundException if no suitable browser is available to
472     *     perform the authorization flow.
473     */
474    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
475    public Intent getEndSessionRequestIntent(
476            @NonNull EndSessionRequest request) {
477        return getEndSessionRequestIntent(request, createCustomTabsIntentBuilder().build());
478    }
479
480    /**
481     * Sends a request to the authorization service to exchange a code granted as part of an
482     * authorization request for a token. The result of this request will be sent to the provided
483     * callback handler.
484     */
485    public void performTokenRequest(
486            @NonNull TokenRequest request,
487            @NonNull TokenResponseCallback callback) {
488        performTokenRequest(request, NoClientAuthentication.INSTANCE, callback);
489    }
490
491    /**
492     * Sends a request to the authorization service to exchange a code granted as part of an
493     * authorization request for a token. The result of this request will be sent to the provided
494     * callback handler.
495     */
496    public void performTokenRequest(
497            @NonNull TokenRequest request,
498            @NonNull ClientAuthentication clientAuthentication,
499            @NonNull TokenResponseCallback callback) {
500        checkNotDisposed();
501        Logger.debug("Initiating code exchange request to %s",
502                request.configuration.tokenEndpoint);
503        new TokenRequestTask(
504                request,
505                clientAuthentication,
506                mClientConfiguration.getConnectionBuilder(),
507                SystemClock.INSTANCE,
508                callback,
509                mClientConfiguration.getSkipIssuerHttpsCheck())
510                .execute();
511    }
512
513    /**
514     * Sends a request to the authorization service to dynamically register a client.
515     * The result of this request will be sent to the provided callback handler.
516     */
517    public void performRegistrationRequest(
518            @NonNull RegistrationRequest request,
519            @NonNull RegistrationResponseCallback callback) {
520        checkNotDisposed();
521        Logger.debug("Initiating dynamic client registration %s",
522                request.configuration.registrationEndpoint.toString());
523        new RegistrationRequestTask(
524                request,
525                mClientConfiguration.getConnectionBuilder(),
526                callback)
527                .execute();
528    }
529
530    /**
531     * Disposes state that will not normally be handled by garbage collection. This should be
532     * called when the authorization service is no longer required, including when any owning
533     * activity is paused or destroyed (i.e. in {@link android.app.Activity#onStop()}).
534     */
535    public void dispose() {
536        if (mDisposed) {
537            return;
538        }
539        mCustomTabManager.dispose();
540        mDisposed = true;
541    }
542
543    private void checkNotDisposed() {
544        if (mDisposed) {
545            throw new IllegalStateException("Service has been disposed and rendered inoperable");
546        }
547    }
548
549    private Intent prepareAuthorizationRequestIntent(
550            AuthorizationManagementRequest request,
551            CustomTabsIntent customTabsIntent) {
552        checkNotDisposed();
553
554        if (mBrowser == null) {
555            throw new ActivityNotFoundException();
556        }
557
558        Uri requestUri = request.toUri();
559        Intent intent;
560        if (mBrowser.useCustomTab) {
561            intent = customTabsIntent.intent;
562        } else {
563            intent = new Intent(Intent.ACTION_VIEW);
564        }
565        intent.setPackage(mBrowser.packageName);
566        intent.setData(requestUri);
567
568        Logger.debug("Using %s as browser for auth, custom tab = %s",
569                intent.getPackage(),
570                mBrowser.useCustomTab.toString());
571
572        //TODO fix logger for configuration
573        //Logger.debug("Initiating authorization request to %s"
574        //request.configuration.authorizationEndpoint);
575
576        return intent;
577    }
578
579    private static class TokenRequestTask
580            extends AsyncTask<Void, Void, JSONObject> {
581
582        private TokenRequest mRequest;
583        private ClientAuthentication mClientAuthentication;
584        private final ConnectionBuilder mConnectionBuilder;
585        private TokenResponseCallback mCallback;
586        private Clock mClock;
587        private boolean mSkipIssuerHttpsCheck;
588
589        private AuthorizationException mException;
590
591        TokenRequestTask(TokenRequest request,
592                         @NonNull ClientAuthentication clientAuthentication,
593                         @NonNull ConnectionBuilder connectionBuilder,
594                         Clock clock,
595                         TokenResponseCallback callback,
596                         Boolean skipIssuerHttpsCheck) {
597            mRequest = request;
598            mClientAuthentication = clientAuthentication;
599            mConnectionBuilder = connectionBuilder;
600            mClock = clock;
601            mCallback = callback;
602            mSkipIssuerHttpsCheck = skipIssuerHttpsCheck;
603        }
604
605        @Override
606        protected JSONObject doInBackground(Void... voids) {
607            InputStream is = null;
608            try {
609                HttpURLConnection conn = mConnectionBuilder.openConnection(
610                        mRequest.configuration.tokenEndpoint);
611                conn.setRequestMethod("POST");
612                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
613                addJsonToAcceptHeader(conn);
614                conn.setDoOutput(true);
615
616                Map<String, String> headers = mClientAuthentication
617                        .getRequestHeaders(mRequest.clientId);
618                if (headers != null) {
619                    for (Map.Entry<String,String> header : headers.entrySet()) {
620                        conn.setRequestProperty(header.getKey(), header.getValue());
621                    }
622                }
623
624                Map<String, String> parameters = mRequest.getRequestParameters();
625                Map<String, String> clientAuthParams = mClientAuthentication
626                        .getRequestParameters(mRequest.clientId);
627                if (clientAuthParams != null) {
628                    parameters.putAll(clientAuthParams);
629                }
630
631                String queryData = UriUtil.formUrlEncode(parameters);
632                conn.setRequestProperty("Content-Length", String.valueOf(queryData.length()));
633                OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
634
635                wr.write(queryData);
636                wr.flush();
637
638                if (conn.getResponseCode() >= HttpURLConnection.HTTP_OK
639                        && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) {
640                    is = conn.getInputStream();
641                } else {
642                    is = conn.getErrorStream();
643                }
644                String response = Utils.readInputStream(is);
645                return new JSONObject(response);
646            } catch (IOException ex) {
647                Logger.debugWithStack(ex, "Failed to complete exchange request");
648                mException = AuthorizationException.fromTemplate(
649                        GeneralErrors.NETWORK_ERROR, ex);
650            } catch (JSONException ex) {
651                Logger.debugWithStack(ex, "Failed to complete exchange request");
652                mException = AuthorizationException.fromTemplate(
653                        GeneralErrors.JSON_DESERIALIZATION_ERROR, ex);
654            } finally {
655                Utils.closeQuietly(is);
656            }
657            return null;
658        }
659
660        @Override
661        protected void onPostExecute(JSONObject json) {
662            if (mException != null) {
663                mCallback.onTokenRequestCompleted(null, mException);
664                return;
665            }
666
667            if (json.has(AuthorizationException.PARAM_ERROR)) {
668                AuthorizationException ex;
669                try {
670                    String error = json.getString(AuthorizationException.PARAM_ERROR);
671                    ex = AuthorizationException.fromOAuthTemplate(
672                            TokenRequestErrors.byString(error),
673                            error,
674                            json.optString(AuthorizationException.PARAM_ERROR_DESCRIPTION, null),
675                            UriUtil.parseUriIfAvailable(
676                                    json.optString(AuthorizationException.PARAM_ERROR_URI)));
677                } catch (JSONException jsonEx) {
678                    ex = AuthorizationException.fromTemplate(
679                            GeneralErrors.JSON_DESERIALIZATION_ERROR,
680                            jsonEx);
681                }
682                mCallback.onTokenRequestCompleted(null, ex);
683                return;
684            }
685
686            TokenResponse response;
687            try {
688                response = new TokenResponse.Builder(mRequest).fromResponseJson(json).build();
689            } catch (JSONException jsonEx) {
690                mCallback.onTokenRequestCompleted(null,
691                        AuthorizationException.fromTemplate(
692                                GeneralErrors.JSON_DESERIALIZATION_ERROR,
693                                jsonEx));
694                return;
695            }
696
697            if (response.idToken != null) {
698                IdToken idToken;
699                try {
700                    idToken = IdToken.from(response.idToken);
701                } catch (IdTokenException | JSONException ex) {
702                    mCallback.onTokenRequestCompleted(null,
703                            AuthorizationException.fromTemplate(
704                                    GeneralErrors.ID_TOKEN_PARSING_ERROR,
705                                    ex));
706                    return;
707                }
708
709                try {
710                    idToken.validate(
711                            mRequest,
712                            mClock,
713                            mSkipIssuerHttpsCheck
714                    );
715                } catch (AuthorizationException ex) {
716                    mCallback.onTokenRequestCompleted(null, ex);
717                    return;
718                }
719            }
720            Logger.debug("Token exchange with %s completed",
721                    mRequest.configuration.tokenEndpoint);
722            mCallback.onTokenRequestCompleted(response, null);
723        }
724
725        /**
726         * GitHub will only return a spec-compliant response if JSON is explicitly defined
727         * as an acceptable response type. As this is essentially harmless for all other
728         * spec-compliant IDPs, we add this header if no existing Accept header has been set
729         * by the connection builder.
730         */
731        private void addJsonToAcceptHeader(URLConnection conn) {
732            if (TextUtils.isEmpty(conn.getRequestProperty("Accept"))) {
733                conn.setRequestProperty("Accept", "application/json");
734            }
735        }
736    }
737
738    /**
739     * Callback interface for token endpoint requests.
740     * @see AuthorizationService#performTokenRequest
741     */
742    public interface TokenResponseCallback {
743        /**
744         * Invoked when the request completes successfully or fails.
745         *
746         * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure
747         * occurred during the request. This can happen if a bad URI was provided, no connection
748         * to the server could be established, or the response JSON was incomplete or incorrectly
749         * formatted.
750         *
751         * @param response the retrieved token response, if successful; `null` otherwise.
752         * @param ex a description of the failure, if one occurred: `null` otherwise.
753         *
754         * @see AuthorizationException.TokenRequestErrors
755         */
756        void onTokenRequestCompleted(@Nullable TokenResponse response,
757                @Nullable AuthorizationException ex);
758    }
759
760    private static class RegistrationRequestTask
761            extends AsyncTask<Void, Void, JSONObject> {
762        private RegistrationRequest mRequest;
763        private final ConnectionBuilder mConnectionBuilder;
764        private RegistrationResponseCallback mCallback;
765
766        private AuthorizationException mException;
767
768        RegistrationRequestTask(RegistrationRequest request,
769                ConnectionBuilder connectionBuilder,
770                RegistrationResponseCallback callback) {
771            mRequest = request;
772            mConnectionBuilder = connectionBuilder;
773            mCallback = callback;
774        }
775
776        @Override
777        protected JSONObject doInBackground(Void... voids) {
778            InputStream is = null;
779            String postData = mRequest.toJsonString();
780            try {
781                HttpURLConnection conn = mConnectionBuilder.openConnection(
782                        mRequest.configuration.registrationEndpoint);
783                conn.setRequestMethod("POST");
784                conn.setRequestProperty("Content-Type", "application/json");
785                conn.setDoOutput(true);
786                conn.setRequestProperty("Content-Length", String.valueOf(postData.length()));
787                OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
788                wr.write(postData);
789                wr.flush();
790
791                is = conn.getInputStream();
792                String response = Utils.readInputStream(is);
793                return new JSONObject(response);
794            } catch (IOException ex) {
795                Logger.debugWithStack(ex, "Failed to complete registration request");
796                mException = AuthorizationException.fromTemplate(
797                        GeneralErrors.NETWORK_ERROR, ex);
798            } catch (JSONException ex) {
799                Logger.debugWithStack(ex, "Failed to complete registration request");
800                mException = AuthorizationException.fromTemplate(
801                        GeneralErrors.JSON_DESERIALIZATION_ERROR, ex);
802            } finally {
803                Utils.closeQuietly(is);
804            }
805            return null;
806        }
807
808        @Override
809        protected void onPostExecute(JSONObject json) {
810            if (mException != null) {
811                mCallback.onRegistrationRequestCompleted(null, mException);
812                return;
813            }
814
815            if (json.has(AuthorizationException.PARAM_ERROR)) {
816                AuthorizationException ex;
817                try {
818                    String error = json.getString(AuthorizationException.PARAM_ERROR);
819                    ex = AuthorizationException.fromOAuthTemplate(
820                            RegistrationRequestErrors.byString(error),
821                            error,
822                            json.getString(AuthorizationException.PARAM_ERROR_DESCRIPTION),
823                            UriUtil.parseUriIfAvailable(
824                                    json.getString(AuthorizationException.PARAM_ERROR_URI)));
825                } catch (JSONException jsonEx) {
826                    ex = AuthorizationException.fromTemplate(
827                            GeneralErrors.JSON_DESERIALIZATION_ERROR,
828                            jsonEx);
829                }
830                mCallback.onRegistrationRequestCompleted(null, ex);
831                return;
832            }
833
834            RegistrationResponse response;
835            try {
836                response = new RegistrationResponse.Builder(mRequest)
837                        .fromResponseJson(json).build();
838            } catch (JSONException jsonEx) {
839                mCallback.onRegistrationRequestCompleted(null,
840                        AuthorizationException.fromTemplate(
841                                GeneralErrors.JSON_DESERIALIZATION_ERROR,
842                                jsonEx));
843                return;
844            } catch (RegistrationResponse.MissingArgumentException ex) {
845                Logger.errorWithStack(ex, "Malformed registration response");
846                mException = AuthorizationException.fromTemplate(
847                        GeneralErrors.INVALID_REGISTRATION_RESPONSE,
848                        ex);
849                return;
850            }
851            Logger.debug("Dynamic registration with %s completed",
852                    mRequest.configuration.registrationEndpoint);
853            mCallback.onRegistrationRequestCompleted(response, null);
854        }
855    }
856
857    /**
858     * Callback interface for token endpoint requests.
859     *
860     * @see AuthorizationService#performTokenRequest
861     */
862    public interface RegistrationResponseCallback {
863        /**
864         * Invoked when the request completes successfully or fails.
865         *
866         * Exactly one of `response` or `ex` will be non-null. If `response` is `null`, a failure
867         * occurred during the request. This can happen if an invalid URI was provided, no
868         * connection to the server could be established, or the response JSON was incomplete or
869         * incorrectly formatted.
870         *
871         * @param response the retrieved registration response, if successful; `null` otherwise.
872         * @param ex a description of the failure, if one occurred: `null` otherwise.
873         * @see AuthorizationException.RegistrationRequestErrors
874         */
875        void onRegistrationRequestCompleted(@Nullable RegistrationResponse response,
876                                            @Nullable AuthorizationException ex);
877    }
878}