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— устаревший подход, хрупкий, не соответствует спецификации OAuth2knock— заброшен, несовместим с современным Devise и Railsdoorkeeperс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 Server | API, который отдаёт защищённые ресурсы |
Стандартный 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_codegrant требует UI: сначала вход, потом редирект.- Токен всегда жёстко привязан к
resource_owner_id. - В API не нужно знать ничего о client_id или пароле пользователя — только access_token.
doorkeeper_authorize! :scope_name— проверяет, что в токене указан нужный scope.