Как стать автором
Обновить

Авторизация OAuth 2.0 в Google Api для Android без специальных библиотек

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров8.5K

На тему OAuth 2.0 написано море хороших статей (например: 1,2), переписывать их не буду, а лучше расскажу про изобретение велосипеда то, как я пытался на практике реализовать авторизацию в Google Api посредством простых запросов.

Про существование библиотек Sign-In, AppAuth, AccountManager я в курсе, но чего они все не дают, так это четкого понимания как происходит обмен ключами и что они из себя представляют. Было принято решение получить токены от Google Books без применения специализированных библиотек для того,чтобы разобраться как все устроено, возможно кому-то пригодится. Сразу скажу, что не являюсь экспертом, и крутым разработчикам будет не интересно изобретение велосипедов,но возможно кому-то из начинающих разработчиков пригодится данная информация.

В теоретические выкладки и перерисовывание схем из других постов вдаваться не буду, оставлю ссылки на основные источники:

  1. OAuth 2.0 for Mobile & Desktop Apps - инструкция от Google

  2. The OAuth 2.0 Authorization Framework - текст стандарта RFC6749 - здесь описан сам протокол OAuth

  3. Proof Key for Code Exchange by OAuth Public Clients текст стандарта RFC7636 - PKCE стандарт для ключей (токенов)

Если кратко, то алгоритм авторизации выглядит следующим образом:

  1. Настроить проект в консоли, добыть Client ID

  2. Сформировать строку code_verifier

  3. Сформировать строку code_challenge

  4. Собрать ссылку для авторизации и открыть по ней экран авторизации

  5. Получить code

  6. Обменять code на токены: access_token и update_token

  7. Решить вопрос с хранением токенов.


Получение Client ID

