Статьи

Подключение вашего защищенного веб-приложения OAuth2 с Android

В своем последнем посте я показал, как отправлять пользовательские уведомления для  устройств Android Wear на часы Android Wear.

Чтобы продемонстрировать это лучше, я использовал события из нашего  приложения Teamgeist  в качестве примера и как могут выглядеть уведомления.

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

Как Google OAuth2 Flow работает для нашего веб-приложения

Наше приложение Teamgeist использует OAuth2 и Google для аутентификации пользователя и получения некоторой информации о профиле пользователя или адресе электронной почты. Поток OAuth2 может быть довольно сложным в первый раз. Поток между нашим клиентом JavaScript и серверной частью можно упростить, выполнив следующие шаги:

1. Первый запрос из нашего приложения JavaScript на сервер будет отправлен без токена в заголовке HTTP. Поэтому мы перенаправляем пользователя на страницу входа, где мы показываем кнопку входа в Google+:

Web1

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

Web3 Consent

Как видите, наше приложение хочет получить доступ к информации профиля пользователя. Если пользователь предоставит разрешение и примет этот экран, Google сгенерирует код авторизации. Во время настройки OAuth2 Google запросил URL обратного вызова. Сгенерированный код авторизации будет отправлен на этот URL.

В обмен на код авторизации сервер получает токен доступа от Google. С помощью этого токена сервер может получать информацию о профиле пользователя за определенное время. Кроме того, сервер получает токен обновления, который длится дольше и может использоваться для получения нового токена доступа.

Токены доступа и обновления должны храниться на сервере. Эти токены никогда не должны передаваться клиентскому приложению (ни Android, ни JavaScript). На стороне клиента мы храним токен носителя приложения. Мы связываем этот токен с пользователем и передаем его клиенту. Это единственный токен, который клиент должен сообщить нашему серверу.

Подключите Android к существующему потоку

Давайте предположим, что пользователь, использующий приложение Android, уже зарегистрирован в Интернете. Чтобы получить любую информацию от сервера, например, события или репутацию, мы должны инициировать запрос на токен на предъявителя. Мы последовали за публикацией в блоге  от Google, шаг за шагом о кросс-клиентской идентичности, которая привела нас к двум рабочим решениям. У обоих есть свои ограничения.

Для обоих из них вам необходимо зарегистрировать приложение Android ВНУТРИ вашего проекта приложения в консоли разработчика Google. Не создавайте новый, так как они будут связаны друг с другом и должны быть частью одного проекта. Пожалуйста, проверьте дважды ключ SHA1 для приложения Android, которое вы вводите И имя пакета вашего приложения Android. В начале мы начали с рефакторинга текущего приложения уведомлений, изменив имя пакета с io.teamgeist.app на io.teamgeist.android. Это приводит к расстраиванию ошибок INVALID_CLIENT_ID и INVALID_AUDIENCE. Когда мы вернулись к .app и заново создали приложение Android в консоли разработчика, все начало работать. Мы не пытались переименовать его обратно в .android, поэтому мы не можем сказать, является ли это запрещенным ключевым словом в имени пакета или, может быть, мы были слишком уверены в переименовании имен пакетов Android для IDE.Если вы столкнулись с какой-либо ошибкой, проверьте ключ SHA1 хранилища ключей и имя вашего пакета. Также посмотрите на это сообщение в блоге , которое пришло довольно кстати.

Если вы все сделали правильно, вы можете получить код авторизации или GoogleIdToken от GoogleAuthUtil. Для этого вам потребуется идентификатор клиента сервера или веб-приложения, зарегистрированного в вашем проекте.

Выберите учетную запись Google

Перед началом работы вам нужно разрешить пользователю выбрать учетную запись Google. Это можно сделать, вызвав метод Выбрать учетную запись из класса AccountPicker:

Intent intent = AccountPicker.newChooseAccountIntent(
        null, null,
        {"com.google"}, 
        false, null, null, null, null);
