Статьи

Tailor Doorkeeper с жетонами обновления, представлениями и стратегиями

В предыдущей части этой серии мы познакомились с Doorkeeper , движком Rails для создания пользовательских поставщиков OAuth 2. Я показал, как интегрировать это решение в ваше приложение и как использовать его для защиты запросов API.

В настоящее время пользователи могут регистрировать свои приложения OAuth 2, получать токены доступа, выполнять запросы API и работать с областями действия. Однако есть несколько вещей, о которых нужно позаботиться:

  • Список приложений OAuth 2 может быть доступен любому
  • Пользователи не могут получить токены обновления и использовать их для получения новых токенов доступа
  • Было бы неплохо создать собственную стратегию OmniAuth, которую позже можно будет упаковать как драгоценный камень.
  • Нам нужен способ настроить вид и маршруты

Поэтому в этой статье мы займемся этими вопросами.

Исходный код можно найти на GitHub .

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

Защита приложений OAuth 2

Напомню, что на данный момент у нас есть два приложения: так называемый «сервер» (фактический поставщик аутентификации) и «клиент» (приложение для тестирования). В настоящее время, даже если пользователь не аутентифицирован, он все равно может посетить localhost: 3000 / oauth / apps и просмотреть список зарегистрированных приложений OAuth 2. Очевидно, что это не очень хорошо, поэтому нам нужен способ ограничить доступ к этой странице.

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

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

[...] admin_authenticator do User.find_by_id(session[:user_id]) || redirect_to(new_session_url) end [...] 

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

Перезагрузите сервер и перейдите к localhost: 3000 / oauth / apps . Если вы не авторизованы, вы будете перенаправлены на страницу входа. Это означает, что все работает так, как запланировано.

Представляем обновленные токены

По умолчанию токены доступа истекают через 2 часа, после чего пользователи должны повторить процесс аутентификации снова. Если вам нужно настроить это поведение, раскомментируйте и измените access_token_expires_in 2.hours внутри файла инициализатора Doorkeeper. Кстати, здесь вы также можете настроить параметр authorization_code_expires_in 10.minutes . Этот код используется на этапе аутентификации для получения токена доступа, и срок его службы должен быть коротким.

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

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

 [...] use_refresh_token [...] 

Не забудьте перезагрузить сервер после изменения этого файла.

Теперь откройте свое клиентское приложение и измените SessionsController для хранения токенов обновления и доступа к времени истечения токена:

sessions_controller.rb

 class SessionsController < ApplicationController def create req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['oauth_redirect_uri']}" response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params) session[:access_token] = response['access_token'] session[:refresh_token] = response['refresh_token'] session[:expires_at] = response['expires_in'].seconds.from_now redirect_to root_path end end 

response['expires_in'] количество секунд, response['expires_in'] как скоро истечет токен, но для нашего удобства я превращаю его в объект DateTime .

Теперь представьте два новых маршрута:

конфиг / routes.rb

 [...] get '/user', to: 'users#show', as: :user get '/user/update', to: 'users#update', as: :update_user [...] 

В настоящее время наша главная страница имеет следующее содержание:

просмотров / страниц / index.html.erb

 <h1>Welcome!</h1> <% if session[:access_token] %> <%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %> <%= link_to 'Update User', "http://localhost:3000/api/user/update?access_token=#{session[:access_token]}" %> <% else %> <%= link_to 'Authorize via Keepa', new_oauth_token_path %> <% end %> 

Я хочу извлечь эти URL «Получить пользователя» и «Обновить пользователя» в действия контроллера, и поэтому мы создали новые маршруты:

просмотров / страниц / index.html.erb

 [...] <% if session[:access_token] %> <%= link_to 'Get User', user_path %> <%= link_to 'Update User', update_user_path %> <% else %> <%= link_to 'Authorize via Keepa', '/auth/keepa' %> <% end %> 

Вот новый контроллер:

