背景
OAuth2.0の仕組みを触って体験したかったので、Github OAuthAppを使って実践してみることにした。
理解に間違えなどがあればご指摘いただけると幸いです。
OAuth2.0とは?
クライアントアプリ(以降:クライアント)がユーザーが登録しているGihubのユーザー名を取得したいとする。
当然、クライアントはユーザーのGithubへのアクセス権限がない為、OAuth2.0という技術を使って解決する。
方法は、クライアントがユーザーにGithubへの認可・認証をしてもらい、Githubの認可サーバからアクセストークンを発行してもらうことで、クライアントは一時的にユーザーが認可した範囲のGithub情報へアクセスすることができるようになる。
尚、OAuth2.0のフローには4つの種類があり、当記事は認可コードを使ってアクセストークンを取得するフローで検証していく。
前提
・Githubアカウントを持っていること
・rails new でプロジェクトが作成されていること
※当記事はOAuth2.0の体験用に最小限かつ簡易的に作ったものです。ご参考にしていただける場合はセキュリティ面に特にご注意いただき自己責任でお願いします。
環境
Mac : Apple M2
rails "7.1.3.2"
ruby "3.2.3"
事前準備
①Github OAuthAppを作成して、各種設定とClient IDとClient secretsを取得しておく
Githubへログインして、画面右上のアイコンをクリック後
Settings > Developer settings > OAuth Apps > New OAuth App
のように遷移すると以下の登録ページに辿り着くため、各項目を任意で設定しRegister application
ボタンを押下
登録後は Client ID と Client secrets が発行される
(機密性が非常に高い情報のため、実装や管理など取り扱い注意!!!)
注意事項
GithubでOAuthAppを作成するためのベストプラクティスでは、OAuthAppではなくGithubAppを使うことが推奨されている。
可能な場合、OAuth app の代わりに GitHub App を使用することを検討してください。 通常、GitHub Apps が OAuth apps より優先されます。 GitHub Apps では、きめ細かいアクセス許可が使われ、アプリでアクセスできるリポジトリをより細かく制御でき、有効期間の短いトークンが使われます。 これらの特徴により、アプリの資格情報が漏洩した場合に発生するおそれがある損害を制限することで、アプリのセキュリティを強化できます。
引用元:OAuth アプリを作成するためのベスト プラクティス
今回は体験のためOAuthAppを使用する
②Faradayをインストールする
APIでGithubのユーザー情報を取得する際、HTTPクライアントにFaradayを使う
# Gemfile
gem 'faraday'
bundle install
③Userモデルを作成する
rails g model User uid:integer name:string
rails db:migrate
実装していく
※Clien ID、Client_secretsはハードコーディングしない!!
①Github認可ページへ誘導する画面を作る
コントローラーとindexファイルを作成
rails g controller welcome index
ルートにしておく
Rails.application.routes.draw do
root 'welcome#index'
end
indexファイルは、セッションを使ってページ表示を切り替えられるようにしておく
<% if current_user %>
<h1>
<%= "あなたのGithubユーザー名は #{current_user.name} です!" %>
</h1>
<%= button_to 'サインアウト', signout_path, method: :delete %>
<% else %>
<h1>Welcome!!</h1>
<%= link_to 'Githubとの連携を許可する', github_auth_path %>
<% end %>
②リンクを押下したらGithub認可ページへ遷移させる
rails g controller Sessions
Github OAuthAppで作成したリダイレクトURIのパスを基に、routes.rbに追記していく
Rails.application.routes.draw do
root 'welcome#index'
get 'github/auth', to: 'sessions#github_auth'
# as: 'github_callback'はredirect_uriに設定するエイリアス
get 'リダイレクトURIで設定したパス', to: 'sessions#github_callback', as: 'github_callback'
delete 'signout', to: 'sessions#destroy', as: 'signout'
end
indexファイルでセッションの有無によってページ表示を切り替えるためヘルパーメソッドを作る
module SessionsHelper
def current_user
@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
@current_user
end
end
module ApplicationHelper
include SessionsHelper
end
このタイミングでサーバを起動すると以下の画面になる
def github_auth
redirect_uri = github_callback_url # routes.rbで作成したエイリアス
scope = 'user' # 認可範囲の指定
state = SecureRandom.hex(16) # 認可サーバからのレスポンスにおける正当化を判別する値を作成
session[:oauth_state] = state # セッションにstate値を入れておいてレスポンス取得時に比較する
# リンクを押下された際のリダイレクト先
redirect_to "https://github.com/login/oauth/authorize?client_id=#{'Client ID'}&redirect_uri=#{redirect_uri}&scope=#{scope}&state=#{state}",
allow_other_host: true
end
redirect_uri
ユーザーが認可・認証を終えた後に、認可サーバが認可コードやアクセストークンを送り返す先のURI。
scope
クライアントがリクエストする認可範囲の指定。
ユーザーは、認可時にクライアントがリクエストするscope範囲を許可するか拒否するかを選択をし、承諾した場合、クライアントは認可サーバからscope範囲のトークンを取得できる(厳密には、認可コードと引き換えにscope範囲のアクセストークンを取得する)
つまり、このscopeは'user'を指定しているため、プロファイル情報の読み書きを可能とするためのscopeとなる。
state
OAuth2.0におけるCSRF(cross site request forgeries)対策として推奨される値。
クライアントで任意の整数を発行しリクエスト時のパラメータで送ることで、認可サーバはレスポンスに受取ったstate値を含めて返すことにより、クライアント側でstate値を比較し通信の正当化を判別することができる。
allow_other_host: true
Railsはデフォルトで、リダイレクト先が同じホストであることを求めている。
今回のリダイレクト先のホストはhttps://github.com
のように異なるホストとなっているため、この状況を許可するためのオプションとなる。
③認可コードと引き換えにアクセストークンを取得しユーザー名を取得する
ユーザーが認可・認証を終えると、Githubの認可サーバは設定されたリダイレクトURIに向けて、認可コードを含むパラメーターを送る。(この際、ブラウザを経由して送られる)
# github_callbackアクションの前にFaradayを初期化する
before_action :client, only: %i[github_callback]
def github_callback
code = params[:code] # 認可コード
state = params[:state] # リクエスト時に送ったstate値
redirect_uri = github_callback_url # アクセストークンを取得するリクエスト時に使用
# state値を比較し異なる場合はセッションデータを削除
if state != session[:oauth_state]
session.delete(:oauth_state)
return redirect_to root_url
end
# アクセストークンを取得
access_token = fetch_access_token(code, redirect_uri)
# Githubのユーザー情報をapi取得
user_data = fetch_user_data(access_token)
# ユーザー名をマッピング保存
user = find_or_create_user(user_data)
if user
session[:user_id] = user.id
redirect_to root_url
end
# セッションの削除
def destroy
session[:user_id] = nil
redirect_to root_url
end
end
private
# Faradayを初期化
def client
@client = Faraday.new do |f|
f.ssl.verify = true
f.request :url_encoded
f.adapter Faraday.default_adapter
end
end
def fetch_access_token(code, redirect_uri)
body = {
client_id: # Client ID,
client_secret: # Client secrets,
code: code,
redirect_uri: redirect_uri,
}
headers = { 'Accept' => 'application/json' }
response = @client.post('https://github.com/login/oauth/access_token', body, headers)
JSON.parse(response.body)['access_token']
end
def fetch_user_data(access_token)
headers = { 'Authorization' => "token #{access_token}"}
user_data = @client.get 'https://api.github.com/user', nil, headers
JSON.parse(user_data.body)
end
def find_or_create_user(user_data)
user = User.find_or_create_by(uid: user_data['id'])
user.update(name: user_data['login']) if
user_data['login'].present?
user
end
画面上で試してみる
Githubのユーザー名が表示されていれば成功です!
参考記事