/*
* Copyright 2013-2017 Amazon.com, Inc. or its affiliates.
* 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 com.amazonaws.mobileconnectors.cognitoauth;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Handler;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import android.text.TextUtils;
import android.util.Log;
import com.amazonaws.cognito.clientcontext.data.UserContextDataProvider;
import com.amazonaws.mobileconnectors.cognitoauth.activities.CustomTabsManagerActivity;
import com.amazonaws.mobileconnectors.cognitoauth.exceptions.AuthClientException;
import com.amazonaws.mobileconnectors.cognitoauth.exceptions.AuthInvalidGrantException;
import com.amazonaws.mobileconnectors.cognitoauth.exceptions.AuthNavigationException;
import com.amazonaws.mobileconnectors.cognitoauth.exceptions.AuthServiceException;
import com.amazonaws.mobileconnectors.cognitoauth.exceptions.BrowserNotInstalledException;
import com.amazonaws.mobileconnectors.cognitoauth.util.AuthHttpResponseParser;
import com.amazonaws.mobileconnectors.cognitoauth.handlers.AuthHandler;
import com.amazonaws.mobileconnectors.cognitoauth.util.ClientConstants;
import com.amazonaws.mobileconnectors.cognitoauth.util.AuthHttpClient;
import com.amazonaws.mobileconnectors.cognitoauth.util.Pkce;
import com.amazonaws.mobileconnectors.cognitoauth.util.LocalDataManager;
import java.net.URL;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION;
/**
* Local client for {@link Auth}.
*
* Encapsulates user level operations, tokens {@link AuthUserSession}, handles
* token caching, and token refresh.
* Manages Cognito-Web user screens.
*
*/
@SuppressWarnings("checkstyle:javadocmethod")
public class AuthClient {
/**
* A random code the custom tabs activity is launched under.
* This is needed by clients to listen for the result.
*/
public static final int CUSTOM_TABS_ACTIVITY_CODE = 49281;
/**
* Namespace for logging client activities
*/
private static final String TAG = AuthClient.class.getSimpleName();
/**
* Name of redirect activity in charge of handling auth responses.
*/
private static final String REDIRECT_ACTIVITY_NAME = "HostedUIRedirectActivity";
/**
* Default timeout duration for auth redirects.
*/
private static final long REDIRECT_TIMEOUT_SECONDS = 10;
/**
* Message returned for a bad request.
*/
private static final String BAD_REQUEST_ERROR = "invalid_request";
/**
* Android application context.
*/
private final Context context;
/**
* Reference to the parent pool.
*/
private final Auth pool;
/**
* Username used to instantiate this class.
*/
private String userId;
/**
* Generated proof-key for PKCE.
*/
private String proofKey;
/**
* SHA256 hash of the generated proof-key.
*/
private String proofKeyHash;
/**
* Session state - stores the unique string generated for to set state query parameter.
*/
private String state;
/**
* Callback handler.
*/
private AuthHandler userHandler;
/**
* Remembers whether redirect activity was found in manifest or not.
*/
private boolean isRedirectActivityDeclared;
/**
* Cache whether browser is installed on the device.
*/
private boolean isBrowserInstalled;
/**
* Cache whether there is browser that supports custom tabs on the device.
*/
private boolean isCustomTabSupported;
/**
* Cache the packageName of the custom-tabs-service that should be used.
*/
private String customTabsPackageName;
// - Chrome Custom Tabs Controls
private CustomTabsClient mCustomTabsClient;
private CustomTabsSession mCustomTabsSession;
private CustomTabsIntent mCustomTabsIntent;
private CustomTabsServiceConnection mCustomTabsServiceConnection;
private CountDownLatch cookiesCleared;
/**
* Constructs {@link AuthClient} with no user name.
* @param context Required: The android application {@link Context}.
* @param pool Required: A reference to the parent, {@link Auth}.
*/
protected AuthClient(final Context context, final Auth pool) {
this(context, pool, null);
}
/**
* Constructs an instance of the Cognito User with username.
* @param context Required: The android application {@link Context}.
* @param pool Required: A reference to the parent, {@link Auth}.
* @param username Required: The username of the user in the Cognito User-Pool.
*/
protected AuthClient(final Context context, final Auth pool, final String username) {
this.context = context;
this.pool = pool;
this.userId = username;
this.isRedirectActivityDeclared = false;
this.isBrowserInstalled = false;
this.isCustomTabSupported = false;
if (isCustomTabSupported()) {
preWarmCustomTabs();
}
}
/**
* Set callback handler for {@link AuthClient}.
* @param handler Required: {@link AuthHandler}.
*/
protected void setUserHandler(final AuthHandler handler) {
if (handler == null) {
throw new InvalidParameterException("Callback handler cannot be null");
}
userHandler = handler;
}
/**
* Sets username.
* @param username Required: Username as a {@link String}.
*/
protected void setUsername(final String username) {
this.userId = username;
}
/**
* Launches user authentication screen and returns a redirect Uri through an {@link Intent}.
*
* Checks for cached, valid tokens and launches the Cognito Web UI if no valid tokens are
* found. This method uses PKCE for authentication. This SDK, therefore, uses code-grant flow
* to authenticate user. The proof-key and a state is generated and its hash is used in added
* as query parameters to create the authentication FQDN.
* The state value set this method is used to temporarily cache the proof-key on the device.
* To exchange the code for tokens, the {@link Auth#getTokens(Uri)} method will use the
* state in the redirect uri to fetch the stored proof-key.
*
* @param showSignInIfExpired true if the web UI should launch when the session is expired
* @param activity The activity to launch the sign in experience from.
* This must not be null when showSignInIfExpired is true.
*/
protected void getSession(final boolean showSignInIfExpired, final Activity activity) {
getSession(showSignInIfExpired, activity, null);
}
/**
* Launches user authentication screen and returns a redirect Uri through an {@link Intent}.
*
* Checks for cached, valid tokens and launches the Cognito Web UI if no valid tokens are
* found. This method uses PKCE for authentication. This SDK, therefore, uses code-grant flow
* to authenticate user. The proof-key and a state is generated and its hash is used in added
* as query parameters to create the authentication FQDN.
* The state value set this method is used to temporarily cache the proof-key on the device.
* To exchange the code for tokens, the {@link Auth#getTokens(Uri)} method will use the
* state in the redirect uri to fetch the stored proof-key.
*
* @param showSignInIfExpired true if the web UI should launch when the session is expired
* @param activity The activity to launch the sign in experience from.
* This must not be null when showSignInIfExpired is true.
* @param browserPackage String specifying the browser package to launch the specified url.
*/
protected void getSession(final boolean showSignInIfExpired, final Activity activity, final String browserPackage) {
try {
proofKey = Pkce.generateRandom();
proofKeyHash = Pkce.generateHash(proofKey);
state = Pkce.generateRandom();
} catch (Exception e) {
userHandler.onFailure(e);
}
// Look for cached tokens
AuthUserSession session =
LocalDataManager.getCachedSession(pool.awsKeyValueStore, context, pool.getAppId(), userId, pool.getScopes());
// Check if the session is valid and returns tokens
if (session.isValidForThreshold()) {
userHandler.onSuccess(session);
return;
}
// Try refreshing the tokens
if (session.getRefreshToken() != null && session.getRefreshToken().getToken() != null) {
refreshSession(
session,
pool.getSignInRedirectUri(),
pool.getScopes(),
userHandler,
showSignInIfExpired,
browserPackage,
activity);
} else if (showSignInIfExpired) {
launchCognitoAuth(pool.getSignInRedirectUri(), pool.getScopes(), activity, browserPackage);
} else {
userHandler.onFailure(new Exception("No cached session"));
}
}
/**
* @return Current Username.
*/
protected String getUsername() {
return userId;
}
/**
* Signs-out a user.
*
* Clears cached tokens for the user. Launches the sign-out Cognito web end-point to
* clear all Cognito Auth cookies stored by Chrome.
*
*/
public void signOut() {
signOut(null);
}
/**
* Signs-out a user.
*
* Clears cached tokens for the user. Launches the sign-out Cognito web end-point to
* clear all Cognito Auth cookies stored by Chrome.
*
*
* @param browserPackage String specifying the browser package to launch the specified url.
*/
public void signOut(String browserPackage) {
signOut(false, browserPackage);
}
/**
* Signs-out a user.
*
* Clears cached tokens for the user. Launches the sign-out Cognito web end-point to
* clear all Cognito Auth cookies stored by Chrome.
*
*
* @param clearLocalTokensOnly true if signs out the user from the client,
* but the session may still be alive from the browser.
*/
public void signOut(final boolean clearLocalTokensOnly) {
signOut(clearLocalTokensOnly, null);
}
/**
* Signs-out a user.
*
* Launches the sign-out Cognito web end-point to clear all Cognito Auth cookies stored
* by Chrome. Cached tokens will be deleted if sign-out redirect is completed.
*
*
* @param clearLocalTokensOnly true if signs out the user from the client,
* but the session may still be alive from the browser.
* @param browserPackage String specifying the browser package to launch the specified url.
*/
public void signOut(final boolean clearLocalTokensOnly, final String browserPackage) {
if (!clearLocalTokensOnly) {
endSession(browserPackage);
}
// Delete local cache
LocalDataManager.clearCache(pool.awsKeyValueStore, context, pool.getAppId(), userId);
userId = null;
}
/**
* Ends current browser session.
* @param browserPackage browser package to launch sign-out endpoint from.
* @throws AuthClientException if sign-out redirect fails to resolve.
*/
private void endSession(final String browserPackage) throws AuthClientException {
boolean redirectReceived;
try {
cookiesCleared = new CountDownLatch(1);
launchSignOut(pool.getSignOutRedirectUri(), browserPackage);
if (!isRedirectActivityDeclared()) {
cookiesCleared.countDown();
}
redirectReceived = cookiesCleared.await(REDIRECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new AuthNavigationException("User cancelled sign-out.");
}
if (!redirectReceived) {
throw new AuthServiceException("Timed out while waiting for sign-out redirect response.");
}
}
/**
* @return {@code true} if valid tokens are available for the user.
*/
@SuppressWarnings("checkstyle:hiddenfield")
public boolean isAuthenticated() {
AuthUserSession session =
LocalDataManager.getCachedSession(pool.awsKeyValueStore, context, pool.getAppId(), userId, pool.getScopes());
return session.isValidForThreshold();
}
/**
* Exchanges code in the Uri for with Cognito JWT.
* Checks if the Uri passed to this method is valid. We can avoid a function
* @param uri Required: The redirect {@link Uri}.
*/
public void getTokens(final Uri uri) {
if (uri == null) {
return;
}
getTokens(uri, userHandler);
}
/**
* Properly handles the event where the user cancels out of the custom tabs auth flow either by closing it
* or navigating back away from it.
*/
public void handleCustomTabsCancelled() {
userHandler.onFailure(new AuthNavigationException("user cancelled"));
}
/**
* Unbind {@link AuthClient#mCustomTabsServiceConnection}
*/
public void unbindServiceConnection() {
if(mCustomTabsServiceConnection != null)
context.unbindService(mCustomTabsServiceConnection);
}
/**
* Internal method to exchange code for tokens.
*
* Checks if the Uri contains a state query parameter. The FQDN for Cognito UI
* Web-Page contains a state. This method considers Uri's without a state parameter as
* redirect.
* Checks if the value of the contained state variable is valid. This is necessary to ensure
* that the SDK is parsing response from a known source. The SDK reads cache for proof-key
* stored with the value of the state in the Uri. If a stored proof-key is found, the Uri
* contains response from a request it generated.
* Checks if the Uri contains an error query parameter. An error query parameter indicates
* that the last request failed. This method invokes
* {@link AuthHandler#onFailure(Exception)} callback to report failure.
* When the above tests succeed, this method makes an http call to Amazon Cognito token
* end-point to exchange code for tokens.
*
* @param uri Required: The redirect uri from the service.
* @param callback Required: {@link AuthHandler}.
*/
private void getTokens(final Uri uri, final AuthHandler callback) {
new Thread(new Runnable() {
final Handler handler = new Handler(context.getMainLooper());
Runnable returnCallback = new Runnable() {
@Override
public void run() {
callback.onFailure(new InvalidParameterException());
}
};
@Override
public void run() {
final Uri fqdn = new Uri.Builder()
.scheme(ClientConstants.DOMAIN_SCHEME)
.authority(pool.getAppWebDomain())
.appendPath(ClientConstants.DOMAIN_PATH_OAUTH2)
.appendPath(ClientConstants.DOMAIN_PATH_TOKEN_ENDPOINT)
.build();
String callbackState =
uri.getQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_STATE);
if (callbackState != null) {
Set tokenScopes = LocalDataManager.getCachedScopes(pool.awsKeyValueStore, context, callbackState);
String proofKeyPlain = LocalDataManager.getCachedProofKey(pool.awsKeyValueStore, context, callbackState);
if (proofKeyPlain == null) {
// The state value is unknown, exit.
return;
}
final String errorText =
uri.getQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_ERROR);
if (errorText != null) {
final String errorDescription = uri.getQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_ERROR_DESCRIPTION);
final String errorMessage;
if (errorText.equals(BAD_REQUEST_ERROR) && errorDescription != null) {
errorMessage = errorDescription.trim();
} else {
errorMessage = errorText;
}
returnCallback = new Runnable() {
@Override
public void run() {
callback.onFailure(new AuthServiceException(errorMessage));
}
};
} else {
// Make http POST call
final AuthHttpClient httpClient = new AuthHttpClient();
Map httpHeaderParams = getHttpHeader();
Map httpBodyParams = generateTokenExchangeRequest(uri, proofKeyPlain);
try {
String response =
httpClient.httpPost(new URL(fqdn.toString()), httpHeaderParams, httpBodyParams);
final AuthUserSession session = AuthHttpResponseParser.parseHttpResponse(response);
userId = session.getUsername();
// Cache tokens if successful
LocalDataManager.cacheSession(pool.awsKeyValueStore, context, pool.getAppId(), userId, session, tokenScopes);
// Return tokens
returnCallback = new Runnable() {
@Override
public void run() {
callback.onSuccess(session);
}
};
} catch (final Exception e) {
returnCallback = new Runnable() {
@Override
public void run() {
callback.onFailure(e);
}
};
}
}
} else {
if (cookiesCleared != null) {
cookiesCleared.countDown();
Log.d(TAG, "Sign-out was successful.");
}
// User sign-out.
returnCallback = new Runnable() {
@Override
public void run() {
callback.onSignout();
}
};
}
handler.post(returnCallback);
}
}).start();
}
/**
* Internal method to refresh tokens.
*
* Makes an http call to Amazon Cognito token end-point to refresh token. On successful
* token refresh, the refresh tokens is retained.
*
* @param session Required: The android application {@link Context}.
* @param redirectUri Required: The redirect Uri, which will be launched after authentication.
* @param tokenScopes Required: A {@link Set} specifying all scopes for the tokens.
* @param callback Required: {@link AuthHandler}.
* @param showSignInIfExpired true if the web UI should launch when the refresh token is expired
* @param browserPackage String specifying the browser package to launch the specified url.
* @param activity The activity to launch the sign in experience from.
* This must not be null if showSignInIfExpired is true.
*/
private void refreshSession(final AuthUserSession session,
final String redirectUri,
final Set tokenScopes,
final AuthHandler callback,
final boolean showSignInIfExpired,
final String browserPackage,
final Activity activity) {
new Thread(new Runnable() {
final Handler handler = new Handler(context.getMainLooper());
Runnable returnCallback;
@Override
public void run() {
final Uri fqdn = new Uri.Builder()
.scheme(ClientConstants.DOMAIN_SCHEME)
.authority(pool.getAppWebDomain())
.appendPath(ClientConstants.DOMAIN_PATH_OAUTH2)
.appendPath(ClientConstants.DOMAIN_PATH_TOKEN_ENDPOINT)
.build();
// Make http POST call
final AuthHttpClient httpClient = new AuthHttpClient();
Map httpHeaderParams = getHttpHeader();
Map httpBodyParams = generateTokenRefreshRequest(redirectUri, session);
try {
String response =
httpClient.httpPost(new URL(fqdn.toString()), httpHeaderParams, httpBodyParams);
AuthUserSession parsedSession = AuthHttpResponseParser.parseHttpResponse(response);
final AuthUserSession refreshedSession = new AuthUserSession(
parsedSession.getIdToken(),
parsedSession.getAccessToken(),
session.getRefreshToken());
final String username = refreshedSession.getUsername();
// Cache session
LocalDataManager.cacheSession(pool.awsKeyValueStore, context, pool.getAppId(), username, refreshedSession, pool.getScopes());
// Return tokens
returnCallback = new Runnable() {
@Override
public void run() {
callback.onSuccess(refreshedSession);
}
};
} catch (final AuthInvalidGrantException invg) {
if (showSignInIfExpired) {
returnCallback = new Runnable() {
@Override
public void run() {
launchCognitoAuth(redirectUri, tokenScopes, activity, browserPackage);
}
};
} else {
returnCallback = new Runnable() {
@Override
public void run() {
userHandler.onFailure(invg);
}
};
}
} catch (final Exception e) {
returnCallback = new Runnable() {
@Override
public void run() {
callback.onFailure(e);
}
};
}
handler.post(returnCallback);
}
}).start();
}
/**
* Generates header for the http request.
* @return Header parameters as a {@link Map}.
*/
private Map getHttpHeader() {
Map httpHeaderParams = new HashMap();
httpHeaderParams.put(ClientConstants.HTTP_HEADER_PROP_CONTENT_TYPE,
ClientConstants.HTTP_HEADER_PROP_CONTENT_TYPE_DEFAULT);
// Add authorization header if the App Id has an associated Secret
if (pool.getAppSecret() != null) {
StringBuilder builder = new StringBuilder();
builder.append(pool.getAppId()).append(":").append(pool.getAppSecret());
httpHeaderParams.put(ClientConstants.HTTP_HEADER_TYPE_AUTHORIZE, "Basic "
+ Pkce.encodeBase64(builder.toString()));
}
return httpHeaderParams;
}
/**
* Generates http body for token exchange.
* @param redirectUri Required: redirect_uri for token exchange.
* @param proofKey Required: The proof key for tokens.
* @return Http request as a {@link Map}.
*/
private Map generateTokenExchangeRequest(final Uri redirectUri,
final String proofKey) {
Map httpBodyParams = new HashMap();
httpBodyParams.put(ClientConstants.TOKEN_GRANT_TYPE,
ClientConstants.TOKEN_GRANT_TYPE_AUTH_CODE);
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_CLIENT_ID, pool.getAppId());
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_REDIRECT_URI, pool.getSignInRedirectUri());
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_CODE_VERIFIER, proofKey);
httpBodyParams.put(ClientConstants.TOKEN_AUTH_TYPE_CODE,
redirectUri.getQueryParameter(ClientConstants.TOKEN_AUTH_TYPE_CODE));
return httpBodyParams;
}
/**
* Generates http body for token refresh.
* @param redirectUri Required: redirect_uri for token refresh.
* @param session Required: User session containing the refresh token.
* @return Http request as a {@link Map}.
*/
private Map generateTokenRefreshRequest(final String redirectUri,
final AuthUserSession session) {
Map httpBodyParams = new HashMap();
httpBodyParams.put(ClientConstants.TOKEN_GRANT_TYPE, ClientConstants.HTTP_REQUEST_REFRESH_TOKEN);
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_REDIRECT_URI, redirectUri);
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_CLIENT_ID, pool.getAppId());
httpBodyParams.put(ClientConstants.HTTP_REQUEST_REFRESH_TOKEN, session.getRefreshToken().getToken());
final String userContextData = getUserContextData();
if (userContextData != null) {
httpBodyParams.put(ClientConstants.DOMAIN_QUERY_PARAM_USERCONTEXTDATA, userContextData);
}
return httpBodyParams;
}
/**
* Creates the FQDM for Cognito's authentication endpoint and launches Cognito Auth web-domain.
* @param redirectUri Required: The redirect Uri, which will be launched after authentication.
* @param tokenScopes Required: A {@link Set} specifying all scopes for the tokens.
* @param activity The activity to launch the sign in experience from.
* This must not be null if showSignInIfExpired is true.
* @param browserPackage String specifying the browser package to launch the specified url.
*/
private void launchCognitoAuth(
final String redirectUri,
final Set tokenScopes,
final Activity activity,
final String browserPackage) {
// Build the complete web domain to launch the login screen
Uri.Builder builder = new Uri.Builder()
.scheme(ClientConstants.DOMAIN_SCHEME)
.authority(pool.getAppWebDomain())
.appendPath(ClientConstants.DOMAIN_PATH_OAUTH2)
.appendPath(ClientConstants.DOMAIN_PATH_SIGN_IN)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_CLIENT_ID, pool.getAppId())
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_REDIRECT_URI, redirectUri)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_RESPONSE_TYPE,
ClientConstants.AUTH_RESPONSE_TYPE_CODE)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_CODE_CHALLENGE, proofKeyHash)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_CODE_CHALLENGE_METHOD,
ClientConstants.DOMAIN_QUERY_PARAM_CODE_CHALLENGE_METHOD_SHA256)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_STATE, state)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_USERCONTEXTDATA, getUserContextData());;
//check if identity provider set as param.
if (!TextUtils.isEmpty(pool.getIdentityProvider())) {
builder.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_IDENTITY_PROVIDER, pool.getIdentityProvider());
}
//check if idp identifier set as param.
if (!TextUtils.isEmpty(pool.getIdpIdentifier())) {
builder.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_IDP_IDENTIFIER, pool.getIdpIdentifier());
}
// Convert scopes into a string of comma separated values.
final int noOfScopes = tokenScopes.size();
if (noOfScopes > 0) {
StringBuilder strBuilder = new StringBuilder();
int index = 0;
for (String scope: tokenScopes) {
strBuilder.append(scope);
if (index++ < noOfScopes - 1) {
strBuilder.append(" ");
}
}
final String scopesStr = strBuilder.toString();
builder.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_SCOPES, scopesStr);
}
final Uri fqdn = builder.build();
LocalDataManager.cacheState(pool.awsKeyValueStore, context, state, proofKey, tokenScopes);
launchCustomTabs(fqdn, activity, browserPackage);
}
/**
* Creates the FQDM for Cognito's sign-out endpoint and launches Cognito Auth Web-Domain to
* sign-out.
* @param redirectUri Required: The redirect Uri, which will be launched after authentication.
* @param browserPackage String specifying the browser package to launch the specified url.
*/
private void launchSignOut(final String redirectUri, final String browserPackage) {
Uri.Builder builder = new Uri.Builder()
.scheme(ClientConstants.DOMAIN_SCHEME)
.authority(pool.getAppWebDomain()).appendPath(ClientConstants.DOMAIN_PATH_SIGN_OUT)
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_CLIENT_ID, pool.getAppId())
.appendQueryParameter(ClientConstants.DOMAIN_QUERY_PARAM_LOGOUT_URI, redirectUri);
final Uri fqdn = builder.build();
launchCustomTabs(fqdn, null, browserPackage);
}
/***
* Check if a browser is installed on the device to launch HostedUI.
* @return true if a browser exists else false.
*/
private boolean isBrowserInstalled() {
if (isBrowserInstalled) {
return true;
}
String url = "https://docs.amplify.aws/";
Uri webAddress = Uri.parse(url);
Intent intentWeb = new Intent(Intent.ACTION_VIEW, webAddress);
if (intentWeb.resolveActivity(context.getPackageManager()) != null) {
isBrowserInstalled = true;
return true;
}
return false;
}
/**
* Get list of packages that support Custom Tabs Service.
* @return list of package names that support Custom Tabs.
*/
private List getSupportedCustomTabsPackages() {
PackageManager packageManager = context.getPackageManager();
Intent serviceIntent = new Intent()
.setAction(ACTION_CUSTOM_TABS_CONNECTION);
// Get all services that can handle ACTION_CUSTOM_TABS_CONNECTION intents.
List resolvedServicesList = packageManager.queryIntentServices(serviceIntent, 0);
List packageNamesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedServicesList) {
packageNamesSupportingCustomTabs.add(info.serviceInfo.packageName);
}
return packageNamesSupportingCustomTabs;
}
/***
* Check if there are any browsers on the device that support custom tabs.
* @return true if custom tabs is supported by any browsers on the device else false.
*/
private boolean isCustomTabSupported() {
if (isCustomTabSupported) {
return true;
}
List supportedCustomTabsPackages = getSupportedCustomTabsPackages();
if (supportedCustomTabsPackages.size() > 0) {
isCustomTabSupported = true;
// get the preferred Custom Tabs package
customTabsPackageName = CustomTabsClient.getPackageName(
context,
supportedCustomTabsPackages
);
return true;
}
return false;
}
/**
* Launches the HostedUI webpage on a Custom Tab.
* @param uri Required: {@link Uri}.
* @param activity Activity to launch custom tabs from and which will listen for the intent completion.
* @param browserPackage Optional string specifying the browser package to launch the specified url.
* Launches intent chooser if set to null.
*/
private void launchCustomTabs(final Uri uri, final Activity activity, final String browserPackage) {
try {
if(!isBrowserInstalled()) {
userHandler.onFailure(new BrowserNotInstalledException("No browsers installed."));
return;
}
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(mCustomTabsSession);
mCustomTabsIntent = builder.build();
if(pool.getCustomTabExtras() != null) {
mCustomTabsIntent.intent.putExtras(pool.getCustomTabExtras());
}
if(browserPackage != null) {
mCustomTabsIntent.intent.setPackage(browserPackage);
} else if (customTabsPackageName != null) {
mCustomTabsIntent.intent.setPackage(customTabsPackageName);
}
mCustomTabsIntent.intent.setData(uri);
if (activity != null) {
activity.startActivityForResult(
CustomTabsManagerActivity.createStartIntent(context, mCustomTabsIntent.intent),
CUSTOM_TABS_ACTIVITY_CODE
);
} else {
Intent startIntent = CustomTabsManagerActivity.createStartIntent(context, mCustomTabsIntent.intent);
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startIntent);
}
} catch (final Exception e) {
userHandler.onFailure(e);
}
}
private String getUserContextData() {
String userContextData = null;
if (pool.isAdvancedSecurityDataCollectionEnabled()) {
UserContextDataProvider dataProvider = UserContextDataProvider.getInstance();
userContextData = dataProvider.getEncodedContextData(this.context, userId, pool.getUserPoolId(),
pool.getAppId());
}
return userContextData;
}
/**
* Connects to Custom Tabs Service on the device.
*/
private void preWarmCustomTabs() {
if (customTabsPackageName == null) {
return;
}
mCustomTabsServiceConnection = new CustomTabsServiceConnection() {
@Override
public void onCustomTabsServiceConnected(final ComponentName name, final CustomTabsClient client) {
mCustomTabsClient = client;
mCustomTabsClient.warmup(0L);
mCustomTabsSession = mCustomTabsClient.newSession(null);
}
@Override
public void onServiceDisconnected(final ComponentName name) {
mCustomTabsClient = null;
}
};
CustomTabsClient.bindCustomTabsService(
context,
customTabsPackageName,
mCustomTabsServiceConnection
);
}
// Inspects context to determine whether HostedUIRedirectActivity is declared in
// customer's AndroidManifest.xml.
private boolean isRedirectActivityDeclared() {
// If the activity was found at least once, then don't bother searching again.
if (isRedirectActivityDeclared) {
return true;
}
if (context == null) {
Log.w(TAG, "Context is null. Failed to inspect packages.");
return false;
}
try {
List packages = context.getPackageManager()
.getInstalledPackages(PackageManager.GET_ACTIVITIES);
for (PackageInfo packageInfo : packages) {
if (packageInfo.activities == null) {
continue;
}
for (ActivityInfo activityInfo : packageInfo.activities) {
if (activityInfo.name.contains(REDIRECT_ACTIVITY_NAME)) {
isRedirectActivityDeclared = true;
return true;
}
}
}
Log.w(TAG, REDIRECT_ACTIVITY_NAME + " is not declared in AndroidManifest.");
} catch (Exception error) {
Log.w(TAG, "Failed to inspect packages.");
}
return false;
}
}