Статьи

OAuth 2 Все вещи с oPRO: настройка

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

Исходный код для серверных и клиентских приложений можно найти на GitHub .

Сфера

Когда пользователи проходят проверку подлинности через oPRO, их просят разрешить действия чтения и записи (или «область» разрешений) для приложения. Чтение является обязательным действием, и это разрешение не может быть отозвано. Это позволяет приложению выполнять запросы GET только к тем действиям, которые allow_oauth! белый список с помощью allow_oauth! метод (см. предыдущую статью .)

Запись, с другой стороны, может быть отозвана пользователем. Это разрешение позволяет приложению выполнять любые действия, используя HTTP DELETE, PATCH и другие глаголы.

Если вам нужен более детальный контроль, измените файл инициализатора oPRO:

конфиг / Инициализаторы / opro.rb

 [...] config.request_permissions = [:write, :list, :invite] [...] 

Далее вам нужно будет принудительно проверить эти атрибуты на наличие желаемых действий. Это делается следующим образом:

 [...] require_oauth_permissions :list, only: :index [...] 

Если приложение не имеет необходимых разрешений, возникнет ошибка 401. Чтобы пропустить проверку разрешений, используйте skip_oauth_permissions (так же, как по умолчанию skip_before_action ):

 [...] skip_oauth_permissions :list, only: :index [...] 

Вы даже можете oauth_client_can_#{permission}? свою собственную логику проверки прав доступа, переопределив метод oauth_client_can_#{permission}? :

 def oauth_client_can_email? # your custom logic here end 

Не забывайте, что такие методы должны возвращать либо true либо false .

Срок действия токена ограничения доступа

Все популярные сервисы предоставляют токен доступа с ограниченным сроком службы в целях безопасности, и вы, вероятно, захотите сделать то же самое. По умолчанию oPRO генерирует «вечный» токен, но его можно изменить, поэтому давайте сделаем это сейчас.

Измените файл инициализатора oPRO:

конфиг / Инициализаторы / opro.rb

 [...] config.require_refresh_within = 12.hours [...] 

Установите значение, которое работает для вас, и не забудьте перезагрузить сервер. Теперь expires_in в хэше аутентификации будет содержать количество секунд, равное expires_in службы токена. Давайте преобразовать его в DateTime и сохранить в таблице:

модели / user.rb

 [...] def from_opro(auth = nil) [...] user.expires_at = auth['expires_in'].seconds.from_now [...] end [...] 

Если пользователь пытается выполнить запрос с токеном с истекшим сроком действия, oPRO выдаст ошибку 401 (неавторизованную), поэтому клиентское приложение, вероятно, должно спасти от него:

 [...] rescue_from RestClient::Unauthorized do flash[:warning] = 'No access rights to perform that action or your token has expired :(' redirect_to root_path end [...] 

Теперь я хочу проверить, не истек ли токен. Если да — обновите его и сохраните новый на столе. Как вы помните, у нас в before_action :check_token есть before_action :check_token и это кажется хорошим местом для выполнения такой проверки:

api_tests_controller.rb

 [...] def check_token redirect_to new_opro_token_path and return if !current_user || current_user.token_missing? || (current_user.token_expired? && current_user.refresh_token_missing?) if current_user.token_expired? updated_current_user = current_user.refresh_token! if updated_current_user login updated_current_user else flash[:warning] = "There was an error while trying to refresh your token..." redirect_to root_path end end end [...] 

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

Вот новые модели действий, которые используются:

модели / user.rb

 [...] def refresh_token_missing? !self.refresh_token.present? end def token_expired? self.expires_at < Time.current end def refresh_token! return unless token_expired? client = OproApi.new(refresh_token: self.refresh_token) User.from_opro(client.refresh!) end [...] 

refresh! это еще один метод API:

модели / opro_api.rb

 [...] def refresh! return unless refresh_token JSON.parse(RestClient.post(TOKEN_URL, { client_id: ENV['opro_client_id'], client_secret: ENV['opro_client_secret'], refresh_token: refresh_token }, accept: :json)) end [...] 

Обновление токена во многом похоже на получение токена доступа, за исключением отправки параметра refresh_token вместо code . oPRO отвечает полным хэшем аутентификации, поэтому мы передаем ответ методу User.from_opro чтобы сохранить новые данные.

Теперь вы можете отредактировать поле expires_at вручную и установить для него некоторую дату в прошлом. Процесс обновления токена должен работать прозрачно для пользователя!

Ограничение скорости

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

