Как предоставить доступ к пользовательским данным Google

Аутентификация устанавливает, кем является пользователь, и обычно называется регистрацией или входом пользователя. Авторизация – это процесс предоставления или отклонения доступа к данным или ресурсам. Например, приложение запрашивает у пользователя разрешение на доступ к его Google Диску.

Вызовы аутентификации и авторизации должны быть двумя отдельными процессами, основанными на потребностях приложения.

Если в вашем приложении есть функции, которые могут использовать данные API Google, но не являются основными функциями вашего приложения, разработайте приложение так, чтобы оно могло корректно обрабатывать случаи, когда данные API недоступны. Например, вы можете скрыть список недавно сохраненных файлов, если пользователь не предоставил доступ к Диску.

Запрашивайте доступ к областям, необходимым для работы с API Google, только когда пользователь выполняет действие, требующее доступа к определенному API. Например, запрашивайте разрешение на доступ к Диску пользователя, когда он нажимает кнопку Сохранить на Диске.

Разделяя авторизацию и аутентификацию, вы можете избежать перегрузки новых пользователей или запутать пользователей в том, почему у них запрашивают определенные разрешения.

Для аутентификации рекомендуем использовать Credential Manager API. Для авторизации действий, которым требуется доступ к пользовательским данным, хранящимся в Google, рекомендуется использовать AuthorizationClient.

Как настроить проект в консоли Google Cloud

  1. Откройте проект в Cloud Console или создайте его, если у вас его ещё нет.
  2. На странице Брендинг убедитесь, что вся информация указана полностью и правильно.
    1. Убедитесь, что у приложения правильно указаны название, логотип и главная страница. Эти значения будут показываться пользователям в окне запроса доступа при входе с аккаунтом Google и на странице сторонних приложений и сервисов.
    2. Убедитесь, что вы указали URL политики конфиденциальности и условий использования приложения.
  3. На странице Клиенты создайте идентификатор клиента Android для своего приложения, если вы этого ещё не сделали. Вам нужно будет указать название пакета приложения и подпись SHA-1.
    1. Перейдите на страницу Клиенты.
    2. Нажмите Создать клиент.
    3. Выберите тип приложения Android.
    4. Введите название клиента OAuth. Это название будет показываться на странице клиентов проекта.
    5. Укажите название пакета приложения для Android. Это значение задается в атрибуте package элемента <manifest> в файле AndroidManifest.xml.
    6. Введите цифровой отпечаток сертификата подписи SHA-1 для распространения приложения.
    7. Если в вашем приложении используется функция подписания приложений в Google Play, скопируйте цифровой отпечаток SHA-1 со страницы подписания приложений в Play Console.
    8. Если вы управляете собственным хранилищем ключей и ключами подписи, используйте утилиту keytool, входящую в состав Java, чтобы вывести информацию о сертификате в человекочитаемом формате. Скопируйте значение SHA-1 в разделе Certificate fingerprints выходных данных keytool. Подробную информацию можно найти в разделе Аутентификация клиента документации по API Google для Android.
    9. Подтвердите право собственности на приложение для Android (необязательно).
  4. На странице Клиенты создайте идентификатор клиента для веб-приложения, если вы ещё этого не сделали. Поля "Разрешенные источники JavaScript" и "Разрешенные URI перенаправления" пока можно не заполнять. Этот идентификатор клиента будет использоваться для идентификации вашего бэкенд-сервера при его взаимодействии с сервисами аутентификации Google.
    1. Перейдите на страницу Клиенты.
    2. Нажмите Создать клиент.
    3. Выберите тип Веб-приложение.

Подтвердите право собственности на приложение

Вы можете подтвердить право собственности на приложение, чтобы снизить риск его имитации.

