Next.js(Vercel) + Rails + Auth0を使ってログイン機能を作る
Next.js(Vercel)でフロントエンド、Railsでバックエンドを用意する。結構やることが多かったのでメモ。
大きな流れ
なかなか全体像がつかみにくかった。今回取り組んでわかったことをまとめる。
ユーザーはNextからログインや登録を行い、Auth0に遷移する。
Auth0に遷移し、そこで認証を行う。認証が成功するとアクセストークンがJWTとして取得できるようになる
Next側でそのJWTを使ってRailsに情報を連携する
Rails側でJWTをパースし、問題なかったら処理を実行する
こういうことを行うためにこれから色々設定をする。
Auth0の設定
アプリケーションを作る
Reguler Web Applicationsを選択。アプリケーションを作ると、QuickStartでフレームワークごとの設定が案内されるので、原則そらのドキュメントに従う。
コールバックの設定
ローカル用と、本番のそれぞれを入力する。後述するSDKを用いればそのあたりのエンドポイントは勝手に作られる。
Next.js側の設定
Auth0 Next.js SDKをインストール
1npm install @auth0/nextjs-auth0
dotenvで環境変数を設定する
どう設定すれば良いのかはドキュメントに記載されてる。
SDK Clientを作る
サンプルをベースに少し改造
1// lib/auth0.js
2
3import { Auth0Client } from "@auth0/nextjs-auth0/server";
4
5export const auth0 = new Auth0Client({
6 domain: process.env.AUTH0_DOMAIN,
7 clientId: process.env.AUTH0_CLIENT_ID,
8 clientSecret: process.env.AUTH0_CLIENT_SECRET,
9 appBaseUrl: process.env.APP_BASE_URL,
10 secret: process.env.AUTH0_SECRET,
11
12 authorizationParameters: {
13 scope: process.env.AUTH0_SCOPE,
14 audience: process.env.AUTH0_AUDIENCE,
15 }
16 async onCallback(error, context, session) {ユーザー作成の処理}
17});
Middlewareの作成
このサンプルもドキュメントから。
1import type { NextRequest } from "next/server";
2import { auth0 } from "./lib/auth0";
3
4export async function middleware(request: NextRequest) {
5 return await auth0.middleware(request);
6}
7
8export const config = {
9 matcher: [
10 "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
11 ],
12};
ここまででログインやログアウトのエンドポイントが作られる。
ログインボタンの設置
1<a href="/auth/login">ログイン/新規登録</a>
ここでLinkなどを使うと先読みが入ってややこしくなるらしい。
Vercel側の設定
環境変数を設定する
設定した環境変数
AUTH0_CLIENT_ID
AUTH0_CLIENT_SECRET
AUTH0_SECRET
AUTH0_DOMAIN:https://を取り除かないとエラーがでる
AUTH0_AUDIENCE
AUTH0_SCOPE
APP_BASE_URL
最初環境変数の設定をミスっていてローカルでは問題ないのに本番でMIDDLEWARE_INVOCATION_FAILEDエラーが発生して詰まっていた。
Railsとの連携
Auth0APIの作成
Auth0のダッシュボードで作成。
Identifierと、ドメインの情報を記録する。これをRailsの設定に使う。
jwt gemの追加
1gem 'jwt'
Identifierと、ドメインの情報をクレデンシャルに追加
1EDITOR="vim" bundle exec rails credentials:edit --environment development
1auth0:
2 api_identifier: xxx
3 domain: xxx
ここでのdomainはhttps://を含まない
JWTの検証をするクラスを作成
このあたりもドキュメントのママ
1# app/lib/json_web_token.rb
2require 'net/http'
3require 'uri'
4
5class JsonWebToken
6 def self.verify(token)
7 JWT.decode(token, nil,
8 true, # Verify the signature of this token
9 algorithm: 'RS256',
10 iss: "https://#{Rails.application.credentials.auth0[:domain]}/",
11 verify_iss: true,
12 aud: Rails.application.credentials.auth0[:api_identifier],
13 verify_aud: true) do |header|
14 jwks_hash[header['kid']]
15 end
16 end
17
18 def self.jwks_hash
19 jwks_raw = Net::HTTP.get URI("https://#{Rails.application.credentials.auth0[:domain]}/.well-known/jwks.json")
20 jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
21 Hash[
22 jwks_keys
23 .map do |k|
24 [
25 k['kid'],
26 OpenSSL::X509::Certificate.new(
27 Base64.decode64(k['x5c'].first)
28 ).public_key
29 ]
30 end
31 ]
32 end
33end
認証周りのサービスクラスを作成
TokenVerificationを作り、認証してユーザーを返すAuthorizationServiceと、新規にユーザーを作成するUserRegistrationServiceを作る
1module TokenVerification
2 private
3
4 def http_token
5 if @headers["Authorization"].present?
6 @headers["Authorization"].split(" ").last
7 end
8 end
9
10 def verify_token
11 JsonWebToken.verify(http_token)
12 end
13end
1class AuthenticationService
2 include TokenVerification
3
4 def initialize(headers = {})
5 @headers = headers
6 end
7
8 def perform!
9 payload, _header = verify_token
10 User.find_by!(sub: payload["sub"])
11 end
12end
1class UserRegistrationService
2 include TokenVerification
3
4 def initialize(headers = {})
5 @headers = headers
6 end
7
8 def perform!
9 payload, _header = verify_token
10 User.find_or_create_by(sub: payload["sub"])
11 end
12end
認証用のコントローラーの作成
認証に成功したらユーザーを返す
1class SecuredController < ApplicationController
2 before_action :authorize_request
3
4 private
5
6 def authorize_request
7 @current_user = AuthenticationService.new(request.headers).perform!
8 rescue JWT::VerificationError, JWT::DecodeError
9 render json: { errors: [ "Not Authenticated" ] }, status: :unauthorized
10 end
11
12 def current_user
13 @current_user
14 end
15end
ユーザーの作成はUsersControllerに作成
1class UsersController < SecuredController
2 skip_before_action :authorize_request, only: [ :create ]
3
4 def create
5 user = UserRegistrationService.new(request.headers).perform!
6 if user.save
7 render json: user, status: :created
8 else
9 render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
10 end
11 end
12end
これでログインまわりの機能ができた