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}