Существует много способов решения этой задачи, поэтому oPRO оставляет логику реализации для вас. Давайте сделаем все просто и создадим два поля в таблице opro_client_apps :

  • requests_count ( integer ) — по умолчанию 0. Увеличивается каждый раз, когда делается запрос API;
  • last_request_at ( date ) — используется для отслеживания последнего запроса. Если дата меньше, чем сегодня, задайте для requests_count значение 0, в противном случае увеличьте его на 1.

Создайте и примените соответствующую миграцию:

 $ rails g migration add_requests_count_and_last_request_at_to_opro_client_apps requests_count:integer last_request_at:date $ rake db:migrate 

Внутри ApplicationController нам нужно изменить два метода, которые изначально пусты: oauth_client_record_access! и oauth_client_rate_limited? :

application_controller.rb

 [...] private def oauth_client_record_access!(client_id, params) client_app = Opro::Oauth::ClientApp.find(client_id) if client_app.last_request_at < Date.today client_app.last_request_at = Date.today client_app.requests_count = 1 else client_app.requests_count += 1 end client_app.save end [...] 

Вот как можно реализовать описанную выше логику: если сегодня не было сделано ни одного запроса, сбросьте счетчик, иначе client_id его на 1. client_id содержит идентификатор приложения, а params содержит параметры, отправленные вместе с запросом (например, access_token ).

application_controller.rb

 [...] def oauth_client_rate_limited?(client_id, params) client_app = Opro::Oauth::ClientApp.find(client_id) client_app.requests_count > 2 end [...] 

Просто найдите необходимое приложение и проверьте, не превысило ли оно пороговое значение. Очевидно, что для реального приложения вам нужно предоставлять чуть большее количество доступных запросов в день. Теперь попробуйте выполнить пару запросов, и после третьего вы получите ошибку «Несанкционированный» — это означает, что наше ограничение скорости работает, как и ожидалось!

Другие параметры конфигурации

Использование пользовательского решения для аутентификации

В настоящее время готовый oPRO поддерживает только Devise, но вы можете настроить несколько параметров и применить свою собственную логику:

конфиг / Инициализаторы / opro.rb

 Opro.setup do |config| [...] config.login_method do |controller, current_user| controller.sign_in(current_user, :bypass => true) end config.logout_method do |controller, current_user| controller.sign_out(current_user) end config.authenticate_user_method do |controller| controller.authenticate_user! end [...] end 

Обмен электронной почты и пароля для токена

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

модели / opro_api.rb

 [...] attr_reader :access_token, :refresh_token, :email, :password def initialize(access_token: nil, refresh_token: nil, email: nil, password: nil) @access_token = access_token @refresh_token = refresh_token @email = email @password = password end def authenticate_with_email! return unless email && password JSON.parse(RestClient.post(TOKEN_URL, { client_id: ENV['opro_client_id'], client_secret: ENV['opro_client_secret'], email: email, password: password }, accept: :json)) end [...] 

Как видите, вместо отправки code мы предоставляем учетные данные пользователя. Если адрес электронной почты и / или пароль неверны, возникнет ошибка 401. Не забывайте, что вы должны использовать безопасное соединение, чтобы предотвратить кражу пароля пользователя!

Кроме того, вы можете разрешить эту функцию только для некоторых приложений. Для этого переопределить oauth_valid_password_auth? внутри вашего ApplicationController :

application_controller.rb

 [...] def oauth_valid_password_auth?(client_id, client_secret) client_id == 'some client id' end [...] 

Конечно, это очень наивная реализация, потому что вы, вероятно, будете иметь массив разрешенных клиентских идентификаторов (oPRO называет их «благословенными идентификаторами») и выполнять проверки против них.

Идите и попробуйте:

 $ rails c $ client = OproApi.new(email: '[email protected]', password: '123') $ client.authenticate_with_email! 

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

К счастью, это делается только с одной настройкой:

конфиг / Инициализаторы / opro.rb

 [...] config.find_user_for_auth do |controller, params| user = User.find(params[:email]) user.valid_password?(params[:password]) ? user : false end [...] 

Этот метод должен либо вернуть пользователя, либо false если учетные данные неверны.

Вывод

Наконец, вы добрались до конца этого урока. Мои поздравления! Во время нашего путешествия мы обсудили много интересных вещей, и, надеюсь, к настоящему времени вы готовы запустить этот поставщик аутентификации и расширить его.

Вы когда-нибудь использовали OPRO? Рассматриваете ли вы использовать его в будущем? Лично мне понравилось работать с ним, хотя некоторые вещи должны быть изменены. Если у вас есть какие-либо вопросы, не стесняйтесь спрашивать.

В следующей статье я расскажу о Doorkeeper — еще одном решении для создания поставщиков аутентификации, так что до скорой встречи!