Статьи

Начало работы с Doorkeeper и OAuth 2.0

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

В моей предыдущей серии я показал, как настроить собственного провайдера OAuth 2, используя oPRO , движок Rails. Сегодня мы собираемся решить ту же проблему, но на этот раз с помощью другого, более популярного инструмента — Doorkeeper gem , созданного Applicake в 2011 году. С тех пор проект значительно развился и теперь представляет собой полноценное и удобное решение. Doorkeeper может использоваться как с базовыми приложениями Rails, так и с Grape .

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

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

Создание приложений

Я собираюсь использовать Rails 4.2 для этой демонстрации. Мы создадим два приложения: фактический поставщик (назовем его «сервер») и приложение для тестирования («клиент»). Начните с сервера:

$ rails new Keepa -T

Нам понадобится какая-то аутентификация для этого приложения, но Doorkeeper не определяет, какой из них использовать. Недавно я рассмотрел множество вариантов , но сегодня давайте закодируем наше простое решение с использованием bcrypt .

Gemfile

 [...]
gem 'bcrypt-ruby'
[...]

Установите гем, сгенерируйте и примените новую миграцию:

 $ bundle install
$ rails g model User email:string:index password_digest:string
$ rake db:migrate

Теперь оснастим модель User

модели / user.rb

 [...]
has_secure_password
validates :email, presence: true
[...]

Создайте новый контроллер для регистрации:

users_controller.rb

 class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      session[:user_id] = @user.id
      flash[:success] = "Welcome!"
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

Вот соответствующий вид:

просмотров / пользователей / new.html.erb

 <h1>Register</h1>

<%= form_for @user do |f| %>
  <%= render 'shared/errors', object: @user %>
  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>

  <div>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </div>

  <%= f.submit %>
<% end %>

<%= link_to 'Log In', new_session_path %>

просмотров / общий / _errors.html.erb

 <% if object.errors.any? %>
  <div>
    <h5>Some errors were found:</h5>

    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Добавьте несколько маршрутов:

конфиг / routes.rb

 [...]
resources :users, only: [:new, :create]
[...]

Чтобы проверить, вошел ли пользователь в систему или нет, мы будем использовать старый добрый метод current_user

application_controller.rb

 [...]
private

def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end

helper_method :current_user
[...]

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

sessions_controller.rb

 class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:email])
    if @user && @user.authenticate(params[:password])
      session[:user_id] = @user.id
      flash[:success] = "Welcome back!"
      redirect_to root_path
    else
      flash[:warning] = "You have entered incorrect email and/or password."
      render :new
    end
  end

  def destroy
    session.delete(:user_id)
    redirect_to root_path
  end
end

authenticate

Вот мнения и маршруты:

просмотры / сессия / new.html.erb

 <h1>Log In</h1>

<%= form_tag sessions_path, method: :post do %>
  <div>
    <%= label_tag :email %>
    <%= email_field_tag :email %>
  </div>

  <div>
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>

  <%= submit_tag 'Log In!' %>
<% end %>

<%= link_to 'Register', new_user_path %>

конфиг / routes.rb

 [...]
resources :sessions, only: [:new, :create]
delete '/logout', to: 'sessions#destroy', as: :logout
[...]

Наконец, добавьте контроллер статических страниц, корневую страницу и маршрут:

pages_controller.rb

 class PagesController < ApplicationController
  def index
  end
end

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

 <% if current_user %>
  You are logged in as <%= current_user.email %><br>
  <%= link_to 'Log Out', logout_path, method: :delete %>
<% else %>
  <%= link_to 'Log In', new_session_path %><br>
  <%= link_to 'Register', new_user_path %>
<% end %>

конфиг / routes.rb

 [...]
root to: 'pages#index'
[...]

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

Интегрирующий привратник

Добавьте новый драгоценный камень в Gemfile :

Gemfile

 [...]
gem 'doorkeeper'
[...]

Установите его и запустите генератор Привратника:

 $ bundle install
$ rails generate doorkeeper:install

Этот генератор создаст новый файл инициализатора и добавит строку use_doorkeeperroute.rb . Эта строка содержит несколько маршрутов Doorkeeper (для регистрации нового OAuth 2, запроса токена доступа и т. Д.), Которые мы обсудим позже.

