Nuxt.jsとAuth0で学ぶ認証認可~つなぎ込み~
2021-03-10

前回で基本的なAPI通信の基礎はできたので、今回は認証付きのAPIアクセスをやってみる。 具体的には、Auth0からもらった公開鍵を使用して、nuxtから送られてきたjwtを検証する。

認証に何使うか?

通常Railsで認証を行う場合、deviseやらsorceryやらを使用することが多いです。 今回はSPAということもあり、別の選択肢が候補に上がります。firebaseを使うパターンもありますが、今回はKnockを使ってみたいと思います。

https://github.com/nsarno/knock

KnockはRails Apiモードに特化したステートレスなセッションを行いたい場合に選択肢に上がる事が多いです。トークンの管理は今回Auth0に任せたいので、そのあたりも相性が良さそうです。

RailsアプリにKnock入れる

Rails側にknockを入れていきます。環境変数も入れたいので、ついでにdotenvも導入します。

今回はKnockを採用しましたが、ruby-jwtというgemもあり、そちらのほうが更新頻度も多いのでそっちを使ったほうがいいかも https://github.com/jwt/ruby-jwt

Gemfile

gem "knock"
gem "dotenv-rails"

Knockを入れると、コマンドで設定ファイルを作成できるようになるので、下記生成する。

rails g knock:install

実行するとconfig/initializers/knock.rb が生成される。この設定ファイルを今回の要件に対応させるように書き換える。

参考 https://rwehresmann.medium.com/ruby-on-rails-auth0-knock-7f114ed87e4b

require "net/http"
require "knock/version"
require "knock/authenticable"

Knock.setup do |config|

  ## Expiration claim
  ## ----------------
  ##
  ## How long before a token is expired. If nil is provided, token will
  ## last forever.
  ##
  ## Default:
  # config.token_lifetime = 1.day

  ## Audience claim
  ## --------------
  ##
  ## Configure the audience claim to identify the recipients that the token
  ## is intended for.
  ##
  ## Default:
  config.token_audience = -> {ENV['AUTH0_CLIENT_ID']}

  ## If using Auth0, uncomment the line below
  # config.token_audience = -> { Rails.application.secrets.auth0_client_id }

  ## Signature algorithm
  ## -------------------
  ##
  ## Configure the algorithm used to encode the token
  ##
  ## Default:
  config.token_signature_algorithm = "RS256"

  jwks_raw = Net::HTTP.get URI(ENV['AUTH0_JWKS'])
  jwks_keys = Array(JSON.perse(jwks_raw)["keys"])

  ## Signature key
  ## -------------
  ##
  ## Configure the key used to sign tokens.
  ##
  ## Default:
  # config.token_secret_signature_key = -> { Rails.application.secrets.secret_key_base }

  ## If using Auth0, uncomment the line below
  # config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret }

  ## Public key
  ## ----------
  ##
  ## Configure the public key used to decode tokens, if required.
  ##
  ## Default:
  config.token_public_key = OpenSSL::X509::Certificate.new(Base64.decode64(jwks_keys[0]["x5c"].first)).public_key

  ## Exception Class
  ## ---------------
  ##
  ## Configure the exception to be used when user cannot be found.
  ##
  ## Default:
  # config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
end

AUTHO0_JWKSにはAuth0の公開鍵情報を取得できるurlを設定します。この公開鍵を用いて、JWTを検証します。

Userモデルを作る

Rails側にログインしたらユーザー情報を保存したいので、ユーザーモデルの作成を行います。 現状はemailアドレスとどんな媒体でログインしたかを知りたいので、emailとsubのカラムを追加します。それ以外でももし欲しい情報があればカラムを用意して保存できるようにすると良いでしょう。

rails g model User email:string sub:string
rails db:migrate

マイグレーションができたら、UserモデルにKnockの認証用のメソッドを定義していきます。

