Skip to main content

OAuth2 в Rails

Для реализации OAuth2 в современном приложении на Rails 8 (для внешнего API, т.е. чтобы сторонние клиенты могли получать access tokens и обращаться к API), лучшими и наиболее актуальными решениями будут:


1. Doorkeeper — стандарт де-факто

Плюсы:

  • Поддержка всех flows (authorization code, client credentials, password, etc.)
  • Хорошая интеграция с Rails 6–8
  • Совместим с OpenID Connect через doorkeeper-openid_connect
  • Гибкий, настраиваемый, battle-tested

Минусы:

  • Местами громоздкий, требует настройки токенов и таблиц

Инсталляция:

bundle add doorkeeper
rails generate doorkeeper:install
rails db:migrate

Документация: https://doorkeeper.gitbook.io/guides/


2. Doorkeeper + Doorkeeper::JWT — если хочешь токены в формате JWT

Плюсы:

  • Позволяет клиентам проверять подпись токена без запроса к API
  • Удобно для stateless микросервисов

Установка:

# Gemfile
gem 'doorkeeper-jwt'

Пример конфигурации (в doorkeeper.rb):

access_token_generator '::Doorkeeper::JWT'

Doorkeeper::JWT.configure do
token_payload do |opts|
user = User.find(opts[:resource_owner_id])
{
iss: 'MyAPI',
iat: Time.current.to_i,
exp: (Time.current + 2.hours).to_i,
sub: user.id,
email: user.email
}
end

secret_key Rails.application.credentials.secret_key_base
encryption_method :hs256
end

3. Rodauth + JWT plugin (если ты не против Roda в составе API)

Плюсы:

  • Современный, безопасный подход (полностью реализует OWASP best practices)
  • Очень гибкий, минималистичный
  • Меньше магии, чем в Devise / Doorkeeper

Минусы:

  • Не стандарт в Rails-мире, требует моста или Roda-приложения

Что НЕ использовать:

  • devise_token_authустаревший подход, хрупкий, не соответствует спецификации OAuth2
  • knock — заброшен, несовместим с современным Devise и Rails
  • doorkeeper с password grant — не рекомендуется, лучше authorization_code + PKCE

Рекомендованный стек:

  • doorkeeper + doorkeeper-jwt для токенов
  • Devise — если у тебя есть веб-интерфейс и нужна регистрация / сессии
  • Grape / Rails API-only mode — если API отдельно

Пример use-case (Client Credentials flow)

curl -X POST http://localhost:3000/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=abc123" \
-d "client_secret=xyz456"

Что такое OAuth2

OAuth2 — это протокол авторизации, позволяющий приложению (например, Zapier) получить ограниченный доступ к ресурсам пользователя на сервере (твоём API), без передачи пароля.


Основные роли

РольОписание
Resource OwnerПользователь, владеющий данными
ClientВнешнее приложение (например, Zapier)
Authorization ServerВыдаёт токены (Doorkeeper)
Resource ServerAPI, который отдаёт защищённые ресурсы

Стандартный OAuth2 Flow: Authorization Code Grant

1. Запрос на авторизацию (от клиента)

GET /oauth/authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://client.app/callback
&scope=zapier
&state=XYZ

Пользователю показывается логин-форма. После входа:

Ответ: 302 Found с редиректом:

Location: https://client.app/callback?code=AUTH_CODE&state=XYZ

2. Обмен AUTH CODE на ACCESS TOKEN

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://client.app/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET

Ответ: 200 OK

{
"access_token": "abc123",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "xyz789",
"scope": "zapier"
}

3. Запрос к API с Access Token

GET /api/zapier/test
Authorization: Bearer abc123

Ответ:

{ "success": true, "account_id": 42 }

Как это реализовано в Doorkeeper

1. Настройка doorkeeper.rb

Doorkeeper.configure do
orm :active_record

resource_owner_authenticator do
current_user || redirect_to(login_path)
end

resource_owner_from_credentials do |routes|
user = User.find_by(email: params[:username])
user if user&.valid_password?(params[:password])
end

access_token_expires_in 2.hours
use_refresh_token
end

2. Создание клиента

Doorkeeper::Application.create!(
name: "Zapier",
redirect_uri: "https://zapier.com/oauth/callback",
scopes: "zapier"
)

3. Протекция эндпоинтов

class Api::Zapier::ApplicationController < Api::ApplicationController
before_action -> { doorkeeper_authorize! :zapier }
end

4. Получение пользователя из токена

def authenticate!
return head :unauthorized unless doorkeeper_token&.accessible?

user = User.find_by(id: doorkeeper_token.resource_owner_id)
return head :unauthorized unless user

@authenticated_user = user
@account = user.account
end

Что важно помнить

  • authorization_code grant требует UI: сначала вход, потом редирект.
  • Токен всегда жёстко привязан к resource_owner_id.
  • В API не нужно знать ничего о client_id или пароле пользователя — только access_token.
  • doorkeeper_authorize! :scope_name — проверяет, что в токене указан нужный scope.