startActivityForResult(intent, PICK_ACCOUNT_CODE);

Когда пользователь выбирает одну учетную запись, запускается метод onActivityResult инициирующего действия. Для того, чтобы получить код авторизации или GoogleIdToken, вам нужен адрес электронной почты пользователя из намерений дополнений.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
...
        if (resultCode == RESULT_OK) {
            String email = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
....

Код авторизации

Идея использовать тот же поток с кодом авторизации была заманчивой. Получите код из приложения Android и отправьте его на сервер, где сервер может обменять его на пару токенов обновления / доступа. Вы можете запросить код авторизации по телефону:

GoogleAuthUtil.
        getToken(yourActivity, email,
        "oauth2:server:client_id:{server_client_id}:api_scope:");

Этот вызов является блокирующим и поэтому должен выполняться, например, в AsyncTask. Кроме того, server_client_id параметра scope должен быть идентификатором клиента сервера.

Когда вы вызываете это, вы не получите код авторизации, а получите исключение типа UserRecoverableAuthException, поскольку вам необходимо авторизовать ваше приложение Android для автономного доступа. Само исключение уже содержит намерение сработать. Будет запущен экран согласия, на котором пользователь должен предоставить запрошенные разрешения приложения.

catch (UserRecoverableAuthException userRecoverableException) {
    yourActivity.startActivityForResult(userRecoverableException.getIntent(), 
    GRANT_PERMISSIONS_CODE);
}

Screenshot_2015-01-02-21-07-33

Когда вы добавляете больше областей в строку области действия (см. Com.google.android.gms.common.Scopes для получения доступных разрешений), согласие будет содержать больше запросов на разрешение.

После того, как пользователь примет согласие, будет вызван onActivityResult инициирующего действия. Из дополнений вы получаете код авторизации:

protected void onActivityResult(int requestCode,int resultCode,Intent data){
    if(requestCode==GRANT_PERMISSIONS_CODE){
        if(resultCode==RESULT_OK){
            Bundle extras=data.getExtras();
            String authtoken=extras.getString("authtoken");
        }
    }
}

Код имеет очень короткое время жизни (TTL) и может использоваться только один раз. После того, как вы отправите токен на сервер, вы сможете получить его в обмен на обновление и доступ. После этого создайте токен на предъявителя и верните его в приложение для Android, как вы делали бы это с вашим приложением JavaScript. Добавьте токен на предъявителя в заголовок всех ваших вызовов REST.

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

1. Если пользователь выходит из приложения Android (удалите токен на предъявителя), вам необходимо будет повторить все шаги, включая экран согласия.

2. В нашем примере нам также не нужен автономный доступ к пользовательским данным (то есть сервер может взаимодействовать с Google без какого-либо взаимодействия с пользователем). Как мы полагаем, пользователь уже зарегистрирован через Интернет и предоставил разрешение на автономный доступ.

В нашем приложении для Android мы хотим получать только данные, содержащиеся на нашем сервере. Далее, давайте взглянем на подход GoogleIdToken.

GoogleIdToken

GoogleIdToken — это веб- токен JSON (JWT). JWT состоит из трех частей: заголовок, полезная нагрузка и подпись. Подпись зашифрована и содержит заголовок и полезную нагрузку. С помощью открытых ключей от Google (https://www.googleapis.com/oauth2/v1/certs) каждый может расшифровать подпись и проверить, соответствует ли она заголовку и полезной нагрузке.

Полезная нагрузка GoogleIdToken содержит несколько сведений о пользователе и приложении. Если вы отправите токен на https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=[jwt], полезная нагрузка может выглядеть следующим образом:

{
    "issuer": "accounts.google.com",
    "issued_to": "[Client id of your android application from the developer console]",
    "audience": "[Client id of your web application from the developer console]",
    "user_id": "10186*************",
    "expires_in": 3581,
    "issued_at": 1420230999,
    "email": "[email protected]",
    "email_verified": true
}

На сервере вы проверяете подпись и затем просматриваете полезную нагрузку.

1. Если проверка подписи в порядке, вы знаете, что токен был создан Google.

2. Вы знаете, что Google уже проверил ваше Android-приложение (он проверяет ключ SHA-1 и имя пакета вашего Android-приложения и сравнивает их с клиентом Android, зарегистрированным в том же проекте, что и веб-приложение / сервер), и таким образом предоставляет ваш приложение с JWT для пользователя с адресом электронной почты в полезной нагрузке.

Вот почему вы должны проверить поле «аудитория». Он должен содержать ваш идентификатор клиента веб / сервера приложений. Вы также можете проверить поле «assign_to» (также называемое «azp»). Он содержит идентификатор клиента вашего приложения Android. Но это на самом деле не нужно, если у вас есть только один клиент, взаимодействующий таким образом с вашим сервером. Google говорит, что это поле может быть подделано от рутированных устройств, хотя мы не знаем, как нам это сделать.

Итак, давайте вернемся к нашему приложению. Мы хотим получить GoogleIdToken. Вы можете получить его из Android с помощью того же вызова метода, с которым вы получили код авторизации:

Измените параметр области в вызове с

"oauth2:server:client_id:{server_client_id}:api_scope:"

в

"audience:server:client_id:{server_client_id}"

В отличие от запроса кода авторизации, мы на этот раз получаем ответ обратно. Экран согласия не требуется, поскольку пользователь уже предоставил разрешение на серверное приложение. На стороне сервера Google предоставляет проверку подписи токена с помощью GoogleIdTokenVerifier. Вы также должны предоставить свой идентификатор клиента веб / серверного приложения через построитель GoogleIdTokenVerifier:

JsonFactory jsonFactory = new JacksonFactory();
NetHttpTransport transport = new NetHttpTransport();
String jwt = "thestringofthejwt";

GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
    .setAudience(Arrays.asList("YOUR SERVER CLIENT ID "))
    .setIssuer("accounts.google.com")
    .build();

GoogleIdToken googleIdToken = verifier.verify(jwt);
if (googleIdToken != null) {
    //The token is valid. You can add a check for the issued_to/azp field
    if (!"YOUR ANDROID CLIENT ID".equals(googleIdToken.getPayload().getAuthorizedParty())) {
        throw new OAuthException("Wrong authorized party returned");
    }

}

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

1. Вам не нужно звонить в Google для подтверждения токена.

2. Вы даже можете изменить свое серверное приложение, чтобы оно принимало не только токен на предъявителя, но и GoogleIdToken. Следовательно, вы можете сэкономить создание токена на предъявителя и сохранить его в базе данных.

Единственное, что вам нужно сделать, это проверить, выполнил ли пользователь вход в систему через Интернет, и выполнить поиск данных пользователя в вашей базе данных пользователей, используя идентификатор социальной сети или адрес электронной почты из JWT.

Недостатки:

1. Пользователь должен войти в приложение через поток кода авторизации из веб-приложения. Там пользователь должен принять экран согласия.

2. Пользователь никогда не выходит из системы. Если срок действия JWT истекает (60 минут), приложение Android может получить новый JWT без взаимодействия с вашим сервером. Даже если вы аннулируете все токены носителя от пользователя, JWT все равно будет действительным. Блокировка пользователя возможна только путем добавления флага к пользователю в вашей базе данных.

3. Вы не можете получить доступ к дополнительным данным на стороне сервера из Google с помощью JWT.

4. Проверка наличия JWT в дополнение к изменениям токена на предъявителя на стороне нашего сервера.

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

В нашем следующем эпизоде ​​мы будем использовать логин, чтобы периодически собирать события с нашего сервера и отправлять их в виде уведомлений на Android Wear Smartwatch.

Не стесняйтесь делиться своими мыслями и мнениями в разделе комментариев ниже.