LoginSignup
2
3

More than 3 years have passed since last update.

GoogleのOpenIDを使ったログインの実装

Posted at

概要

OpenIDについて調べたので、実際にGoogleのOpenIDを利用してのログインの実装方法を調べました。

採用技術

実装

クライアント

クライアントは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をキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3