001/*
002 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the
010 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
011 * express or implied. See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package net.openid.appauth;
016
017import android.app.Activity;
018import android.app.PendingIntent;
019import android.app.PendingIntent.CanceledException;
020import android.content.ActivityNotFoundException;
021import android.content.Context;
022import android.content.Intent;
023import android.net.Uri;
024import android.os.Bundle;
025import androidx.annotation.VisibleForTesting;
026import androidx.appcompat.app.AppCompatActivity;
027
028import net.openid.appauth.AuthorizationException.AuthorizationRequestErrors;
029import net.openid.appauth.internal.Logger;
030import org.json.JSONException;
031
032/**
033 * Stores state and handles events related to the authorization management flow. The activity is
034 * started by {@link AuthorizationService#performAuthorizationRequest} or
035 * {@link AuthorizationService#performEndSessionRequest}, and records all state pertinent to
036 * the authorization management request before invoking the authorization intent. It also functions
037 * to control the back stack, ensuring that the authorization activity will not be reachable
038 * via the back button after the flow completes.
039 *
040 * The following diagram illustrates the operation of the activity:
041 *
042 * ```
043 *                          Back Stack Towards Top
044 *                +------------------------------------------>
045 *
046 * +------------+            +---------------+      +----------------+      +--------------+
047 * |            |     (1)    |               | (2)  |                | (S1) |              |
048 * | Initiating +----------->| Authorization +----->| Authorization  +----->| Redirect URI |
049 * |  Activity  |            |  Management   |      |   Activity     |      |   Receiver   |
050 * |            |<-----------+   Activity    |<-----+ (e.g. browser) |      |   Activity   |
051 * |            | (C2b, S3b) |               | (C1) |                |      |              |
052 * +------------+            +-+---+---------+      +----------------+      +-------+------+
053 *                           |  |  ^                                              |
054 *                           |  |  |                                              |
055 *                   +-------+  |  |                      (S2)                    |
056 *                   |          |  +----------------------------------------------+
057 *                   |          |
058 *                   |          v (S3a)
059 *             (C2a) |      +------------+
060 *                   |      |            |
061 *                   |      | Completion |
062 *                   |      |  Activity  |
063 *                   |      |            |
064 *                   |      +------------+
065 *                   |
066 *                   |      +-------------+
067 *                   |      |             |
068 *                   +----->| Cancelation |
069 *                          |  Activity   |
070 *                          |             |
071 *                          +-------------+
072 * ```
073 *
074 * The process begins with an activity requesting that an authorization flow be started,
075 * using {@link AuthorizationService#performAuthorizationRequest} or
076 * {@link AuthorizationService#performEndSessionRequest}.
077 *
078 * - Step 1: Using an intent derived from {@link #createStartIntent}, this activity is
079 *   started. The state delivered in this intent is recorded for future use.
080 *
081 * - Step 2: The authorization intent, typically a browser tab, is started. At this point,
082 *   depending on user action, we will either end up in a "completion" flow (S) or
083 *   "cancelation flow" (C).
084 *
085 * - Cancelation (C) flow:
086 *     - Step C1: If the user presses the back button or otherwise causes the authorization
087 *       activity to finish, the AuthorizationManagementActivity will be recreated or restarted.
088 *
089 *     - Step C2a: If a cancellation PendingIntent was provided in the call to
090 *       {@link AuthorizationService#performAuthorizationRequest} or
091 *       {@link AuthorizationService#performEndSessionRequest}, then this is
092 *       used to invoke a cancelation activity.
093 *
094 *     - Step C2b: If no cancellation PendingIntent was provided (legacy behavior, or
095 *       AuthorizationManagementActivity was started with an intent from
096 *       {@link AuthorizationService#getAuthorizationRequestIntent} or
097 *       @link AuthorizationService#performEndOfSessionRequest}), then the
098 *       AuthorizationManagementActivity simply finishes after calling {@link Activity#setResult},
099 *       with {@link Activity#RESULT_CANCELED}, returning control to the activity above
100 *       it in the back stack (typically, the initiating activity).
101 *
102 * - Completion (S) flow:
103 *     - Step S1: The authorization activity completes with a success or failure, and sends this
104 *       result to {@link RedirectUriReceiverActivity}.
105 *
106 *     - Step S2: {@link RedirectUriReceiverActivity} extracts the forwarded data, and invokes
107 *       AuthorizationManagementActivity using an intent derived from
108 *       {@link #createResponseHandlingIntent}. This intent has flag CLEAR_TOP set, which will
109 *       result in both the authorization activity and {@link RedirectUriReceiverActivity} being
110 *       destroyed, if necessary, such that AuthorizationManagementActivity is once again at the
111 *       top of the back stack.
112 *
113 *     - Step S3a: If this activity was invoked via
114 *       {@link AuthorizationService#performAuthorizationRequest} or
115 *       {@link AuthorizationService#performEndSessionRequest}, then the pending intent provided
116 *       for completion of the authorization flow is invoked, providing the decoded
117 *       {@link AuthorizationManagementResponse} or {@link AuthorizationException} as appropriate.
118 *       The AuthorizationManagementActivity finishes, removing itself from the back stack.
119 *
120 *     - Step S3b: If this activity was invoked via an intent returned by
121 *       {@link AuthorizationService#getAuthorizationRequestIntent}, then this activity
122 *       calls {@link Activity#setResult(int, Intent)} with {@link Activity#RESULT_OK}
123 *       and a data intent containing the {@link AuthorizationResponse} or
124 *       {@link AuthorizationException} as appropriate.
125 *       The AuthorizationManagementActivity finishes, removing itself from the back stack.
126 */
127public class AuthorizationManagementActivity extends AppCompatActivity {
128
129    @VisibleForTesting
130    static final String KEY_AUTH_INTENT = "authIntent";
131
132    @VisibleForTesting
133    static final String KEY_AUTH_REQUEST = "authRequest";
134
135    @VisibleForTesting
136    static final String KEY_AUTH_REQUEST_TYPE = "authRequestType";
137
138    @VisibleForTesting
139    static final String KEY_COMPLETE_INTENT = "completeIntent";
140
141    @VisibleForTesting
142    static final String KEY_CANCEL_INTENT = "cancelIntent";
143
144    @VisibleForTesting
145    static final String KEY_AUTHORIZATION_STARTED = "authStarted";
146
147    private boolean mAuthorizationStarted = false;
148    private Intent mAuthIntent;
149    private AuthorizationManagementRequest mAuthRequest;
150    private PendingIntent mCompleteIntent;
151    private PendingIntent mCancelIntent;
152
153    /**
154     * Creates an intent to start an authorization flow.
155     * @param context the package context for the app.
156     * @param request the authorization request which is to be sent.
157     * @param authIntent the intent to be used to get authorization from the user.
158     * @param completeIntent the intent to be sent when the flow completes.
159     * @param cancelIntent the intent to be sent when the flow is canceled.
160     */
161    public static Intent createStartIntent(
162            Context context,
163            AuthorizationManagementRequest request,
164            Intent authIntent,
165            PendingIntent completeIntent,
166            PendingIntent cancelIntent) {
167        Intent intent = createBaseIntent(context);
168        intent.putExtra(KEY_AUTH_INTENT, authIntent);
169        intent.putExtra(KEY_AUTH_REQUEST, request.jsonSerializeString());
170        intent.putExtra(KEY_AUTH_REQUEST_TYPE, AuthorizationManagementUtil.requestTypeFor(request));
171        intent.putExtra(KEY_COMPLETE_INTENT, completeIntent);
172        intent.putExtra(KEY_CANCEL_INTENT, cancelIntent);
173        return intent;
174    }
175
176    /**
177     * Creates an intent to start an authorization flow.
178     * @param context the package context for the app.
179     * @param request the authorization management request which is to be sent.
180     * @param authIntent the intent to be used to get authorization from the user.
181     */
182    public static Intent createStartForResultIntent(
183            Context context,
184            AuthorizationManagementRequest request,
185            Intent authIntent) {
186        return createStartIntent(context, request, authIntent, null, null);
187    }
188
189    /**
190     * Creates an intent to handle the completion of an authorization flow. This restores
191     * the original AuthorizationManagementActivity that was created at the start of the flow.
192     * @param context the package context for the app.
193     * @param responseUri the response URI, which carries the parameters describing the response.
194     */
195    public static Intent createResponseHandlingIntent(Context context, Uri responseUri) {
196        Intent intent = createBaseIntent(context);
197        intent.setData(responseUri);
198        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
199        return intent;
200    }
201
202    private static Intent createBaseIntent(Context context) {
203        return new Intent(context, AuthorizationManagementActivity.class);
204    }
205
206    @Override
207    protected void onCreate(Bundle savedInstanceState) {
208        super.onCreate(savedInstanceState);
209        if (savedInstanceState == null) {
210            extractState(getIntent().getExtras());
211        } else {
212            extractState(savedInstanceState);
213        }
214    }
215
216    @Override
217    protected void onResume() {
218        super.onResume();
219
220        /*
221         * If this is the first run of the activity, start the authorization intent.
222         * Note that we do not finish the activity at this point, in order to remain on the back
223         * stack underneath the authorization activity.
224         */
225
226        if (!mAuthorizationStarted) {
227            try {
228                startActivity(mAuthIntent);
229                mAuthorizationStarted = true;
230            } catch (ActivityNotFoundException e) {
231                handleBrowserNotFound();
232                finish();
233            }
234            return;
235        }
236
237        /*
238         * On a subsequent run, it must be determined whether we have returned to this activity
239         * due to an OAuth2 redirect, or the user canceling the authorization flow. This can
240         * be done by checking whether a response URI is available, which would be provided by
241         * RedirectUriReceiverActivity. If it is not, we have returned here due to the user
242         * pressing the back button, or the authorization activity finishing without
243         * RedirectUriReceiverActivity having been invoked - this can occur when the user presses
244         * the back button, or closes the browser tab.
245         */
246
247        if (getIntent().getData() != null) {
248            handleAuthorizationComplete();
249        } else {
250            handleAuthorizationCanceled();
251        }
252        finish();
253    }
254
255    @Override
256    protected void onNewIntent(Intent intent) {
257        super.onNewIntent(intent);
258        setIntent(intent);
259    }
260
261    @Override
262    protected void onSaveInstanceState(Bundle outState) {
263        super.onSaveInstanceState(outState);
264        outState.putBoolean(KEY_AUTHORIZATION_STARTED, mAuthorizationStarted);
265        outState.putParcelable(KEY_AUTH_INTENT, mAuthIntent);
266        outState.putString(KEY_AUTH_REQUEST, mAuthRequest.jsonSerializeString());
267        outState.putString(KEY_AUTH_REQUEST_TYPE,
268                AuthorizationManagementUtil.requestTypeFor(mAuthRequest));
269        outState.putParcelable(KEY_COMPLETE_INTENT, mCompleteIntent);
270        outState.putParcelable(KEY_CANCEL_INTENT, mCancelIntent);
271    }
272
273    private void handleAuthorizationComplete() {
274        Uri responseUri = getIntent().getData();
275        Intent responseData = extractResponseData(responseUri);
276        if (responseData == null) {
277            Logger.error("Failed to extract OAuth2 response from redirect");
278            return;
279        }
280        responseData.setData(responseUri);
281
282        sendResult(mCompleteIntent, responseData, RESULT_OK);
283    }
284
285    private void handleAuthorizationCanceled() {
286        Logger.debug("Authorization flow canceled by user");
287        Intent cancelData = AuthorizationException.fromTemplate(
288                AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW,
289                null)
290                .toIntent();
291
292        sendResult(mCancelIntent, cancelData, RESULT_CANCELED);
293    }
294
295    private void handleBrowserNotFound() {
296        Logger.debug("Authorization flow canceled due to missing browser");
297        Intent cancelData = AuthorizationException.fromTemplate(
298                AuthorizationException.GeneralErrors.PROGRAM_CANCELED_AUTH_FLOW,
299                null)
300                .toIntent();
301
302        sendResult(mCancelIntent, cancelData, RESULT_CANCELED);
303    }
304
305    private void extractState(Bundle state) {
306        if (state == null) {
307            Logger.warn("No stored state - unable to handle response");
308            finish();
309            return;
310        }
311
312        mAuthIntent = state.getParcelable(KEY_AUTH_INTENT);
313        mAuthorizationStarted = state.getBoolean(KEY_AUTHORIZATION_STARTED, false);
314        mCompleteIntent = state.getParcelable(KEY_COMPLETE_INTENT);
315        mCancelIntent = state.getParcelable(KEY_CANCEL_INTENT);
316        try {
317            String authRequestJson = state.getString(KEY_AUTH_REQUEST, null);
318            String authRequestType = state.getString(KEY_AUTH_REQUEST_TYPE, null);
319            mAuthRequest = authRequestJson != null
320                    ? AuthorizationManagementUtil.requestFrom(authRequestJson, authRequestType)
321                    : null;
322        } catch (JSONException ex) {
323            sendResult(
324                    mCancelIntent,
325                    AuthorizationRequestErrors.INVALID_REQUEST.toIntent(),
326                    RESULT_CANCELED);
327        }
328    }
329
330    private void sendResult(PendingIntent callback, Intent cancelData, int resultCode) {
331        if (callback != null) {
332            try {
333                callback.send(this, 0, cancelData);
334            } catch (CanceledException e) {
335                Logger.error("Failed to send cancel intent", e);
336            }
337        } else {
338            setResult(resultCode, cancelData);
339        }
340    }
341
342    private Intent extractResponseData(Uri responseUri) {
343        if (responseUri.getQueryParameterNames().contains(AuthorizationException.PARAM_ERROR)) {
344            return AuthorizationException.fromOAuthRedirect(responseUri).toIntent();
345        } else {
346            AuthorizationManagementResponse response =
347                    AuthorizationManagementUtil.responseWith(mAuthRequest, responseUri);
348
349            if (mAuthRequest.getState() == null && response.getState() != null
350                    || (mAuthRequest.getState() != null && !mAuthRequest.getState()
351                    .equals(response.getState()))) {
352
353                Logger.warn("State returned in authorization response (%s) does not match state "
354                        + "from request (%s) - discarding response",
355                        response.getState(),
356                        mAuthRequest.getState());
357
358                return AuthorizationRequestErrors.STATE_MISMATCH.toIntent();
359            }
360
361            return response.toIntent();
362        }
363    }
364}