RailsをAPIとして使用して、 Auth0の認証機能を実装しよう!!
はじめに
前回、ひとまずRailsアプリケーションにAoth0という認証機能を実装し、Renderというサービスを利用してデプロイまで成功したので、勢いに乗って、今度はAuth0をAPIで使用する実装に挑戦してみました
!!!
RailsにAuth0の認証機能を追加、ついでにRenderでデプロイしてみた
この仕組みを理解できたら、フロントをReactで、バックエンドでRailsをAPIとして使用する、今流行りのアプリケーションを作ることができそう🎶
「Auth0をRailsのAPIで使用する」って、結局どういうこと?簡単に説明!!
Auth0とは?
認証・認可プラットフォームの一つです。
Auth0を利用すると、自分で開発したアプリ側では特に特別なコーディングをすることなく、スライドボタンをオンにするだけでfacebookやGoogle、Xなど30種類以上のソーシャルアカウントで簡単サインインができます。複雑なOAuthの実装やトークンの管理などに煩わされることなく、機能の開発に専念することができます。
APIとしてのRailsの役割
RailsをAPIとして使用する際は、フロントエンド(ユーザーインターフェース)とは独立したバックエンド(データ処理や保存を行う部分)をRailsで構築します。
従来、Railsを使えばユーザーインターフェースからデータベースまで、アプリケーション全体を一つのフレームワークで開発できるのですが、フロントエンド技術の発展に伴い、Railsをバックエンド(API)としてのみ使用し、フロントエンドはReactなどの別の技術を使って開発するケースが増えてきました。
RailsをAPIとして使う場合、Railsはクライアント(フロントエンド)からのリクエストを受け取り、それに応じてデータベースからデータを取得したり、データを保存したりといった処理を行います。そして、処理の結果をJSONなどの形式でクライアントに返します。
RailsをAPIとして使うメリットって何?
RailsをAPIとして使うことで、フロントエンドとバックエンドを分離し、それぞれを独立して開発・運用することができるため、フロントエンドとバックエンドで異なる技術を使うことができ、より柔軟で拡張性の高いアプリケーション開発が可能になります。
また、モバイルアプリやデスクトップアプリなど、ウェブアプリ以外のクライアントにもデータを提供できるようになるというメリットもあります。
では、実際にRailsをAPIとして使用する時の開発手順と、リクエストとレスポンスを確認する作業を行なっていきます!!
開発の手順
Auth0を設定する
Auth0アカウントを作成する
アカウントがない場合は、Auth0のアカウントを作成します。
Auth0にAPIを作成する
- 左側のメニューバーからApplicationsの中からAPIsを選択し、
+ Create API
のボタンをクリックします。 - フォームに必要事項を入力していきます。
- Name:アプリケーションの名前(
Rails API app
などわかりやすいものにします) - Identifire:APIのエンドポイントURLを設定するのが良いようです(今回は
https://api-rails-auth-app
) - Signing Algorithm:デフォルトで
RS256
が入っているのでそのままで大丈夫です。-
RS256
は秘密鍵と公開鍵のペアを使用するため、Auth0アカウントの公開鍵に対してトークンを検証します
-
- Name:アプリケーションの名前(
Create
をクリックするとAuth0にAPIが作成されます。
- ダッシュボードから権限の設定などを行うことができます。
権限を使用すると、特定のアクセス トークンを使用してユーザーに代わってリソースにアクセスする方法を定義できます。
今回はread:messages
スコープを使用してmessages
ユーザーが管理者アクセス レベルを持っている場合はリソースへの読み取りアクセスを許可するよう設定します。
Rails側の環境構築をする
Railsアプリケーションを新規作成する
- api_rails_auth_appという名前でアプリケーションを作成します。
rails new api_rails_auth_app --api
今回はapiでのみ使用するため、お尻に--api
のオプションをつけます。
--api
オプションを使用すると、APIに特化した軽量なRailsアプリケーションを作成できます。
不要なミドルウェアやファイルが除外され、APIの開発に最適化された環境が整う一方、フルスタックのWebアプリケーションを開発する場合に必要な機能が自動生成されないため注意が必要です。
Rails側でAuth0認証を追加するための設定を行う
アクセストークンの検証に必要なjwtをGemとしてインストールする
ruby-jwtはSecured受信アクセストークンによる認証を必要とするエンドポイントを承認するために使用されます。
jwtを使うことでAuth0のアクセストークンを正しく検証し、APIエンドポイントを保護することができるようになります。
gem 'jwt'
bundle install
JWTとアクセストークンの検証
- Webアプリケーションでユーザーがログインすると、サーバはユーザーを識別するための情報を含んだ
アクセストークン
を発行します。 -
アクセストークン
は、ユーザーがログイン後に行うリクエストに含められ、サーバはこのトークンを確認することでユーザーを認証します。 - RailsアプリケーションでAuth0を使用する場合、Auth0がアクセストークンを発行します。
- このトークンは、JSON Web Token(JWT)という形式で作成されます。
- JWTは、トークンの内容を暗号化し、改ざんを防ぐために電子署名が付与されています。
- アプリケーション側では、ユーザーから受信したアクセストークンが正当なものであるかを検証する必要があります
- トークンの署名が正当であること(Auth0の秘密鍵で署名されていること)
- トークンが期限切れでないこと
- 対象のアプリケーション向けに発行されたトークンであること
- jwtを使うことで、トークンの復号と署名の検証、クレームの確認などを簡単に行うことができます。
- Securedモジュールを定義したコントローラに
before_action :authorize
を追加することで、アクセストークンによる認証が必要なエンドポイント(アクション)を指定できます。 - authorizeメソッドの中でjwtを使ってトークンの検証を行います。
- トークンの検証に成功した場合はリクエストを処理し、失敗した場合は適切なエラーレスポンスを返します。
- Securedモジュールを定義したコントローラに
Auth0Clientクラスを作成する
- app/libディレクトの配下に
Auth0Client
というクラスを作成します。
リクエストのヘッダーから取得した受信アクセストークンを検証するためのクラスになります。
アクセストークンの検証のながれ
Auth0が発行したアクセストークンについて、正当なものであるかをアプリケーション側で検証する必要があります。
- リクエストのヘッダーから、Authorizationという名前のヘッダーを探します。
- Authorizationヘッダーの値として設定されているアクセストークンを取り出します。
- アクセストークンをデコード(復号)します。
- デコードしたトークンの内容を検証します。
- トークンの署名が正当であるか(Auth0の秘密鍵で署名されているか)
- トークンが期限切れでないか
- 対象のアプリケーション向けに発行されたトークンであるか
app/libディレクトリの下にAuth0Clientクラスを定義することで、コントローラからトークンの検証機能を呼び出しやすくなり、コントローラのアクションでトークンの検証を行うことができます。
トークンの検証に成功した場合はリクエストを処理し、失敗した場合は適切なエラーレスポンスを返します。
# app/lib/auth0_client.rb
# frozen_string_literal: true
require 'jwt'
require 'net/http'
class Auth0Client
# Auth0 Client Objects
Error = Struct.new(:message, :status)
Response = Struct.new(:decoded_token, :error)
# Helper Functions
def self.domain_url
"https://#{Rails.configuration.auth0.domain}/"
end
def self.decode_token(token, jwks_hash)
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: Rails.configuration.auth0.audience,
verify_aud: true,
jwks: { keys: jwks_hash[:keys] }
})
end
def self.get_jwks
jwks_uri = URI("#{domain_url}.well-known/jwks.json")
Net::HTTP.get_response jwks_uri
end
# Token Validation
def self.validate_token(token)
jwks_response = get_jwks
unless jwks_response.is_a? Net::HTTPSuccess
error = Error.new(message: 'Unable to verify credentials', status: :internal_server_error)
return Response.new(nil, error)
end
jwks_hash = JSON.parse(jwks_response.body).deep_symbolize_keys
decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)
rescue JWT::VerificationError, JWT::DecodeError => e
error = Error.new('Bad credentials', :unauthorized)
Response.new(nil, error)
end
end
- config/application.rbを変更してlibフォルダ内のファイルを読み込む処理を追加します
# config/application.rb
module RailsAuth0
class Application < Rails::Application
<中略>
config.autoload_paths << "#{Rails.root}/lib"
end
end
Securedモジュールを定義する
app/controllers/concerns
配下にsecured.rb
ファイルを作成し、受信リクエストのAuthorizationヘッダーからアクセストークンを検索するSecuredモジュールを定義します。
Securedモジュールの役割
WebアプリケーションでAuth0を使用する場合、アクセストークンによる認証が必要なエンドポイント(APIのURL)にアクセスするにはリクエストに有効なアクセストークンを含める必要があります。
RailsでAPIを作成する場合、エンドポイントに対応するアクションはコントローラに定義します。
つまり、アクセストークンによる認証が必要なコントローラアクションでは、トークンの検証処理を行う必要があります。
Securedモジュールは、アクセストークンの検証処理を実装したモジュールで、各コントローラにインクルード(読み込み)することで、アクセストークンの検証機能が使えるようになります。
具体的なSecuredモジュールの処理
- リクエストのヘッダーからAuthorizationヘッダーを取得する
- Authorizationヘッダーの値からアクセストークンを取り出す
- アクセストークンをAuth0Clientクラスに渡して検証する
- トークンの検証結果に応じて、以下のような処理を行う
- 検証成功:リクエストを処理する
- 検証失敗:エラーレスポンスを返す
Securedモジュールをコントローラにインクルードし、アクションにbefore_action :authorize
を追加することで、そのコントローラアクションではアクセストークンの検証が行われるようになります。
# app/controllers/concerns/secured.rb
# frozen_string_literal: true
module Secured
extend ActiveSupport::Concern
REQUIRES_AUTHENTICATION = { message: 'Requires authentication' }.freeze
BAD_CREDENTIALS = {
message: 'Bad credentials'
}.freeze
MALFORMED_AUTHORIZATION_HEADER = {
error: 'invalid_request',
error_description: 'Authorization header value must follow this format: Bearer access-token',
message: 'Bad credentials'
}.freeze
def authorize
token = token_from_request
return if performed?
validation_response = Auth0Client.validate_token(token)
return unless (error = validation_response.error)
render json: { message: error.message }, status: error.status
end
private
def token_from_request
authorization_header_elements = request.headers['Authorization']&.split
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
unless authorization_header_elements.length == 2
render json: MALFORMED_AUTHORIZATION_HEADER, status: :unauthorized and return
end
scheme, token = authorization_header_elements
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
token
end
end
スコープの検証をするための記述をする
- Auth0Clientクラスに、アクセストークンが要求されたリソースへのアクセスに必要十分なスコープがトークンにあるかどうかを検証するメソッドを追加します。
# app/lib/auth0_client.rb
class Auth0Client
Token = Struct.new(:token) do
def validate_permissions(permissions)
# スコープの検証ロジック
required_permissions = Set.new permissions
scopes = token[0]['scope']
token_permissions = scopes.present? ? Set.new(scopes.split(" ")) : Set.new
required_permissions <= token_permissions
end
end
# ... 他のメソッド ...
end
アクセストークンのスコープとは
アクセストークンには、そのトークンが持つ権限を表す「スコープ」という情報が含まれています。
スコープは、トークンを使ってアクセスできるリソース(APIのエンドポイントなど)や、実行できる操作(読み取り、書き込みなど)を定義します。
例えば、read:messages
というスコープを持つトークンは、メッセージを読み取る権限を持っていますが、メッセージを書き込む権限は持っていません。
RailsアプリケーションでAPIを作成する場合、エンドポイントごとに必要なスコープを定義することがあります。
例えば、/api/private
エンドポイントにアクセスするには、read:private
スコープが必要だと定義したとします。この場合、/api/private
にアクセスするリクエストに含まれるアクセストークンがread:private
スコープを持っていない場合、アクセスを拒否する必要があります。
Auth0Clientクラスに定義したvalidate_permissionsメソッドによって、アクセストークンが特定のスコープを持っているかどうかを検証します。
- validate_permissionsメソッドに、必要なスコープの配列を渡す
- アクセストークンのペイロード(デコードされたトークンの中身)からスコープの情報を取り出す
- 必要なスコープがすべてトークンのスコープに含まれているかどうかを確認する
- すべてのスコープが含まれている場合はtrue、そうでない場合はfalseを返す
これにより、APIエンドポイントごとに必要なスコープを定義し、適切な権限を持つトークンでのみアクセスを許可することができます。
- 適切な権限なしでリソースを要求しようとした場合に適切なエラーメッセージを返すためのSecuredエラー定数を定義します。
呼び出しの戻り値を更新し、最後にトークンに適切な権限があるかどうかを確認する新しいメソッドを作成し、そうでない場合はエラーメッセージとともにステータスコードを返します。
secured.rbに下記のコードを追加します。
# app/controllers/concerns/secured.rb
# frozen_string_literal: true
module Secured
extend ActiveSupport::Concern
# ...
INSUFFICIENT_PERMISSIONS = {
error: 'insufficient_permissions',
error_description: 'The access token does not contain the required permissions',
message: 'Permission denied'
}.freeze
def authorize
token = token_from_request
return if performed?
validation_response = Auth0Client.validate_token(token)
@decoded_token = validation_response.decoded_token # ここを追加する
return unless (error = validation_response.error)
render json: { message: error.message }, status: error.status
end
def validate_permissions(permissions)
raise 'validate_permissions needs to be called with a block' unless block_given?
return yield if @decoded_token.validate_permissions(permissions)
render json: INSUFFICIENT_PERMISSIONS, status: :forbidden
end
private
# ...
Securedモジュールで定義されている処理を、各コントローラで呼び出せるようにする
- ApplicationControllerにSecuredモジュールをインクルードします。
ApplicationControllerでSecuredモジュールをインクルードすることで、ApplicationControllerを継承しているすべてのコントローラでSecuredモジュールの機能を使えるようになります。
class ApplicationController < ActionController::API
include Secured
end
コントローラを作成する
JWT付きのリクエストのみ許可する(認証を必要とする)privateのAPIコントローラを作成する
- app/controllersの配下にprivate_controller.rbファイルを作成します。
# app/controllers/private_controller.rb
# frozen_string_literal: true
class PrivateController < ApplicationController
def private
render json: 'Hello from a private endpoint! You need to be authenticated to see this.'
end
end
- authorizeメソッドを追加します
before_action :authorize
とすることで保護することができます。
アクセストークンに適切な権限があることを確認するには、private-scoped
アクション内でvalidate_permissions
メソッドを呼び出します。
# app/controllers/private_controller.rb
class PrivateController < ActionController::API
before_action :authorize
def private
# スコープの検証に成功した場合の処理
render json: { message: 'Hello from a private endpoint! You need to be authenticated to see this.' }
end
def private_scoped
validate_permissions ['read:messages'] do
# スコープの検証に成功した場合の処理
render json: { message: 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.' }
end
end
end
認証を必要とせずに、一般公開するpublicのAPIコントローラを作成する
app/controllersの配下にpublic_controller.rbファイルを作成します。
# app/controllers/public_controller.rb
# frozen_string_literal: true
class PublicController < ApplicationController
# こちらには authentication を使用するための`before_action :authorize`の記述は必要ありません
def public
render json: { message: "Hello from a public endpoint! You don't need to be authenticated to see this." }
end
end
作成したコントローラに対するルーティングを定義します
Rails.application.routes.draw do
get '/public', to: 'public#public'
get '/private', to: 'private#private'
end
Railsの開発サーバ起動
以上で Rails API の構築は完了です!!!
rails sでRailsの開発サーバを起動しましょう。
おなじみのRailsの初期画面が表示されればOKです🎶
APIコールを試す
一般公開用APIへアクセスする
ブラウザから http://localhost:3000/public へアクセスします。
正常にレスポンスが返ってきている場合
{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}
というメッセージがJSONで返ってきます。
curlコマンドでの確認はこちら。
$ curl http://localhost:3000/public
{"message":"Hello from a public endpoint! You don't need to be authenticated to see this."}
認可機能付きAPIへアクセスする
認証なしでアクセスしてみる
ブラウザから http://localhost:3000/private へアクセスします。
ブラウザからの単純なアクセスではJWTを付与できないので、エラーメッセージが表示されます。
{"message":"Requires authentication"}
curlコマンドでの確認はこちら。
HTTPレスポンスコードとして401(Unauthorized)が返ってきているのがわかります。
curl -v http://localhost:3000/private
* Trying [::1]:3000...
* Connected to localhost (::1) port 3000
> GET /private HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< x-frame-options: SAMEORIGIN
< x-xss-protection: 0
< x-content-type-options: nosniff
< x-permitted-cross-domain-policies: none
< referrer-policy: strict-origin-when-cross-origin
< content-type: application/json; charset=utf-8
< vary: Accept
< cache-control: no-cache
< x-request-id: 0d6cf71f-0503-4762-85bd-f5de7feec158
< x-runtime: 0.012781
< server-timing: start_processing.action_controller;dur=0.02, halted_callback.action_controller;dur=0.01, process_action.action_controller;dur=0.74
< Content-Length: 37
<
* Connection #0 to host localhost left intact
{"message":"Requires authentication"}
-v オプションをつける意味
-v
オプションは、curlコマンドでリクエストを送信する際に、リクエストとレスポンスのより詳細な情報(verbose output)を表示するために使用します。
- リクエストの詳細
- 使用するHTTPメソッド(GET, POST, など)
- リクエストのURL
- リクエストのヘッダー
- レスポンスの詳細
- HTTPステータスコード(200 OK, 404 Not Found, など)
- レスポンスのヘッダー
- レスポンスのボディ
-v
オプションをつけることで、APIのデバッグやトラブルシューティングに非常に役立ちます。
- APIの認証が正しく機能しているかどうかを確認する
認証が必要なエンドポイントにアクセスする際に、適切な認証ヘッダーが送信されているかどうか確認する - APIにアクセスしたときに期待した結果が返ってこない場合に問題の原因を特定する
認証ありでアクセスしてみる
curlでJWT付きのリクエストを送ってみます。
- access_token(JWT)を取得します
Auth0でAPIを作成すると自動的にM2M用のApplicationが作成されます。
Auth0の「Applications」画面からAPIの名前のApplicationの詳細を開き「Settings」から情報を取得します。
DOMAIN=dev-40o731wzgfxo3z3h.us.auth0.com
CLIENT_ID=a0jcUmirzvlN3nZJStvIK1dhbGUiBE8A
CLIENT_SECRET=*****************************
AUDIENCE=https://api-rails-auth-app # API作成時に指定した`Identifier`の部分です
curl --url https://${DOMAIN}/oauth/token \
--header 'content-type: application/json' \
--data "{\"client_id\":\"${CLIENT_ID}\",\"client_secret\":\"${CLIENT_SECRET}\",\"audience\":\"${AUDIENCE}\",\"grant_type\":\"client_credentials\"}"
- curl実行後のレスポンスで返ってきたaccess_tokenを添付してRailsへAPIコールを行います
curl http://localhost:3000/private \
> -H "authorization: Bearer ${ACCESS_TOKEN}"
Auth0のAPIを直接呼び出す手順
Auth0のトークンエンドポイント(通常はhttps://YOUR_DOMAIN/oauth/token
)に対して、POSTリクエストを送信します。
リクエストのボディに以下のパラメータを含めます:
`grant_type`: # トークンの取得方法を指定します。クライアント認証の場合は`client_credentials`を使用します
`client_id`: # Auth0ダッシュボードで作成したAPIのクライアントIDを指定します
`client_secret`: # Auth0ダッシュボードで作成したAPIのクライアントシークレットを指定します
`audience`: # APIのオーディエンス(識別子)を指定します。Auth0ダッシュボードのAPIの設定ページで確認できます。
リクエストを送信すると、Auth0からaccess_token
の値が返されます。
取得したアクセストークンを使用して、認証が必要なエンドポイントに対して、リクエストヘッダーにAuthorization: Bearer <access_token>
を含めたGETリクエストを送信します。
これらの方法を使用することで、フロントエンドを作成していない場合でも、Auth0からアクセストークンを取得し、APIの認証をテストすることができます。
うまくいかないこと
これでスッキリ、APIでAuth0の認証実装できた〜!!!
といくはずだったんですが。。。
Privateのほうは認証で401エラーになってしまいます・・・😭
Started GET "/private" for ::1 at 2024-05-25 23:47:28 +0900
ActiveRecord::SchemaMigration Load (0.9ms) SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by PrivateController#private as */*
Parameters: {"private"=>{}}
Filter chain halted as :authorize rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.3ms | ActiveRecord: 0.0ms | Allocations: 313)
Postmanやcurlを使って、アクセストークンを取得するところまではうまくいっていそうですが、authorizeフィルタ内で認証が失敗しているようです。
原因として考えられることとして
- リクエストに有効なアクセストークンが含まれていない
- アクセストークンが無効または期限切れになっている
- アクセストークンに必要な権限(スコープ)が不足している
など??
Auth0のサンプルAPIと比較したり、AIさんにも聞いてみたりといろいろやってみてますが、解決方法がみつからずです。
ちなみに、サンプルAPIを見ながら追加した記述は以下の箇所です。
- config/routes.rbに
private-scoped
のルーティングを追加
get '/private-scoped', to: 'private#private_scoped'
- config/application.rbに追加
config.auth0 = config_for(:auth0)
config_for(:auth0)は、config/auth0.ymlファイルのをに記載されている設定値を読み込み、環境ごとの設定値を取得します。
取得した設定値は、Rails.application.config.auth0に格納されます。
- config/auth0.ymlに追加
development:
domain: <%= ENV["AUTH0_DOMAIN"] %>
audience: <%= ENV["AUTH0_API_AUDIENCE"] %>
環境ごとのAuth0の設定値を定義するために使用されます。
Auth0のドメインとオーディエンスの値を環境変数から取得し、アプリケーションの設定に反映させることができます。
- .env ファイルを作成します
Gemfile にdotenv-rails gem
を追加しbundle install
します。
.envの中身を記述します。
AUTH0_DOMAIN=dev-40o731wzgfxo3z3h.us.auth0.com
AUTH0_AUDIENCE=https://api-rails-auth-app
今回の実装で学んだこと
便利機能をサクッと使おうと思っても、基礎がわかっていないとやはりスムーズにいかないです。
Web周りの技術書や記事を読んだりしながら、リクエストとレスポンスの流れなどについても勉強しておかなければとつくづく実感しました。
今後の展望
このAPIをDocker環境に移し、本番環境で動かすことが最終目標です!!!
そうすれば、チーム開発において開発環境の構築がスムーズに行くはず。
Privateの認証エラーと戦いつつ、そちらも勉強していきます💪
参考資料
Auth0公式ドキュメント
Rails 7 と Auth0 による認可付きAPIの構築
Webを支える技術 -HTTP、URI、HTML、そしてREST
Discussion