イチカラOAuthとOIDC理解#4 - アクセストークンを検証してみる
アクセストークンで利用するリソースサーバーを実際に作ってみよう
色んな本が出ているが初めてOAuthやOIDC(OpenID Connect)に触れる人にはアーキテクチャの世界観が掴みにくい。
リソースサーバーでアクセストークンを検証するとはどういうことなのか体験してみて理解するための備忘録。
注意事項
-
Dockerを利用するために本章ではRancher Desktopを使用する。
以下サイトよりインストール。
https://rancherdesktop.io/詳しいインストール手順は以下リンク先の手順「step3:Rancher Desktop のインストール」以降を参照。
https://dev.classmethod.jp/articles/migration-from-docker-desktop-to-rancher-desktop/
-
※ Rancher Desktopではなく、Docker Desktopを使いたい場合
Docker Desktopは条件に該当した場合のみ無料で利用できるように規約が変更になっている。
会社から貸与されたPCなどでDocker Desktopを使用すると有償ライセンス条件に引っかかる可能性あります。
このため、有償条件に該当する企業に所属している場合でDocker Desktopを使用したい場合は、自己学習として自宅のPCで実行することをおすすめします。Docker Desktop は、小規模なビジネス向け(従業員 250名未満、かつ収益 1 千万ドル未満)、個人利用、教育目的、非商用のオープンソースプロジェクトに対しては、無償提供が継続されます。 大規模なエンタープライズ向けのプロフェッショナル利用に対しては、有償サブスクリプションが必要です。
出典: https://matsuand.github.io/docs.docker.jp.onthefly/desktop/faqs/
アクセストークンの検証方法には二つの方法がある
-
手段1: 認可サーバー(AuthorizationSever)が提供しているイントロスペクションエンドポイントにリクエストを行い確認する。
-
手段2: 認可サーバー(AuthorizationSever)から取得した公開鍵を利用し、access_tokenの署名を検証して確認する。
Auth0には手段1のイントロスペクションエンドポイントが存在しないため、手段2の方法でaccess_tokenの正当性を確認する。
やることのイメージ
今までの手順で、React.jsとAuth0を利用し、クライアントアプリケーション(Relyng Party)と認可サーバー(OpenID Provider)の作成はできている。
本章ではこれまでの章で発行したaccess_token
でリクエストするリソースサーバーのAPIをRubyOnRailsで作成する。
API側ではリクエストしたaccess_tokenの検証を行なう。
ローカルの開発環境の用意
sample_api
の名前でフォルダを作成する
1.任意のディレクトリで、
Dockerfile
とdocker-compose.yml
ファイルを新規作成する
2.作成したフォルダをvisual studio codeで開き、- Dockerfile
FROM ruby:3.2.2
# yarnをinstallするためのリポジトリーを取得
# https://classic.yarnpkg.com/en/docs/install#debian-stable
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
# ライブラリをインストール
RUN apt-get update -qq && \
apt-get install -y build-essential libpq-dev nodejs postgresql-client yarn vim
ENV EDITOR=vim
WORKDIR /sample_api
# Docker内でGemfileに記載のライブラリをインストールする
COPY Gemfile /sample_api/Gemfile
COPY Gemfile.lock /sample_api/Gemfile.lock
RUN bundle install && bundle update
COPY . /sample_api
RUN bundle install && bundle update
RUN rm -f tmp/pids/server.pid
docker-compose.yml
version: '3'
services:
db:
image: postgis/postgis:13-master
volumes:
- postgres:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- PGPASSWORD=password
ports:
- "5433:5432"
web:
build: .
command: tail -f /dev/null
volumes:
- .:/sample_api
- bundle:/usr/local/bundle
- rails-cache:/sample_api/tmp/cache
ports:
- "3001:3001"
volumes:
postgres:
driver: local
bundle:
driver: local
rails-cache:
driver: local
- 実際の画像
3.PC上でDockerが起動していることを確認する
powershellなどコマンドラインでdocker ps
と打ったら応答が返ってくることを確認する。
C:\Users> docker ps
4.GemfileとGemfile.lockを作成する
- Gemfile
source 'https://rubygems.org'
gem 'rails', '7.1.1'
-
Gemfile.lock
は空のままでOK
ctrlキー
と@キー
でTERMINALを開く
5.-
コマンドラインで
docker-compose build
を実行する。
buildが始まり、正常終了したことを確認する
-
docker-compose up -d
をTERMINALに入力しenter
正常終了を確認する。 -
docker-compose ps -a
をTERMINALに入力しenter
docker-compose.ymlで定義したweb
と付けたサービスとdb
とつけたサービスが起動していることを確認する
-
docker-compose exec web bash
をTERMINALに入力しenter
webとつけたサービスの中に入り、シェルのbashを起動している
6.Railsアプリを作成する
- terminal上で以下を入力し、enter
docker-compose run --rm web rails new . --force --no-deps --database=postgresql --api
-
Dockerfile
の中身が書き換わった場合は、手順2で作成したDockerfileの内容に戻す。 -
/sample_api/config/database.yml
を修正
defaultの行の下に、host
とusername
とpassword
行を追加する。
default: &default
adapter: postgresql
encoding: unicode
host: db
username: postgres
password: password
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: sample_api_development
・・・(省略)
-
コマンドラインでもう一度
docker-compose build
を実行する。
※もし…省略failed to fetch anonymous token…省略
みたいなエラーが出たら、以下を実行後にdocker-compose build
を再実行する。# windowsの場合(Powershellの場合) set DOCKER_BUILDKIT=0 set COMPOSE_DOCKER_CLI_BUILD=0 # macの場合 export DOCKER_BUILDKIT=0 export COMPOSE_DOCKER_CLI_BUILD=0
# docker-compose buildの再実行 docker-compose build
※それでもerrorが出る場合は、docker loginを行なう。
docker login # その後はDocker Hubに登録してあるユーザーとパスワードを入力しEnter # ログインできたら、docker-compose buildの再実行 docker-compose build
-
docker-compose up -d
をTERMINALに入力しenter -
docker-compose ps -a
をTERMINALに入力しenter
docker-compose.ymlで定義したweb
と付けたサービスとdb
とつけたサービスが起動していることを確認する
-
docker-compose exec web bash
をTERMINALに入力しenter
webとつけたサービスの中に入り、シェルのbashを起動している
-
bash上で、
bundle install
を入力し、enter
-
bash上で、
bundle update
を入力し、enter
-
bash上で、
rails db:create
を入力し、enter -
bash上で、
rails s -b 0.0.0.0 -p 3001
を入力しenterすると、Railsサーバーが起動する。 -
ブラウザで
http://localhost:3001
と入力すると、Railsの起動画面が表示されることを確認する。
-
serverを停止したい場合は
ctrlキー
とcキー
でサーバーを終了できる。
ここまでやったこと
リソースサーバーの起動が成功しました。
続いて、リソースサーバーでAPIの作成、access_tokenの検証を行う実装を行っていく。
APIの作成
今回はAPIを作ることが目的ではなく、APIで受け取ったアクセストークンをどのように検証しているかを確認することが目的。
このためAPIの中身の実装には深くはフォーカスしない。
1-a.APIの作成
- bash上で
rails g controller profiles
を実行
- /sample_api/routes.rbファイルを開き、以下の定義を設定する。
root "profiles#index"
resources :profiles
-
/sample_api/app/controllers/profiles_controller.rb
を下記のように編集して保存。
class ProfilesController < ApplicationController
def index
render :json => {id: 123, address: "東京都タワーサンプルホテル1丁目3番地"}
end
end
1-b.作成したAPIの確認
-
ブラウザで
http://localhost:3001
と入力し、Enter
以下のようにJSONが表示されていることを確認できれば、APIが作成できた。
-
Postmanでも確認してみる。
レスポンスに指定したJSONが返却されていることを確認できる。
2.ライブラリのインストール
編集するファイル: Gemfile
-
access_token検証のためのライブラリ
gem "jwt"
を記載する -
ローカル開発環境で環境変数を扱いやすくするためのライブラリ
gem "dotenv-rails"
を記載する
source "https://rubygems.org"
ruby "3.2.2"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.1"
# Use postgresql as the database for Active Record
gem "pg", "~> 1.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
# gem "jbuilder"
# Use Redis adapter to run Action Cable in production
# gem "redis", ">= 4.0.1"
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
# gem "image_processing", "~> 1.2"
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible
# gem "rack-cors"
# https://github.com/jwt/ruby-jwt
gem "jwt"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ]
# https://github.com/bkeepers/dotenv
gem "dotenv-rails"
end
group :development do
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"
end
- bash上で
bundle install
を実行
3.環境変数の定義を追加
.envファイルを作成する
-
AUTH0_DOMAINには以下の値を入れる。
https://自分のAuth0のドメイン.us.auth0.com
= 以下画像の赤字の部分を指定。
-
AUTH0_AUDIENCEにはAuth0に登録したAPI情報の
http://localhost:3001
4.Auth0Client クラスを作成する
Create an Auth0Client classを参考にし、Auth0Clientには各APIで利用する共通のメソッドを定義する。
- 作成するファイル
app/lib/auth0_client.rb
# 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
# Auth0のドメイン
ENV.fetch("AUTH0_DOMAIN", nil)
end
# access_tokenのデコード
## JWT.decodeメソッドの中身は下記を参照
## https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/decode.rb
def self.decode_token(token, jwks_hash)
JWT.decode(token, nil, true, {
algorithm: 'RS256',
iss: domain_url,
verify_iss: true,
aud: ENV.fetch("AUTH0_AUDIENCE", nil),
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
# access_tokenの正当性を検証
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
# access_tokenの署名を検証とデコード処理
decoded_token = decode_token(token, jwks_hash)
Response.new(decoded_token, nil)
# 今回は理解促進のためライブラリruby-jwtのエラーをそのまま返す
# 補足:
# 通常はAPI呼び出し側に何が原因でトークン検証がエラーになったかの理由を教えるべきではないので、
# Bad Credentialのエラーメッセージを返したり、
# それさえせずに401ステータスでunauthorizedメッセージだけを返す方が望ましい
rescue JWT::VerificationError, JWT::DecodeError => e
error = Error.new(e.message, :unauthorized)
Response.new(nil, error)
end
end
5.アクセストークンの検証処理をハンドリングする共通処理を作成
Define a Secured concernを参考にし、Auth0Clientには各APIで利用する共通のメソッドを定義する。
- 作成するファイル
app/controllers/concerns/secured.rb
# frozen_string_literal: true
# app/controllers/concerns/secured.rb
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
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)
# access_tokenの検証結果がNGだった場合はエラーを返す
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
# リクエストヘッダーのAuthorization値を取得
def token_from_request
authorization_header_elements = request.headers['Authorization']&.split
# もしリクエストヘッダーにAuthorizationがない場合はエラーレスポンスを返す
render json: REQUIRES_AUTHENTICATION, status: :unauthorized and return unless authorization_header_elements
# もしリクエストヘッダーにAuthorizationの値の要素が2でなければ、エラーを返す
# 想定しているAuthorizationの値の形式はBearer access-tokenの2つの要素のため
unless authorization_header_elements.length == 2
render json: MALFORMED_AUTHORIZATION_HEADER,
status: :unauthorized and return
end
scheme, token = authorization_header_elements
# もしリクエストヘッダーにAuthorizationの値の最初の要素の値がBearerでなければエラーを返す
render json: BAD_CREDENTIALS, status: :unauthorized and return unless scheme.downcase == 'bearer'
token
end
end
6.APIでaccess_tokenの検証処理が行われるように定義する
- 編集するファイル
app/controllers/application_controller.rb
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
# # app/controllers/concerns/secured.rbをincludeする
include Secured
end
app/controllers/profiles_controller.rb
before_actionの定義を追加する
# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
# APIの処理が呼び出される前にaccess_tokenの検証処理を実行する
before_action :authorize
def index
render :json => {id: 123, address: "東京都タワーサンプルホテル1丁目3番地"}
end
end
これでaccess_tokenの検証処理まで作成が完了した!
access_tokenの検証が行われるかAPIを試してみる
正当なaccess_tokenの場合
APIにリクエストを行うと200ステータスで欲しい情報のレスポンスデータを取得できる。
正当でないaccess_tokenの場合
- 例:有効期限が切れているaccess_token(JWT)
- 例:Auth0の自身の環境の公開鍵に対応しない秘密鍵で署名されたaccess_token(JWT)
確認が終わりコンテナを停止したい場合
確認が終わりコンテナを停止したい場合は以下手順で行える。
- 起動しているDockerコンテナを確認する。
docker-compose ps
以下が表示される。
sample_api-db-1 省略
sample_api-web-1 省略
- 以下コマンドでRailsをDockerコンテナを終了する
docker compose down
- 起動しているDockerコンテナを確認し、起動しているコンテナがないことを確認する
docker-compose ps
参考
1: 今回作成したAPIのコード
今回作成したAPIのコードは以下レポジトリに置いてあります。
2: ruby-jwtのaccess_token検証処理
- ライブラリruby-jwtでaccess_tokenの検証(公開鍵での署名の検証等)のロジック部分は下記を参照。
3: ruby-jwtのaccess_token検証処理(Claimsの検証)
- ライブラリruby-jwtでaccess_tokenのClaims(expの値など)を検証しているロジック部分は下記を参照。
Discussion