blob: 0806f1db2bb7357ddb81bec503c5706547879ed0 [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* 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 android.credentials;
import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ORIGIN;
import static java.util.Objects.requireNonNull;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentSender;
import android.os.CancellationSignal;
import android.os.ICancellationSignal;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.provider.DeviceConfig;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Manages user authentication flows.
*
* <p>Note that an application should call the Jetpack CredentialManager apis instead of directly
* calling these framework apis.
*
* <p>The CredentialManager apis launch framework UI flows for a user to register a new credential
* or to consent to a saved credential from supported credential providers, which can then be used
* to authenticate to the app.
*/
@SystemService(Context.CREDENTIAL_SERVICE)
public final class CredentialManager {
private static final String TAG = "CredentialManager";
/** @hide */
@IntDef(
flag = true,
prefix = {"PROVIDER_FILTER_"},
value = {
PROVIDER_FILTER_ALL_PROVIDERS,
PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY,
PROVIDER_FILTER_USER_PROVIDERS_ONLY,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProviderFilter {}
/**
* Returns both system and user credential providers.
*
* @hide
*/
@TestApi public static final int PROVIDER_FILTER_ALL_PROVIDERS = 0;
/**
* Returns system credential providers only.
*
* @hide
*/
@TestApi public static final int PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY = 1;
/**
* Returns user credential providers only.
*
* @hide
*/
@TestApi public static final int PROVIDER_FILTER_USER_PROVIDERS_ONLY = 2;
private final Context mContext;
private final ICredentialManager mService;
/**
* Flag to enable and disable Credential Manager.
*
* @hide
*/
public static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER =
"enable_credential_manager";
/**
* Flag to enable and disable Credential Description api.
*
* @hide
*/
private static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API =
"enable_credential_description_api";
/**
* @hide instantiated by ContextImpl.
*/
public CredentialManager(Context context, ICredentialManager service) {
mContext = context;
mService = service;
}
/**
* Launches the necessary flows to retrieve an app credential from the user.
*
* <p>The execution can potentially launch UI flows to collect user consent to using a
* credential, display a picker when multiple credentials exist, etc.
* Callers (e.g. browsers) may optionally set origin in {@link GetCredentialRequest} for an
* app different from their own, to be able to get credentials on behalf of that app. They would
* need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
* to use this functionality
*
* @param request the request specifying type(s) of credentials to get from the user
* @param activity the activity used to launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this {@link Executor}
* @param callback the callback invoked when the request succeeds or fails
*/
public void getCredential(
@NonNull GetCredentialRequest request,
@NonNull Activity activity,
@Nullable CancellationSignal cancellationSignal,
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
requireNonNull(request, "request must not be null");
requireNonNull(activity, "activity must not be null");
requireNonNull(executor, "executor must not be null");
requireNonNull(callback, "callback must not be null");
if (cancellationSignal != null && cancellationSignal.isCanceled()) {
Log.w(TAG, "getCredential already canceled");
return;
}
ICancellationSignal cancelRemote = null;
try {
cancelRemote =
mService.executeGetCredential(
request,
new GetCredentialTransport(activity, executor, callback),
mContext.getOpPackageName());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
if (cancellationSignal != null && cancelRemote != null) {
cancellationSignal.setRemote(cancelRemote);
}
}
/**
* Launches the necessary flows to register an app credential for the user.
*
* <p>The execution can potentially launch UI flows to collect user consent to creating or
* storing the new credential, etc.
* Callers (e.g. browsers) may optionally set origin in {@link CreateCredentialRequest} for an
* app different from their own, to be able to get credentials on behalf of that app. They would
* need additional permission {@link CREDENTIAL_MANAGER_SET_ORIGIN}
* to use this functionality
*
* @param request the request specifying type(s) of credentials to get from the user
* @param activity the activity used to launch any UI needed
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this {@link Executor}
* @param callback the callback invoked when the request succeeds or fails
*/
public void createCredential(
@NonNull CreateCredentialRequest request,
@NonNull Activity activity,
@Nullable CancellationSignal cancellationSignal,
@CallbackExecutor @NonNull Executor executor,
@NonNull
OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) {
requireNonNull(request, "request must not be null");
requireNonNull(activity, "activity must not be null");
requireNonNull(executor, "executor must not be null");
requireNonNull(callback, "callback must not be null");
if (cancellationSignal != null && cancellationSignal.isCanceled()) {
Log.w(TAG, "createCredential already canceled");
return;
}
ICancellationSignal cancelRemote = null;
try {
cancelRemote =
mService.executeCreateCredential(
request,
new CreateCredentialTransport(activity, executor, callback),
mContext.getOpPackageName());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
if (cancellationSignal != null && cancelRemote != null) {
cancellationSignal.setRemote(cancelRemote);
}
}
/**
* Clears the current user credential state from all credential providers.
*
* <p>You should invoked this api after your user signs out of your app to notify all credential
* providers that any stored credential session for the given app should be cleared.
*
* <p>A credential provider may have stored an active credential session and use it to limit
* sign-in options for future get-credential calls. For example, it may prioritize the active
* credential over any other available credential. When your user explicitly signs out of your
* app and in order to get the holistic sign-in options the next time, you should call this API
* to let the provider clear any stored credential session.
*
* @param request the request data
* @param cancellationSignal an optional signal that allows for cancelling this call
* @param executor the callback will take place on this {@link Executor}
* @param callback the callback invoked when the request succeeds or fails
*/
public void clearCredentialState(
@NonNull ClearCredentialStateRequest request,
@Nullable CancellationSignal cancellationSignal,
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, ClearCredentialStateException> callback) {
requireNonNull(request, "request must not be null");
requireNonNull(executor, "executor must not be null");
requireNonNull(callback, "callback must not be null");
if (cancellationSignal != null && cancellationSignal.isCanceled()) {
Log.w(TAG, "clearCredentialState already canceled");
return;
}
ICancellationSignal cancelRemote = null;
try {
cancelRemote =
mService.clearCredentialState(
request,
new ClearCredentialStateTransport(executor, callback),
mContext.getOpPackageName());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
if (cancellationSignal != null && cancelRemote != null) {
cancellationSignal.setRemote(cancelRemote);
}
}
/**
* Sets a list of all user configurable credential providers registered on the system. This API
* is intended for settings apps.
*
* @param providers the list of enabled providers
* @param userId the user ID to configure credential manager for
* @param executor the callback will take place on this {@link Executor}
* @param callback the callback invoked when the request succeeds or fails
* @hide
*/
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
public void setEnabledProviders(
@NonNull List<String> providers,
int userId,
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, SetEnabledProvidersException> callback) {
requireNonNull(executor, "executor must not be null");
requireNonNull(callback, "callback must not be null");
requireNonNull(providers, "providers must not be null");
try {
mService.setEnabledProviders(
providers, userId, new SetEnabledProvidersTransport(executor, callback));
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Returns {@code true} if the calling application provides a CredentialProviderService that is
* enabled for the current user, or {@code false} otherwise. CredentialProviderServices are
* enabled on a per-service basis so the individual component name of the service should be
* passed in here.
*
* @param componentName the component name to check is enabled
*/
public boolean isEnabledCredentialProviderService(@NonNull ComponentName componentName) {
requireNonNull(componentName, "componentName must not be null");
try {
return mService.isEnabledCredentialProviderService(
componentName, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of CredentialProviderInfo for all discovered credential providers on this
* device but will include test system providers as well.
*
* @hide
*/
@NonNull
@TestApi
@RequiresPermission(
anyOf = {
android.Manifest.permission.QUERY_ALL_PACKAGES,
android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS
})
public List<CredentialProviderInfo> getCredentialProviderServicesForTesting(
@ProviderFilter int providerFilter) {
try {
return mService.getCredentialProviderServicesForTesting(providerFilter);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the list of CredentialProviderInfo for all discovered credential providers on this
* device.
*
* @hide
*/
@NonNull
@RequiresPermission(
anyOf = {
android.Manifest.permission.QUERY_ALL_PACKAGES,
android.Manifest.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS
})
public List<CredentialProviderInfo> getCredentialProviderServices(
int userId, @ProviderFilter int providerFilter) {
try {
return mService.getCredentialProviderServices(userId, providerFilter);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns whether the service is enabled.
*
* @hide
*/
@TestApi
public static boolean isServiceEnabled(@NonNull Context context) {
requireNonNull(context, "context must not be null");
if (context == null) {
return false;
}
CredentialManager credentialManager =
(CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
if (credentialManager != null) {
return credentialManager.isServiceEnabled();
}
return false;
}
private boolean isServiceEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER, true);
}
/**
* Returns whether the credential description api is enabled.
*
* @hide
*/
public static boolean isCredentialDescriptionApiEnabled(Context context) {
if (context == null) {
return false;
}
CredentialManager credentialManager =
(CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
if (credentialManager != null) {
return credentialManager.isCredentialDescriptionApiEnabled();
}
return false;
}
private boolean isCredentialDescriptionApiEnabled() {
return DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API, false);
}
/**
* Registers a {@link CredentialDescription} for an actively provisioned {@link Credential} a
* CredentialProvider has. This registry will then be used to determine where to fetch the
* requested {@link Credential} from. Not all credential types will be supported. The
* distinction will be made by the JetPack layer. For the types that are supported, JetPack will
* add a new key-value pair into {@link GetCredentialRequest}. These will not be persistent on
* the device. The Credential Providers will need to call this API again upon device reboot.
*
* @param request the request data
* @throws {@link UnsupportedOperationException} if the feature has not been enabled.
* @throws {@link com.android.server.credentials.NonCredentialProviderCallerException} if the
* calling package name is not also listed as a Credential Provider.
* @throws {@link IllegalArgumentException} if the calling Credential Provider can not handle
* one or more of the Credential Types that are sent for registration.
*/
public void registerCredentialDescription(
@NonNull RegisterCredentialDescriptionRequest request) {
requireNonNull(request, "request must not be null");
try {
mService.registerCredentialDescription(request, mContext.getOpPackageName());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Unregisters a {@link CredentialDescription} for an actively provisioned {@link Credential}
* that has been registered previously.
*
* @param request the request data
* @throws {@link UnsupportedOperationException} if the feature has not been enabled.
*/
public void unregisterCredentialDescription(
@NonNull UnregisterCredentialDescriptionRequest request) {
requireNonNull(request, "request must not be null");
try {
mService.unregisterCredentialDescription(request, mContext.getOpPackageName());
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
private static class GetCredentialTransport extends IGetCredentialCallback.Stub {
// TODO: listen for cancellation to release callback.
private final Activity mActivity;
private final Executor mExecutor;
private final OutcomeReceiver<GetCredentialResponse, GetCredentialException> mCallback;
private GetCredentialTransport(
Activity activity,
Executor executor,
OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
mActivity = activity;
mExecutor = executor;
mCallback = callback;
}
@Override
public void onPendingIntent(PendingIntent pendingIntent) {
try {
mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(
TAG,
"startIntentSender() failed for intent:" + pendingIntent.getIntentSender(),
e);
// TODO: propagate the error.
}
}
@Override
public void onResponse(GetCredentialResponse response) {
mExecutor.execute(() -> mCallback.onResult(response));
}
@Override
public void onError(String errorType, String message) {
mExecutor.execute(
() -> mCallback.onError(new GetCredentialException(errorType, message)));
}
}
private static class CreateCredentialTransport extends ICreateCredentialCallback.Stub {
// TODO: listen for cancellation to release callback.
private final Activity mActivity;
private final Executor mExecutor;
private final OutcomeReceiver<CreateCredentialResponse, CreateCredentialException>
mCallback;
private CreateCredentialTransport(
Activity activity,
Executor executor,
OutcomeReceiver<CreateCredentialResponse, CreateCredentialException> callback) {
mActivity = activity;
mExecutor = executor;
mCallback = callback;
}
@Override
public void onPendingIntent(PendingIntent pendingIntent) {
try {
mActivity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
} catch (IntentSender.SendIntentException e) {
Log.e(
TAG,
"startIntentSender() failed for intent:" + pendingIntent.getIntentSender(),
e);
// TODO: propagate the error.
}
}
@Override
public void onResponse(CreateCredentialResponse response) {
mExecutor.execute(() -> mCallback.onResult(response));
}
@Override
public void onError(String errorType, String message) {
mExecutor.execute(
() -> mCallback.onError(new CreateCredentialException(errorType, message)));
}
}
private static class ClearCredentialStateTransport extends IClearCredentialStateCallback.Stub {
// TODO: listen for cancellation to release callback.
private final Executor mExecutor;
private final OutcomeReceiver<Void, ClearCredentialStateException> mCallback;
private ClearCredentialStateTransport(
Executor executor, OutcomeReceiver<Void, ClearCredentialStateException> callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onSuccess() {
mCallback.onResult(null);
}
@Override
public void onError(String errorType, String message) {
mExecutor.execute(
() -> mCallback.onError(new ClearCredentialStateException(errorType, message)));
}
}
private static class SetEnabledProvidersTransport extends ISetEnabledProvidersCallback.Stub {
// TODO: listen for cancellation to release callback.
private final Executor mExecutor;
private final OutcomeReceiver<Void, SetEnabledProvidersException> mCallback;
private SetEnabledProvidersTransport(
Executor executor, OutcomeReceiver<Void, SetEnabledProvidersException> callback) {
mExecutor = executor;
mCallback = callback;
}
public void onResponse(Void result) {
mExecutor.execute(() -> mCallback.onResult(result));
}
@Override
public void onResponse() {
mExecutor.execute(() -> mCallback.onResult(null));
}
@Override
public void onError(String errorType, String message) {
mExecutor.execute(
() -> mCallback.onError(new SetEnabledProvidersException(errorType, message)));
}
}
}