Следующим шагом является создание миграций. По умолчанию Doorkeeper использует ActiveRecord, но вы можете использовать doorkeeper-mongodb для поддержки Mongo.

 $ rails generate doorkeeper:migration

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

 $ rake db:migrate

Откройте файл инициализатора Doorkeeper и найдите строку resource_owner_authenticator do По умолчанию это вызывает исключение, поэтому замените содержимое блока на:

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

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

Модель User Вы можете загрузить сервер, зарегистрироваться и перейти на localhost: 3000 / oauth / apps . Это страница для создания вашего нового приложения OAuth 2. Создайте его, указав «http: // localhost: 3001 / oauth / callback» в качестве URL-адреса перенаправления. Обратите внимание, что мы используем порт 3001 для клиентского приложения (еще не существует), поскольку порт 3000 уже используется серверным приложением. После этого вы увидите идентификатор приложения и секретный ключ. Оставьте эту страницу открытой или запишите значения.

Следующим шагом является создание клиентского приложения.

Создание клиентского приложения

Запустите генератор Rails, чтобы создать приложение:

 $ rails new KeepaClient -T

Добавьте контроллер статических страниц, корневую страницу и маршрут:

pages_controller.rb

 class PagesController < ApplicationController
  def index
  end
end

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

 <h1>Welcome!</h1>

конфиг / routes.rb

 [...]
root to: 'pages#index'
[...]

Теперь давайте также создадим файл local_env.yml для хранения некоторой конфигурации, в частности Client Id и Secret, полученных от серверного приложения на предыдущем шаге:

конфиг / local_env.yml

 server_base_url: 'http://localhost:3000'
oauth_token: <CLIENT ID>' 
oauth_secret: '<SECRET>'
oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Foauth%2Fcallback'

Загрузите это в ENV

конфиг / application.rb

 [...]
if Rails.env.development?
  config.before_configuration do
    env_file = File.join(Rails.root, 'config', 'local_env.yml')
    YAML.load(File.open(env_file)).each do |key, value|
      ENV[key.to_s] = value
    end if File.exists?(env_file)
  end
end
[...]

Исключите файл .yml из контроля версий, если хотите:

.gitignore

 [...]
config/local_env.yml
[...]

Получение токена доступа

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

Мы будем использовать rest-client для отправки запросов.

Добавьте новый драгоценный камень и bundle install

Gemfile

 [...]
gem 'rest-client'
[...]

Чтобы получить токен доступа, пользователю сначала необходимо посетить URL «localhost: 3000 / oauth / authorize», указав идентификатор клиента, URI перенаправления и тип ответа. Давайте введем вспомогательный метод для генерации соответствующего URL:

application_helper.rb

 [...]
def new_oauth_token_path
  "#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code"
end
[...]

Отобразите его на главной странице клиентского приложения:

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

 [...]
<%= link_to 'Authorize via Keepa', new_oauth_token_path %>
[...]

Теперь представим маршрут обратного вызова:

конфиг / routes.rb

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

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']
    redirect_to root_path
  end
end

После посещения «localhost: 3000 / oauth / authorize» пользователь будет перенаправлен на URL обратного вызова с установленным параметром code Внутри действия createclient_idclient_secretcodegrant_typeredirect_uri Если все было сделано правильно, ответ будет содержать JSON с токеном доступа и временем его жизни (по умолчанию установлено 2 часа). В противном случае будет возвращена ошибка 401.

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

Если вы читаете мои статьи о OPRO, этот процесс должен быть очень знакомым. Если нет — мои поздравления, вы только что закодировали клиентское приложение, и теперь пользователи могут получить токены доступа.

Это все замечательно, но для чего мы можем использовать эти токены? Очевидно, что для защиты нашего API, давайте представим это сейчас!

Представляем простой API

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

Контроллеры / API / users_controller.rb

 class Api::UsersController < ApplicationController
  before_action :doorkeeper_authorize!

  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
end

Давайте продолжим шаг за шагом здесь.