class User < ApplicationRecord
  def self.from_token_payload(payload)
    find_by(sub: payload["sub"]) || create!(sub: payload["sub"], email: payload['email'])
  end
end

Knockを適用した範囲のAPIでは、自動でfrom_token_payloadというメソッドが呼ばれる。このメソッドの第一引数に、JWTのペイロードが入ってくる。

また、適用したAPIの範囲と言っているように、APIにKnockを使用することを明示してあげます。 ApplicationControllerに入れておきましょう。

class ApplicationController < ActionController::API
  include Knock::Authenticable
end

ここまでできたら、認証付きのAPIコントローラーを設定します。今回はauth_controller.rbという名前にしました。

module Api
  module V1
    class AuthController < ApplicationController
      before_action :authenticate_user

      def index
        render json: {
          message: "ID : #{current_user.id}, SUB: #{current_user.sub} ",
        }
      end
    end
  end
end

ルーティングも変更。

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :hello
      resources :auth
    end
  end
end
before_action :authenticate_user

を行っているので、authコントローラーにだけauthorizeするようにしています。 この状態でauthにアクセスしても、401が返ってきます。

ポストマンやcurlを用いてBearerにid_tokenを入れて検証してみます。 戻り値として、authコントローラーで指定した形で値が取得できればOKです。

{"message":"ID : 6, SUB: google-oauth2|hogehogefugafuga "}

Nuxtから呼べるようにする

最後に、フロントエンド側から、この情報を取得できるようにしておきます。

id_rokenを検証にわたしてあげたいので、LocalStrageからid_tokenを抜き出すメソッドを定義します。

auth0.ts

getIdtoken () {
    return this.isAuthenticated() ? localStorage.getItem('idToken') : null
  }

ログインしていればローカルストレージからidTokenを取得し、そうでないならnullを返すようにします。

index.vueにも、auth用のボタンを置きましょう。 全体像はこうなりました。

index.vue

<template>
  <main>
    <ul>
      <li
        v-for="page in ['Options API', 'Class API', 'Composition API']"
        :key="page"
      >
        <nuxt-link
          :to="
            `/${page
              .toLowerCase()
              .split(' ')
              .join('-')}`
          "
        >
          With {{ page }}
        </nuxt-link>
      </li>
    </ul>
    <div v-if="!loggedIn()">
      <div>ログインしてください。</div>
      <nuxt-link class="button is-primary" to="/login">
        <span>login</span>
      </nuxt-link>
    </div>
    <div v-else>
      <div>{{ userInfo().nickname }}</div>
      <button @click="logout()">
        ログアウトする
      </button>
    </div>
    <div>
      <button @click="greet()">
        挨拶する
      </button>
    </div>
    <div>
      <button @click="auth()">
        authする
      </button>
    </div>
  </main>
</template>

<script lang="ts">
export default {
  methods: {
    loggedIn () {
      return this.$auth0.isAuthenticated()
    },
    userInfo () {
      const user = this.$auth0.getUserData()
      console.log(user)
      return user
    },
    logout () {
      this.$auth0.logOut()
      this.$router.replace('/')
    },
    async greet () {
      const ret = await this.$axios.$get('/api/v1/hello')
      console.log(ret)
    },
    async auth () {
      const data = await this.$axios.$get('/api/v1/auth', { headers: { Authorization: 'Bearer ' + this.$auth0.getIdToken() } })
      console.log(data)
    }
  }
}
</script>

この状態で、authボタンを押すと、console.logによってapi/v1/authにアクセスした結果のidとsubが返ってきています。

スクリーンショット 2021-03-10 025017.png

これで、フロントでAuth0を用いてログイン、バックエンドアプリでid_tokenを使った検証、検証データをフロントでっ取得するという流れができました。 本番で運用する際は、ログイン後の画面に関しては共通のヘッダーを設定したり、検証したデータでリソースの表示を行ったりなど、色々やることはありますが、仕組みがいい感じにわかったので今回はここまでにします。