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

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

# 採用技術
* Vue.js
* Ruby on Rails
* [GoogleSignIn](https://developers.google.com/identity)

# 実装
## クライアント
クライアントはVue.jsで作っていきます。Vue.jsについての説明は省略します。
今回はImplicit Flowでのログインを行いたいので、GoogleSignInを利用します。実装方法は[こちら](https://developers.google.com/identity/sign-in/web)で説明されています。
ただ、今回Vue.jsを利用するので、そのまま利用はできませんでした。
概要は以下のようなものです。index.htmlの`<div id="app"></div>`にApp.jsが描画されると思ってください。

index.html

```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

```vuejs
<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ボタンを表示してくれませんでした。なので[ここ](https://developers.google.com/identity/sign-in/web/reference#gapisignin2renderid_options)を参考にmountedでボタンを描画してます。




## API側実装
クライアントからAPIへリクエストするときはAuthorizationヘッダーでIDTokenを渡し、APIはそのIDTokenを検証することでリクエストの認証を行います。
以下では自分のプロフィール情報を取得するAPIを作成します。

クライアントからのリクエストはこんな感じ(apiはlocalhost:3000で起動しているものとします。）

```js
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に認証処理を記述します。

### 認証処理

```ruby
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を検証するための公開鍵を取得します。
公開鍵がどこにあるかというと、[こちら](https://developers.google.com/identity/protocols/OpenIDConnect#discovery)で説明されています。以下のURLからOpenIDConnectの情報が取れるみたいです。

```
https://accounts.google.com/.well-known/openid-configuration
```

この`/.well-known/openid-configuration`ですが、[OpenIDConnectの仕様](https://openid.net/specs/openid-connect-discovery-1_0.html)にも記載されているので、他のOpenIDプロバイダーを利用する際もこんなURLで公開されているんだと思います。

この情報から、公開鍵は以下のURLにあるとわかります。

```
https://www.googleapis.com/oauth2/v3/certs
```

#### ④ruby-jwtでIDTokenを検証
基本的には[ruby-jwt](https://github.com/jwt/ruby-jwt)がいい感じで検証してくれます。
以下の3つは必ず検証するよう記載されていました。

* iss(accounts.google.com)
* aud(プロジェクトID）
* exp

expは特にコード上書いていませんが、ruby-jwtがチェックしてくれるみたいです。
また、ドメインを限定したい場合は、`hd`にドメインが記載されているのでこちらもチェックすると良いと思います。


### アクションに認証をかける
あとは必要なコントローラーで使うだけです。

app/controllers/my/profiles_controller.rb

```ruby
class My::ProfilesController < ApplicationController
  before_action :authenticate!
  def show
    render json: current_user
  end
end
```

# 終わりに
上記コードは認証の概要を理解するためにかなり簡略なもので終わらせています。
公開鍵をキャッシュしたり、検証済みのIDTokenをキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。