Чтобы пройти проверку, вы можете использовать аккаунт разработчика Google Play, если он у вас есть и ваше приложение зарегистрировано в Google Play Console. Для успешной проверки должны быть выполнены следующие требования:

  • У вас должно быть зарегистрированное приложение в Google Play Console с тем же названием пакета и цифровым отпечатком сертификата для подписи SHA-1, что и у клиента OAuth для Android, для которого вы проходите проверку.
  • У вас должны быть права администратора для приложения в Google Play Console. Подробнее об управлении доступом в Google Play Console…

В разделе Подтверждение права собственности на приложение клиента Android нажмите кнопку Подтвердить право собственности, чтобы завершить процесс проверки.

Если проверка пройдет успешно, вы увидите уведомление об этом. В противном случае появится сообщение об ошибке.

Чтобы устранить проблему, попробуйте сделать следующее:

  • Убедитесь, что приложение, которое вы проверяете, зарегистрировано в Google Play Console.
  • Убедитесь, что у вас есть разрешение Администратор для приложения в Google Play Console.

Объявление зависимостей

В файле build.gradle модуля объявите зависимости, используя последнюю версию библиотеки сервисов идентификации Google.

dependencies {
  // ... other dependencies

  implementation "com.google.android.gms:play-services-auth:22.0.0"
}

Запрос разрешений, необходимых для действий пользователя

Когда пользователь выполняет действие, требующее дополнительной области действия, вызовите AuthorizationClient.authorize(). Например, если пользователь выполняет действие, для которого требуется доступ к хранилищу приложения на Диске, сделайте следующее:

Kotlin

val requestedScopes: List<Scope> = listOf(DriveScopes.DRIVE_FILE)
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequestBuilder.build())
    .addOnSuccessListener { authorizationResult ->
        if (authorizationResult.hasResolution()) {
            val pendingIntent = authorizationResult.pendingIntent
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(IntentSenderRequest.Builder(pendingIntent!!.intentSender).build())
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        if (authorizationResult.hasResolution()) {
            // Access needs to be granted by the user
            startAuthorizationIntent.launch(
                new IntentSenderRequest.Builder(
                    authorizationResult.getPendingIntent().getIntentSender()
                ).build()
            );
        } else {
            // Access was previously granted, continue with user action
            saveToDriveAppFolder(authorizationResult);
        }
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));

При определении ActivityResultLauncher обработайте ответ, как показано в следующем фрагменте кода. Предполагается, что это делается во фрагменте. Код проверяет, что необходимые разрешения были успешно предоставлены, а затем выполняет действие пользователя.

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                // extract the result
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // continue with user action
                saveToDriveAppFolder(authorizationResult);
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
startAuthorizationIntent =
    registerForActivityResult(
        new ActivityResultContracts.StartIntentSenderForResult(),
        activityResult -> {
            try {
            // extract the result
            AuthorizationResult authorizationResult =
                Identity.getAuthorizationClient(requireActivity())
                    .getAuthorizationResultFromIntent(activityResult.getData());
            // continue with user action
            saveToDriveAppFolder(authorizationResult);
            } catch (ApiException e) {
            // log exception
            }
        });
}

Если вы обращаетесь к API Google на стороне сервера, вызовите метод getServerAuthCode() из AuthorizationResult, чтобы получить код авторизации, который вы отправляете на свой бэкенд для обмена на токен доступа и токен обновления. Подробнее о том, как сохранить доступ к данным пользователя…

Как отозвать разрешения на доступ к пользовательским данным или ресурсам

Чтобы отозвать ранее предоставленный доступ, вызовите AuthorizationClient.revokeAccess(). Например, если пользователь удаляет свой аккаунт из вашего приложения, которому ранее был предоставлен доступ к DriveScopes.DRIVE_FILE, используйте следующий код, чтобы отозвать доступ:

Kotlin

val requestedScopes: MutableList<Scope> = mutableListOf(DriveScopes.DRIVE_FILE)
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build()

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener { Log.i(TAG, "Successfully revoked access") }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to revoke access", e) }

Java

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_FILE);
RevokeAccessRequest revokeAccessRequest = RevokeAccessRequest.builder()
    .setAccount(account)
    .setScopes(requestedScopes)
    .build();

