SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Auth0でJWT認証してみた話

スマートキャンプでボクシルのエンジニアをしている井上です。

本記事はスマートキャンプ Advent Calendar 2019 - Qiitaの20日目の記事です。

個人的に遊んでいるAuth0について書いてきます。

前回はAuth0でのよくある認証をAuth0 Nuxtで実装しましたが、 今回は前回の作成したものを使って、Auth0でJWT認証をやってみたいと思います。

前回の記事はこちら tech.smartcamp.co.jp

JWTとは

webbibouroku.com

Auth0を設定する

前回作成したAuth0の設定に加えてAPIを追加していきます。

Auth0にログインしたら下記のようにAPIに移動します。 f:id:smartcamp-inoue:20191220164132p:plain

移動後にAPI Createを押下すると、下記のような画面が表示されますので それぞれ、Nameなどを下記のように設定していきます。

Name: test-api
Identifier: https:/ test-api
Signing Algorithm: RS256

f:id:smartcamp-inoue:20191220164136p:plain

作成ボタンを押すとAPIが作られます! f:id:smartcamp-inoue:20191220164140p:plain

Rails側のJWTサンプルをダウンロードしよう

Auth0ではsampleの実装を公開していますので、それを使用して行います。 また、事前にプロジェクトを作ったことで.envに必要な設定が記載された状態で作成できます。

auth0.com

ダウンロードしたRails sampleを起動する

プロジェクト直下に移動してdocker環境を起動してみましょう!

docker build -t auth0-rubyonrails-api-rs256 .
docker run --env-file .env -p 3010:3010 -it auth0-rubyonrails-api-rs256

これでRailsアプリが立ち上がります! アクセスしてみると、下記のおなじみのRails画面が表示されるかと思います。 f:id:smartcamp-inoue:20191220142512p:plain

Rails側が何やってるのか一応触れる

PrivateController

今回、JWTリクエストを送るのは自動生成されたPrivateControllerのprivateアクションです。

ここにリクエストを送って、Hello from a private endpoint!のメッセージが帰ってくればJWT認証が成功となります。

# frozen_string_literal: true
class PrivateController < ActionController::API
  include Secured

  def private
    render json: { message: 'Hello from a private endpoint! You need to be authenticated to see this.' }
  end

  def private_scoped
    render json: { message: 'Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this.' }
  end
end

Secured module

JWTを受け取って、認証されているかどうか返す部分をSecuredというmoduleで作成しています。

これをcontroller側でincludeすることで、before_actionでJWT認証をおこなうようになっています。

# frozen_string_literal: true
module Secured
  extend ActiveSupport::Concern

  SCOPES = {
    '/api/private'    => nil,
    '/api/private-scoped' => ['read:messages']
  }

  included do
    before_action :authenticate_request!
  end

  private

  def authenticate_request!
    @auth_payload, @auth_header = auth_token

    render json: { errors: ['Insufficient scope'] }, status: :forbidden unless scope_included
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  def http_token
    if request.headers['Authorization'].present?
      request.headers['Authorization'].split(' ').last
    end
  end

  def auth_token
    JsonWebToken.verify(http_token)
  end

  def scope_included
    # The intersection of the scopes included in the given JWT and the ones in the SCOPES hash needed to access
    # the PATH_INFO, should contain at least one element
    if SCOPES[request.env['PATH_INFO']] == nil
      true
    else
      (String(@auth_payload['scope']).split(' ') & (SCOPES[request.env['PATH_INFO']])).any?
    end
  end
end

JsonWebToken Class

そして、実際にJWTの認証をしてるのは下記のJsonWebTokenです。 ここで、decodeする際にAuth0のdomainなどを使用してます

# frozen_string_literal: true
require 'net/http'
require 'uri'

class JsonWebToken
  def self.verify(token)
    JWT.decode(token, nil,
               true, # Verify the signature of this token
               algorithm: 'RS256',
               iss: "https://#{Rails.application.secrets.auth0_domain}/",
               verify_iss: true,
               aud: Rails.application.secrets.auth0_api_audience,
               verify_aud: true) do |header|
      jwks_hash[header['kid']]
    end
  end

  def self.jwks_hash
    jwks_raw = Net::HTTP.get URI("https://#{Rails.application.secrets.auth0_domain}/.well-known/jwks.json")
    jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
    Hash[
      jwks_keys
      .map do |k|
        [
          k['kid'],
          OpenSSL::X509::Certificate.new(
            Base64.decode64(k['x5c'].first)
          ).public_key
        ]
      end
    ]
  end
end

Nuxt側にJWTの設定を追加しよう

続いてNuxt側に設定を追加していきます。

設定は先ほどnuxt.config.jsに追加した, auth部分に下記のように追加するのみです。

//nuxt.config.js

  auth: {
    strategies: {
        auth0: {
          domain: 'Domain',  
          client_id: 'Client ID'  ,
        scope: ['openid', 'profile'], // 今回追加,
        response_type: 'id_token token',// 今回追加,
        token_key: 'id_token'// 今回追加,
        }
    },
    redirect: {
      login: '/login',
      logout: '/logout',
      callback: '/callback', 
      home: '/mypage', 
    },
  },

JWT認証をやるためaxiosのheaderにtokenを設定します。

また、ここは新規でplugins/axios.jsを作成します。

//plugins/axios.js

export default function({ $axios, store }) {
  $axios.interceptors.request.use(config => {
    config.headers.common['access-token'] =
      store.$auth.$storage._state['_token.auth0']
    config.headers.common['Access-Control-Allow-Origin'] = '*'
    config.headers.common['access-token'] =
      store.$auth.$storage._state['_token.auth0']
    return config
  })
  $axios.onError(error => {
    console.log(error)
    console.log('opps')
  })
}

また、nuxt.configのpluginsにも設定を追加します

//nuxt.config.js

  plugins: [
    '@/plugins/element-ui',
    '@/plugins/axios', // 今回追加
  ],

mypage.vueをまるっと下記に書き換えます。 もはや, mypageとは?という状態ですが遊びなので許してください

//mypage.vue
<template>
  <el-container>
    <el-form>
      <el-button type="primary" @click="jwtRequest"
        >JWTを試す
      </el-button>
    </el-form>
  </el-container>
</template>

<script>
export default {
  methods: {
    jwtRequest: function() {
      this.$axios
      .get('http://dockerのip:3010/api/private')
      .then(response => {
        confirm(response.data.message)
      })
    }
  }
}
</script>

JWTで認証を試してみる

ログインする

ログインボタンを押下して、下記の画面が表示されるかと思いますので、 Google認証でログインしましょう! f:id:smartcamp-inoue:20191220172404p:plain

JWTリクエストする

/mypageに移動しJWTを試すボタンを押下 f:id:smartcamp-inoue:20191220173056p:plain

認証に成功すると、Rails側からJWT認証が通ったとメッセージが帰ってきます

f:id:smartcamp-inoue:20191220173038p:plain

まとめ

Auth0でJWTはサンプルもあることで、かなりか簡単に試せるようになってますね!

auth0ようのmoduleもあったりなど、今後より使いやすくなっていくのが楽しみですね