users_controller.rb

 class UsersController < ApplicationController def show redirect_to "http://localhost:3000/api/user?access_token=#{session[:access_token]}" end def update redirect_to "http://localhost:3000/api/user/update?access_token=#{session[:access_token]}" end end 

Теперь нам нужно проверить, не истек ли токен доступа перед выполнением запроса API. Самый простой способ — использовать before_action :

users_controller.rb

 [...] before_action :check_access_token [...] private def check_access_token redirect_to root_path unless session[:access_token] if session[:expires_at] <= Time.current req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&refresh_token=#{session[:refresh_token]}&grant_type=refresh_token&redirect_uri=#{ENV['oauth_redirect_uri']}" response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params) set_oauth_info_from response end end 

Если токен доступа не установлен, мы просто перенаправляем пользователя обратно. Если токен истек, мы генерируем новую строку с параметрами запроса client_id , client_secret , refresh_token , redirect_uri и grant_type (для этого необходимо установить значение refresh_token ). Вы спросите, а как насчет сферы? По умолчанию новый токен доступа будет предоставлен для выполнения тех же действий, которые были первоначально одобрены пользователем. Вы можете сузить область, но вы не можете добавить области, которые не были предоставлены ранее.

Далее мы берем ответ и передаем его в метод set_oauth_info_from который будет представлен сейчас:

application_controller.rb

 [...] private def set_oauth_info_from(response) session[:access_token] = response['access_token'] session[:refresh_token] = response['refresh_token'] session[:expires_at] = response['expires_in'].seconds.from_now end 

Используйте этот метод внутри SessionsController чтобы избежать дублирования кода:

sessions_controller.rb

 def create req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['oauth_redirect_uri']}" response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params) set_oauth_info_from response redirect_to root_path end 

Это оно! Теперь наши пользователи могут без проблем получать токены обновления!

Создание собственного провайдера OmniAuth

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

Библиотека / OmniAuth / стратегии / keepa.rb

 require 'omniauth-oauth2' module OmniAuth module Strategies class Keepa < OmniAuth::Strategies::OAuth2 option :name, :keepa option :client_options, { :site => "http://localhost:3000", :authorize_url => "/oauth/authorize" } uid { raw_info["id"] } info do { :email => raw_info["email"] } end def raw_info @raw_info ||= access_token.get('/api/user').parsed end end end end 

Я назвал свою стратегию Keepa, поэтому имя класса и option :name должны быть установлены соответственно.

Метод raw_info используется для получения информации о пользователе, который проходит аутентификацию с помощью нашей стратегии. /api/user — это маршрут, который мы ввели в предыдущей статье. Напомню, как выглядит соответствующее действие:

апи / users_controller.rb

 [...] def show render json: current_resource_owner.as_json end private def current_resource_owner User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token end [...] 

Мы в основном выбираем пользователя, который инициировал фазу аутентификации, и отображаем его информацию в формате JSON.

uid { raw_info["id"] } объясняет, какое поле использовать в качестве уникального идентификатора. Мы используем идентификатор из таблицы, но вместо этого вы можете ввести специальное поле.

 info do { :email => raw_info["email"] } end 

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

Этот код зависит от абстрактной стратегии omniauth-oauth2 , поэтому подключите его:

Gemfile

 [...] gem 'omniauth-oauth2', '1.3.1' [...] 

и беги

 $ bundle install 

Я использую версию 1.3.1 здесь из-за этой проблемы, которая затрагивает Doorkeeper .

Теперь создайте новый файл инициализатора для клиентского приложения:

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

 require File.expand_path('lib/omniauth/strategies/keepa', Rails.root) Rails.application.config.middleware.use OmniAuth::Builder do provider :keepa, ENV['oauth_token'], ENV['oauth_secret'], scope: 'public write' end 

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

public и write — это области видимости, которые были представлены в предыдущей статье. В качестве разделителя следует использовать пробел.

OmniAuth указывает нам добавить специальный маршрут обратного вызова, поэтому замените старый:

конфиг / routes.rb

 [...] get '/oauth/callback', to: 'sessions#create' [...] 