Подготовительная часть (подробная инстукция от Google здесь):

  1. Перейти в Google Developer Console и создать проект.

  2. Перейти в OAuth consent screen - создать экран авторизации:
    Для обычного приложения нужно выбрать тип External.

  3. После заполнения формы необходимо добавить Scopes необходимые вам области доступа - можно выбрать отсюда, в моей случае scope для доступа к книгам пользователя это строка, которая выглядит как ссылка "https://www.googleapis.com/auth/books".

  4. Далее нужно добавить тестовых пользователей - реальные аккаунты, с которых вы будете тестировать приложение. Пока статус приложения "Testing" - будут допускаться только пользователи из списка тестовых.

  5. Переходим в Credentials - необходимо создать OAuth client ID -> тип приложения Android -> Здесь понадобится ввести имя пакета - package name, оно должно совпадать с настройками в Gradle applicationId:

 defaultConfig {
        applicationId "ru.example.googlebooksclient"

Также потребуется ввести SHA-1 certificate fingerprint (подробнее про App Signing). Для отладки приложения можно взять debug key - добыть его можно в терминале (стандартный пароль для получения ключа - android), нужно скопировать SHA1 и закинуть в форму. Типичный путь для Android Studio:

keytool -list -v -keystore ~/.android/debug.keystore
Certificate fingerprints:
	 SHA1: D9:E9:59:FA:7A:46:72:4E:60:1F:96:28:8C:F9:AE:82:3A:5D:2F:03

После того как форма заполнена, будет виден заветный Client ID. Остается зайти в API LIbrary, выбрать нужный API и активировать.

Ну и, конечно, добавить в манифест разрешение на использование интернета.На этом подготовка окончена.

 <uses-permission android:name="android.permission.INTERNET" />

Коды verifier и challenge

Теперь, когда есть Client ID, можно переходить к самим запросам.

code_verifier-криптографически-устойчивая случайная строка длиной от 43 до 128 символов. Читаем стандарт - рекомендуется генерировать строку длиной 32 байта и пропустить ее через base64url-encode, что на выходе даст 43 символа. Тут я наступил на грабли и пытался использовать обычный base64-encode, который на пару символов отличается, но этого достаточно чтобы ничего не работало. Генерируем code_verifier:

 val  charPool : List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
      val secureRandom = SecureRandom()
      val str= (1..STRING_LENGTH).map {
      secureRandom.nextInt(charPool.size).let { charPool[it] }
      }.joinToString("")
     codeVerifier= Base64.getUrlEncoder().withoutPadding()
                    .encodeToString(str.toByteArray())

Должна получиться строка длинной 43 символов вида:

U0tTVUFNWВJZQUJpV3ZNOGszZ2FscFgxT0xUcVo3a2M

code_challenge - читаем стандарт, данная строка получается из прошлой с помощью BASE64URL-ENCODE(SHA256(ASCII(code_verifier))), сказано -сделано:

 val md=c.getInstance("SHA-256")
  codeChallenge=  Base64.getUrlEncoder().withoutPadding()
                  .encodeToString(md.digest(codeVerifier.toByteArray()))

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


Экран авторизации

Согласно шагу 2 инструкции собираем ссылку для перехода на экран авторизации:

https://accounts.google.com/o/oauth2/v2/auth

Необходимо добавить параметры:

  • client_id - наш client id из консонли

  • redirect_uri - ссылка для обратного перехода в приложение после авторизации, причем в документации сказано, что она должна точно совпадать с ссылкой которую мы указали в настройках проекта, тут не совсем ясно, мы же ничего не указывали, кроме имени пакета, ок - ссылкой будет applicationId -имя пакета

  • response_type - для android приложений должно быть "code"

  • scope - выбранный ранее Scope

  • code_challenge - строка code_challenge

  • code_challenge_method - для SHA256 должно быть "S256"

Собираем ссылку:

 https://accounts.google.com/o/oauth2/v2/auth
  ?client_id=189284811864-i5gehoqcvqesloriuccem7iu2p3hlare.apps.googleusercontent.com
  &redirect_uri=ru.example.googlebooksclient:/
  &scope=https://www.googleapis.com/auth/books
  &code_challenge=VR2MxXBNS71bE6RudDxCQ3M3-cc72B7ph_S1jOmZu48
  &code_challenge_method=S256
  &response_type=code

И тут меня ожидали грабли: redirect_uri считается некорректной, пока не добавим к applicationId в конце ":/", но это мелочи, теперь нужно перейти по ссылке:

// добавляем зависимость в Gradle
implementation 'androidx.browser:browser:1.5.0'

//открыть ссылку
val authUrl="$authlink?client_id=$MY_CLIENT_ID&redirect_uri=$MY_REDIRECT_URI:/" +
                "&scope=$BOOKS_SCOPE&code_challenge=$codeChallenge&code_challenge_method=S256&response_type=code"
        val builder = CustomTabsIntent.Builder( )
        builder.setShowTitle(true)
        builder.setInstantAppsEnabled(true)
        val customBuilder = builder.build()
        customBuilder.launchUrl(curContext, Uri.parse(authUrl))

И прилетает ошибка disallowed_useragent - так я узнал, что использование WebView запрещено в целях безопасности.

Web developers may encounter this error when an Android app opens a general web link in an embedded user-agent and a user navigates to Google's OAuth 2.0 authorization endpoint from your site. Developers should allow general links to open in the default link handler of the operating system, which includes both Android App Links handlers or the default browser app. The Android Custom Tabs library is also a supported option.

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


Обмен кода авторизации на токены

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

<intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="ru.example.googlebooksclient"/>
</intent-filter>

Для того чтобы Activity не перезапускалась каждый раз заново в манифест(в activity) нужно добавить:

 android:launchMode="singleTask"

В таком случае, при обращении к уже существующей Activity, она не будет создаваться заново, а будет вызван метод onNewIntent(intent: Intent).Поле intent.data будет содержать непосредственно ответ. Ответ может содержать code в случае успеха, или error если, например, пользователь отказался войти в аккаунт.

code=4%2F0AbUR1VPxw9shy7AwPM7XTnrK_1Ejble9dwd7oe3GexdLho_1D-jhT5RSets8z-q1e6RaoA

Теперь когда есть code - осталось обменять его на заветные токены. Для этого необходимо отправить POST запрос на

https://oauth2.googleapis.com/token

с параметрами:

client_id - наш client_id
code - полученный код
code_verifier - строка code_verifier
grant_type=authorization_code
redirect_uri =наш redirect_uri

В ответ на POST запрос прилетит JSON:

{
"access_token": "ya25.a0AWY6Ckkl292RjoOMfhEtX8Ub_UPM-P3WHQSD1v5QSU_zfr5ig4KB_yMjXmmeIQXg8gRt4hmooGe2OyRFtLOxU2UHYvj40v9JS4kmhGhE2S3lIN5NxcWnOLJOqX9OhpZTzKVi9kofm9LwzFAXKY7B49wKMpiraCgYKARwSARISFQG1tDrpIMvYW4ruiJUUDUvQjlEapw0163",
 "expires_in": 3599,
 "refresh_token": "1//0c1f5N617z5JyCgYIARAAGAwSNwF-L9IrQnmw_Xx670pofbNnUtH5alJTHYoheRisu35RH_PWJzgIaQW6PdlRrxdzmJTEPnw9eCo",
 "scope": "https://www.googleapis.com/auth/books",
 "token_type": "Bearer"
 }

Остается решить вопрос только с хранением и обновлением токенов, хранить токены локально - в любом случае небезопасно, можно придумать какое-то шифрование и хранить их SharedPreferences. Время действия токена в секундах указано в expires_in.


Обновление токена

Для обновления токена, необходимо отправить POST запрос на:

https://oauth2.googleapis.com/token

с параметрами: client_id, refresh_token, grant_type=refresh_token

В ответ прилетит GSON с новым access_token и временем действия.

Отмена авторизации

Для отмены авторизации достаточно отправить POST запрос для отмены действия токена:

https://oauth2.googleapis.com/revoke?token={здесь токен}

Итог

В итоге получилось приложение с одной Activity, которое с помощью Custom Tabs открывает экран авторизации в Google, получает токены доступа. При этом никак не привязано к аккаунтам в телефоне, которых может и не быть. Очевидно что вариант черновой, но дает понимание алгоритма действий и в случае необходимости, данный алгоритм можно допилить под нестандартные API или применить знания при допиливании "костылей" к существующим библиотекам.

Ссылка на Github - приложение закидывает все запросы и ответы в Logcat.

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии2

Публикации