before_action :doorkeeper_authorize! позволяет нам защитить действия контроллера, предотвращая неавторизованные запросы. Это означает, что пользователи должны предоставить токены доступа для выполнения действия. Если токен не предоставлен, Дверной дворник преградит им путь, сказав: «Ты не пройдешь!», Как Гэндальф. Ну, на самом деле в нашем случае будет возвращена ошибка 401, но вы поняли.

current_resource_owner doorkeeper_token.resource_owner_idresource_owner_authenticator

as_jsonUser Вы можете предоставить опцию except

 current_resource_owner.as_json(except: :password_digest)

Добавить новый маршрут:

конфиг / routes.rb

 namespace :api do
  get 'user', to: 'users#show'
end

Теперь, если вы попытаетесь получить доступ к «localhost: 3000 / api / user», вы увидите пустую страницу. Откройте консоль, и вы увидите ошибку 401, означающую, что это действие теперь защищено.

Если вы хотите настроить эту неавторизованную страницу, вы можете переопределить метод doorkeeper_unauthorized_render_optionsApplicationController Например, добавьте сообщение об ошибке, подобное этому:

application_controller.rb

 def doorkeeper_unauthorized_render_options(error: nil)
  { json: { error: "Not authorized" } }
end

Настроить главную страницу клиентского приложения:

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

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

Авторизуйтесь, нажмите на ссылку «Получить пользователя» и посмотрите результат — вы должны увидеть данные своего пользователя!

Работа с областями действия

Области — это способ определить, какие действия сможет выполнить клиент. Давайте представим две новые области:

  • public
  • write

Прежде всего, измените файл инициализатора Doorkeeper, чтобы включить следующие новые области:

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

 [...]
default_scopes :public
optional_scopes :write
[...]

default_scopes

optional_scopes Если пропущен какой-то неизвестный объем, возникнет ошибка.

Обновите контроллер API:

апи / users_controller.rb

 [...]
before_action -> { doorkeeper_authorize! :public }, only: :show
before_action -> { doorkeeper_authorize! :write }, only: :update

def show
  render json: current_resource_owner.as_json
end

def update
  render json: { result: current_resource_owner.touch(:updated_at) }
end
[...]

Мы передаем имя области действия doorkeeper_authorize! говоря, какой объем требуется для выполнения действия. Обратите внимание, что можно также передать несколько областей:

 doorkeeper_authorize! :admin, :write

Это означает, что действие может быть выполнено, если у клиента есть права adminили права на write Если вы хотите, чтобы присутствовали оба разрешения, используйте следующую конструкцию:

 doorkeeper_authorize! :admin
doorkeeper_authorize! :write

Введите новый маршрут:

конфиг / routes.rb

 [...]
namespace :api do
  get 'user', to: 'users#show'
  get 'user/update', to: 'users#update'
end
[...]

Чтобы все было как можно проще, я использую глагол GET, но для реального приложения было бы лучше использовать PATCH. Теперь в клиентском приложении давайте изменим вспомогательный метод, чтобы запрашивать как publicwrite

application_helper.rb

 [...]
def new_oauth_token_path
  "#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code&scope=public+write"
end
[...]

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

Наконец, посетите «http: // localhost: 3000 / oauth / Applications», откройте свое приложение для редактирования и заполните поле «Области» +public write

Перезагрузите сервер и посетите страницу авторизации. Вы заметите, что теперь у пользователя запрашивается разрешение на выполнение двух действий, но его имена не очень полезны. Например, что означает «писать»? Это может быть очевидно для вас, но не для наших пользователей. Поэтому было бы лучше предоставить немного больше информации по каждой области. Это можно сделать, отредактировав файл I18n привратника, который был сгенерирован для вас:

конфиг / локали / doorkeeper.en.yml

 en:
  doorkeeper:
    scopes:
      public: 'Access your public data.'
      write: 'Make changes to your profile.'
[...]

Теперь пользователи увидят подробное описание для каждой области.

Наконец, добавьте новую ссылку на главную страницу:

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

 [...]
<% 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 %>

Иди и попробуй это!

Вывод

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

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

Как всегда, не стесняйтесь оставлять свои вопросы и до скорой встречи!