с

конфиг / routes.rb

 [...] get '/auth/:provider/callback', to: 'sessions#create' [...] 

Изменить файл конфигурации:

конфиг / local_env.yml

 [...] oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fkeepa%2Fcallback' 

Также не забудьте открыть серверное приложение, перейти к localhost: 3000 / oauth / apps и отредактировать URI перенаправления для вашего приложения.

Измените главную страницу клиентского приложения:

просмотров / страниц / index.html.erb

 [...] <% if session[:access_token] %> <%= link_to 'Get User', user_path %> <%= link_to 'Update User', update_user_path %> <% else %> <%= link_to 'Authorize via Keepa', '/auth/keepa' %> <% end %> 

Следующим шагом является изменение действия обратного вызова:

sessions_controller.rb

 [...] def create set_oauth_info_from request.env['omniauth.auth']['credentials'] redirect_to root_path end [...] 

request.env['omniauth.auth'] должен быть вам знаком. Он содержит хэш аутентификации в следующем формате:

 {"provider"=>:keepa, "uid"=>1, "info"=>{"email"=>"[email protected]"}, "credentials"=> {"token"=>"8d8...", "refresh_token"=>"321...", "expires_at"=>1453301961, "expires"=>true}, "extra"=>{}} о {"provider"=>:keepa, "uid"=>1, "info"=>{"email"=>"[email protected]"}, "credentials"=> {"token"=>"8d8...", "refresh_token"=>"321...", "expires_at"=>1453301961, "expires"=>true}, "extra"=>{}} 

Обратите внимание, что поле называется token (не access_token ), и вместо expires_in у нас есть expires_at .

Поэтому метод set_oauth_info_from должен быть немного изменен:

application_controller.rb

 [...] def set_oauth_info_from(response) session[:access_token] = response['access_token'] || response['token'] session[:refresh_token] = response['refresh_token'] if response['expires_in'] session[:expires_at] = response['expires_in'].seconds.from_now else session[:expires_at] = Time.at response['expires_at'] end end [...] 

Вспомогательный метод new_oauth_token_path может быть полностью удален, и вы готовы аутентифицироваться с помощью вашей новой блестящей стратегии. Большой!

Дальнейшая настройка

Вы, вероятно, задаетесь вопросом, возможно ли изменить вид и маршрут Дверного дворника? И да — это вполне возможно.

Чтобы изменить представления , выполните эту простую команду:

 $ rails generate doorkeeper:views 

Он сгенерирует все виды и макеты, используемые Doorkeeper.

Вы можете использовать свои собственные макеты, настроив эти параметры в файле application.rb :

 config.to_prepare do Doorkeeper::ApplicationsController.layout "my_layout" Doorkeeper::AuthorizationsController.layout "my_layout" Doorkeeper::AuthorizedApplicationsController.layout "my_layout" end 

Маршруты и вспомогательные методы из вашего приложения можно использовать в представлениях Привратника, но они должны были иметь префикс main_app (например, main_app.login_path ), потому что это решение является изолированным механизмом.

Настройка маршрутов по умолчанию также довольно проста. Метод use_doorkeeper принимает блок со всеми необходимыми конфигурациями. Например, вы можете использовать собственный контроллер для управления приложениями OAuth 2:

 use_doorkeeper do controllers :applications => 'custom_applications' end 

controllers принимают значения :authorizations :tokens :applications и :authorized_applications .

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

 use_doorkeeper do skip_controllers :applications end 

skip_controllers принимает те же значения.

Обратитесь к этой странице вики для получения дополнительной информации.

Вывод

В этой статье мы доработали нашего собственного поставщика OAuth 2, немного его защитив, представив токены обновления и разработав собственную стратегию OmniAuth. Теперь вы готовы к дальнейшей настройке и расширению своего приложения по мере необходимости! Я настоятельно рекомендую вам просмотреть вики Doorkeeper, так как она содержит несколько хороших руководств по решению общих проблем.

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