Identity.getAuthorizationClient(activity)
    .revokeAccess(revokeAccessRequest)
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully revoked access"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to revoke access", e));

Очистите кеш токенов

Токены доступа OAuth кешируются локально после получения с сервера, что ускоряет доступ и сокращает количество сетевых вызовов. Эти токены автоматически удаляются из кеша по истечении срока действия, но могут стать недействительными и по другим причинам. Если при использовании токена вы получаете ошибку IllegalStateException, очистите локальный кеш, чтобы следующий запрос авторизации для токена доступа был отправлен на сервер OAuth. Следующий фрагмент кода удаляет invalidAccessToken из локального кеша:

Kotlin

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener { Log.i(TAG, "Successfully removed the token from the cache") }
    .addOnFailureListener{ e -> Log.e(TAG, "Failed to clear token", e) }

Java

Identity.getAuthorizationClient(activity)
    .clearToken(ClearTokenRequest.builder().setToken(invalidAccessToken).build())
    .addOnSuccessListener(unused -> Log.i(TAG, "Successfully removed the token from the cache"))
    .addOnFailureListener(e -> Log.e(TAG, "Failed to clear the token cache", e));

Получение информации о пользователе во время авторизации

Ответ на запрос авторизации не содержит никакой информации об использованном аккаунте пользователя, а только токен для запрошенных областей действия. Например, ответ для получения токена доступа к Google Диску пользователя не раскрывает идентификационные данные аккаунта, выбранного пользователем, хотя его можно использовать для доступа к файлам на Диске пользователя. Чтобы получить информацию, например имя или адрес электронной почты пользователя, вы можете:

  • Прежде чем запрашивать авторизацию, выполните вход пользователя в аккаунт Google с помощью Credential Manager API. Ответ аутентификации от Менеджера учетных данных включает информацию о пользователе, например адрес электронной почты, и устанавливает выбранный аккаунт в качестве аккаунта по умолчанию для приложения. При необходимости вы можете отслеживать этот аккаунт в своем приложении. Последующий запрос авторизации использует аккаунт по умолчанию и пропускает шаг выбора аккаунта в процессе авторизации. Если вы хотите использовать для авторизации другой аккаунт, ознакомьтесь с разделом Авторизация из аккаунта, не используемого по умолчанию.

  • В запросе на авторизацию, помимо нужных вам областей действия (например, Drive scope), запросите области действия userinfo, profile и openid. После получения токена доступа запросите информацию о пользователе, отправив HTTP-запрос GET к конечной точке OAuth userinfo (https://www.googleapis.com/oauth2/v3/userinfo). Для этого используйте любую HTTP-библиотеку и добавьте в заголовок полученный токен доступа. Это эквивалентно следующей команде curl:

    curl -X GET \ "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" \ -H "Authorization: Bearer $TOKEN"
    

    В ответ вы получите объект UserInfo в формате JSON, ограниченный запрошенными областями действия.

Авторизация из аккаунта, не являющегося аккаунтом по умолчанию

Если вы используете Менеджер учетных данных для аутентификации и запускаете AuthorizationClient.authorize(), аккаунтом по умолчанию для вашего приложения будет выбранный пользователем. Это означает, что все последующие вызовы для авторизации будут использовать этот аккаунт по умолчанию. Чтобы принудительно показать окно выбора аккаунта, выполните выход пользователя из приложения с помощью API clearCredentialState() из Менеджера учетных данных.

Сохранять постоянный доступ к данным пользователя

Если вашему приложению нужен доступ к данным пользователя, вызовите метод AuthorizationClient.authorize() один раз. В последующих сеансах, пока пользователь не отзовет предоставленные разрешения, вызывайте тот же метод, чтобы получить токен доступа и выполнить необходимые действия без участия пользователя. Если же вам нужен доступ к данным пользователя в офлайн-режиме с бэкенд-сервера, запросите токен обновления.

Токены доступа намеренно создаются с коротким сроком действия – один час. Если токен доступа будет перехвачен или скомпрометирован, ограниченный срок его действия минимизирует потенциальный ущерб. После этого токен становится недействительным, и любые попытки его использования будут отклонены сервером ресурсов. Поскольку токены доступа действуют недолго, серверы используют токены обновления, чтобы поддерживать постоянный доступ к данным пользователя. Токены обновления – это токены с длительным сроком действия, которые используются клиентом для запроса у сервера авторизации токена доступа с коротким сроком действия, когда срок действия старого токена доступа истекает, без какого-либо взаимодействия с пользователем.

Чтобы получить токен обновления, сначала нужно получить код авторизации на этапе авторизации в приложении, запросив офлайн-доступ, а затем обменять код авторизации на токен обновления на сервере. Долгосрочные токены обновления необходимо хранить на сервере в безопасном месте, поскольку их можно использовать многократно для получения новых токенов доступа. Поэтому из соображений безопасности настоятельно не рекомендуется хранить токены обновления на устройстве. Их следует хранить на бэкенд-серверах приложения, где происходит обмен на токен доступа.

После того как код авторизации будет отправлен на бэкенд-сервер вашего приложения, вы можете обменять его на токен доступа с коротким сроком действия на сервере и токен обновления с длительным сроком действия, следуя инструкциям в руководстве по авторизации аккаунта. Этот обмен должен происходить только в серверной части вашего приложения.

Kotlin

// Ask for offline access during the first authorization request
val authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build()

Identity.getAuthorizationClient(activity)
    .authorize(authorizationRequest)
    .addOnSuccessListener { authorizationResult ->
        startAuthorizationIntent.launch(IntentSenderRequest.Builder(
            pendingIntent!!.intentSender
        ).build())
    }
    .addOnFailureListener { e -> Log.e(TAG, "Failed to authorize", e) }

Java

// Ask for offline access during the first authorization request
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .setRequestedScopes(requestedScopes)
    .requestOfflineAccess(serverClientId)
    .build();

Identity.getAuthorizationClient(getContext())
    .authorize(authorizationRequest)
    .addOnSuccessListener(authorizationResult -> {
        startAuthorizationIntent.launch(
            new IntentSenderRequest.Builder(
                authorizationResult.getPendingIntent().getIntentSender()
            ).build()
        );
    })
    .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize"));

В приведенном ниже фрагменте кода предполагается, что авторизация запускается из фрагмента.

Kotlin

private lateinit var startAuthorizationIntent: ActivityResultLauncher<IntentSenderRequest>

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?,
): View? {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { activityResult ->
            try {
                val authorizationResult = Identity.getAuthorizationClient(requireContext())
                    .getAuthorizationResultFromIntent(activityResult.data)
                // short-lived access token
                accessToken = authorizationResult.accessToken
                // store the authorization code used for getting a refresh token safely to your app's backend server
                val authCode: String = authorizationResult.serverAuthCode
                storeAuthCodeSafely(authCode)
            } catch (e: ApiException) {
                // log exception
            }
        }
}

Java

private ActivityResultLauncher<IntentSenderRequest> startAuthorizationIntent;

@Override
public View onCreateView(
    @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // ...
    startAuthorizationIntent =
        registerForActivityResult(
            new ActivityResultContracts.StartIntentSenderForResult(),
            activityResult -> {
                try {
                    AuthorizationResult authorizationResult =
                        Identity.getAuthorizationClient(requireActivity())
                            .getAuthorizationResultFromIntent(activityResult.getData());
                    // short-lived access token
                    accessToken = authorizationResult.getAccessToken();
                    // store the authorization code used for getting a refresh token safely to your app's backend server
                    String authCode = authorizationResult.getServerAuthCode()
                    storeAuthCodeSafely(authCode);
                } catch (ApiException e) {
                    // log exception
                }
            });
}