概要
OpenIDについて調べたので、実際にGoogleのOpenIDを利用してのログインの実装方法を調べました。
採用技術
- Vue.js
- Ruby on Rails
- GoogleSignIn
実装
クライアント
クライアントはVue.jsで作っていきます。Vue.jsについての説明は省略します。
今回はImplicit Flowでのログインを行いたいので、GoogleSignInを利用します。実装方法はこちらで説明されています。
ただ、今回Vue.jsを利用するので、そのまま利用はできませんでした。
概要は以下のようなものです。index.htmlの<div id="app"></div>
にApp.jsが描画されると思ってください。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://apis.google.com/js/platform.js"></script>
<title>app</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
App.js
<template>
<div>
<div v-if="!signedIn" id="google-signin-button"></div>
<a href="#" @click="signOut" v-if="signedIn">Sign out</a>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
signedIn: false
}
},
mounted() {
this.renderSignInButton();
},
methods: {
renderSignInButton() {
gapi.load("auth2", (signin2) => {
gapi.auth2.init({
client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com'
scope: 'profile email',
hosted_domain: 'YOUR_DOMAIN' // ドメインを限定したい場合
});
gapi.signin2.render('google-signin-button', {
onsuccess: this.onSignIn,
})
});
},
onSignIn(googleUser) {
this.signedIn = true;
},
}
}
</script>
GoogleSignInの実装サンプルではclass="g-signin2"
としているところにボタンを描画してくれるんだと思います。ただ、App.jsの中身が描画されるのが間に合わないらしく、そのままではGoogleSignInボタンを表示してくれませんでした。なのでここを参考にmountedでボタンを描画してます。
API側実装
クライアントからAPIへリクエストするときはAuthorizationヘッダーでIDTokenを渡し、APIはそのIDTokenを検証することでリクエストの認証を行います。
以下では自分のプロフィール情報を取得するAPIを作成します。
クライアントからのリクエストはこんな感じ(apiはlocalhost:3000で起動しているものとします。)
var auth2 = gapi.auth2.getAuthInstance();
var idToken = auth2.currentUser.get().getAuthResponse().id_token;
fetch("http://localhost:3000/my/profile", {
headers: {
Authorization: `Bearer ${idToken}`
}
}).then((res) => {
return res.json();
}).then((json)=>{
console.log(json);
});
API側ではとりあえずapplication_controllerに認証処理を記述します。
認証処理
class ApplicationController < ActionController::API
before_action :verify_id_token
def verify_id_token
return false unless request.headers['Authorization'].present?
# ①IDTokenを取り出してデコード
id_token = request.headers['Authorization'].gsub(/Bearer /, '')
decoded_token = JWT.decode id_token, nil, false
# ②Googleから公開鍵情報を取得
res = Faraday.get('https://www.googleapis.com/oauth2/v3/certs')
keys = JSON.parse(res.body)['keys']
key = keys.find { |item| item['kid'] == decoded_token[1]['kid'] }
# ③公開鍵情報から公開鍵作成
exponential = OpenSSL::BN.new(Base64.urlsafe_decode64(key['e']), 2)
modulus = OpenSSL::BN.new(Base64.urlsafe_decode64(key['n']), 2)
public_key = OpenSSL::PKey::RSA.new.set_key(modulus, exponential, nil).public_key
# ④ruby-jwtでIDTokenを検証
raise JWT::VerificationError if decoded_token[0]['hd'] != 'YOUR_DOMAIN'
@id_token = JWT.decode id_token, public_key, true, aud: "YOUR_CLIENT_ID.apps.googleusercontent.com", iss: "accounts.google.com", verify_aud: true, verify_iss: true, algorithm: 'RS256'
rescue JWT::DecodeError => exception
# ログ出力などなど
end
def current_user
return unless @id_token
@current_user ||= User.find_or_create_by(google_user_id: @id_token[0]['sub'])
end
def authenticate!
render status: :forbidden unless current_user.present?
end
end
②Googleから公開鍵情報を取得
IDTokenを検証するための公開鍵を取得します。
公開鍵がどこにあるかというと、こちらで説明されています。以下のURLからOpenIDConnectの情報が取れるみたいです。
https://accounts.google.com/.well-known/openid-configuration
この/.well-known/openid-configuration
ですが、OpenIDConnectの仕様にも記載されているので、他のOpenIDプロバイダーを利用する際もこんなURLで公開されているんだと思います。
この情報から、公開鍵は以下のURLにあるとわかります。
https://www.googleapis.com/oauth2/v3/certs
④ruby-jwtでIDTokenを検証
基本的にはruby-jwtがいい感じで検証してくれます。
以下の3つは必ず検証するよう記載されていました。
- iss(accounts.google.com)
- aud(プロジェクトID)
- exp
expは特にコード上書いていませんが、ruby-jwtがチェックしてくれるみたいです。
また、ドメインを限定したい場合は、hd
にドメインが記載されているのでこちらもチェックすると良いと思います。
アクションに認証をかける
あとは必要なコントローラーで使うだけです。
app/controllers/my/profiles_controller.rb
class My::ProfilesController < ApplicationController
before_action :authenticate!
def show
render json: current_user
end
end
終わりに
上記コードは認証の概要を理解するためにかなり簡略なもので終わらせています。
公開鍵をキャッシュしたり、検証済みのIDTokenをキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。