始めに
ポートフォリオを作成する際にログイン認証を実装すると思いますが、実装方法が色々あり何を選ぼうか悩むことはありませんか?
今回私はFirebase Authenticationを使用してTwitterのログイン認証を実装してみました!
そこでなぜFirebase Authenticationを選んでどんなふうに実装していったのかざっくりまとめ見ました!
1.Firebase Authentificationとは?
Firebase Authenticationはユーザー認証機能を提供し、ユーザ情報をクラウドで保存してくれる、Google運営のサービス。
Firebase Authenticationを使用することでtwitterやfacebook、メールアドレス・パスワード、Googleアカウントなど様々な方法で認証機能の実現を可能にできます。
2.なぜFirebase Authenticationを選んだのか?
Firebase Authentificationを選んだ理由を、2点挙げさせていただきます!
①ログイン機能を実装する方法で、外部サービスを選ぶのが最善の策だと考えたから。
②他の外部サービスとの比較から、Firebase Authentificationを選ぶのがいいと思ったから。
①ログイン機能を実装する方法で、外部サービスを選ぶのが最善の策だと考えたから。
※ここでは捉え方がそれぞれではあると思うので一意見の感想として捉えていただきたいです。
今回はRails APIモードでSPA認証を行う上で、外部サービスの認証機能を使用するのが最善の策という考えに至りました。
SPA認証ではセッションベースではなくトークンを用いた認証を用いるのが一般的であり、サーバサイドにユーザ情報を保存しないため、クライアント側にユーザー情報が含まれているトークンを保存することになります。セッションベースとトークンベースの認証についてこの記事を参考にしました!
しかし、クライアントサイド側で情報を保存(Local Storageに保存するなど)することはクロスサイトスクリプティング攻撃などの問題が生じる恐れがあります。
そこで外部サービスを使用し、トークン情報を保管する方法を選びました。
Firebase Authenificationは、ログインが成功すればJWT仕様のIDトークンを返却されるためクライアントサイド側で保存することなくSPA認証を実現できます。
②他の外部サービスとの比較から、今回はFirebase Authentificationを選ぶのがいいと思ったから
外部の認証機能を採用するにあたり、Auth0とFirebaseAuthで悩みました。そこでFirebase Authentificationを選んだポイントや外部サービスにはない特徴を挙げさせていただきます!
①電話認証以外は基本的に無料
個人的にできるものなら節約したかったのと電話認証を使う予定がなかったのでFirebase Authentificationを選びました!
②匿名認証ができる
今回は匿名認証は使用しませんでしたが、匿名認証に興味がある人向けに記載させていただきました!匿名認証とは端的に言えば、個人情報を入力せずにサービスを使用することができる仕組みのこと。
③利用する上でのハードルが低い
Firebaseを利用する際に必要なものがGoogleアカウントのみであり、フォーム入力や支払いに関する登録もせず利用できます。
3.Firebase Authentificationを使用して実装してみる
それではFirebase Authentificationを使用して、Twitter認証の流れを追っていきたいと思います!
今回はRails × React でFirebase AuthenticationのTwitter認証の流れを見ていきます!
認証機能の流れ
ざっくり認証機能の流れをまとめました!
- ログインボタンを押すと、Twitterのログイン画面に遷移する。(今回はポップアップモードを採用)
- 認証に成功したらユーザーの情報(IdTokenを含む)が返却される。
- 返却されたIdTokenをフロント側からバックエンドに送信
- 送信されたFirebase Id TokenをRails側で検証。送られてきたuser_idから新規登録か既存のユーザーかを特定する。
上記の流れをわかりやすく図にした記事があったので参考にさせていただきました!
Rails API×Firebase authの場合、Railsは何をすべきなのかを考えた【設計編】
以降各項目について説明させていただきます!
React(フロント側)の実装
今回はFirebaseの初期設定であるプロジェクトの作成などは省略させていただきます。
(下記の記事を参照しました)
【完全版】React 18.2のFirebase v9.10 Authentication(認証)を基礎からマスター
ログイン状態を保持
-
onAuthStateChanged
関数を使用しサインインしているユーザーを取得し、ログイン・ログアウト時の処理を設定。user情報を監視しています。 - もしuser情報がなければログアウトと認識し、user情報があればログインの状態を保持し続けます。
- user情報はどのコンポーネントでも使用する可能性を考慮し、
useContext
を使用しuserの情報をグローバルな状態で管理します。
import { createContext, useState, useEffect } from 'react'
import { onAuthStateChanged } from 'firebase/auth'
import { auth } from './firebase'
//
export const AuthContext = createContext('')
export const App = () => {
//
const [user, setUser] = useState({})
const handleLogin = (user) => {
setUser(user)
}
const handleLogout = () => {
setUser({})
}
useEffect(() => {
const unsubscribed = onAuthStateChanged(auth, (user) => {
if (user) {
handleLogin(user)
} else {
handleLogout()
}
})
//クリーンアップ処理
return ()=> {unsubscribed();}
}, [])
return (
<AuthContext.Provider value={[user]}>
<Component/>
</AuthContext.Provider>
)
}
ユーザ登録画面でツイッターボタンを作成
- 今回はMUIを使用し、ツイッターログインボタンを作成します。
- ログインボタンを押すとポップアップウィンドウでサインインする
signInWithPopup
を呼び出す。 - サインインしたuserの情報からIdTokenを取り出しバックエンドに送ります。
import { Button } from '@mui/material'
import axios from 'axios'
export const Login = () => {
//twitterログインの処理
const handleTwitterLogin = () => {
signInWithPopup(auth, provider)
.then(async (result) => {
const user = await result.user
const token = await user.getIdToken(true)
const config = { headers: { authorization: `Bearer ${token}` } }
axios.post("バックエンド側のpath",config)
})
.catch((error) => {
//失敗時に表示したい項目を記載
})
}
return (
<Button onClick={handleTwitterLogin}>
Twitterログイン
</Button>
)
}
2.Rails(バックエンド側)の実装
- フロント側から送られてきたIdTokenを検証します。
- Firebase Authntification SDKを使用すると、簡単にJWT使用で送られてくるIdTokenを検証できるが、Rubyはサポート対象外でした….。そのため、サードパーティのJWTライブラリを使用してIDトークンを確認する必要がある。
実際にRubyで実装されている記事がありましたので参考にさせていただきました!
RubyでFirebaseのidトークンを認証に使ってみる
Rails + Next.js + Firebase Authentication で認証付きアプリを作成する
今回はruby-jwt
ライブラリを使用します。
gem 'jwt'
- 次に、IDトークンのヘッダー、ペイロード、および署名を確認します。
IDトークンのヘッダーが次の制約に準拠していること
-
アルゴリズムが"RS256"であること
-
Google公開鍵から送られてくる中から、kidをキーに持つものと一致していること。
kidとは署名に使用するキーを識別する値
IDトークンのペイロードが次の制約に準拠していること
-
tokenの有効期限が切れていないこと
-
tokenの発行時が過去であること
-
audが自分のproject_idであること
-
署名の発行者が
"https://securetoken.google.com/<aud>"
である必要がある -
uidが空ではない文字列であること
複数の公開鍵からkidをキーに持つものを取得してJWTライブラリを使用して署名を確認
参照記事
Firebase ドキュメント
これらを踏まえてRails側で送られてきたidTokenの検証コードを記載していきます。
- 検証コードは使い回すことを考慮して、concern配下にファイルを作成します。
- 送られてきたトークンを検証していきます。
- まずはIdトークンが文字列かどうか確認後、一回目は検証を行わずjwt形式のトークンをデコードして署名に必要なkidを取得する。
- トークンの情報が先ほど挙げさせていただいたヘッダー、ペイロード、および署名の確認事項から正しいフォーマットであるかチェックを行う。
def authenticate_token
authenticate_with_http_token do |token, options|
return { data: verify_id_token(token) }
rescue
##エラー時の処理を記載
end
end
private
def verify_id_token(token)
raise 'エラーメッセージ' unless token.is_a?(String)
full_decoded_token = decode_jwt(token, false)
errors = validate(full_decoded_token)
raise #エラー時の処理
public_key = fetch_public_keys[full_decoded_token[:header]['kid']]
unless public_key
#トークンが正しい鍵から作られていない場合のエラー文を記載
end
#公開鍵から証明書を作成
certificate = OpenSSL::X509::Certificate.new(public_key)
#証明書を利用しトークンを再度デコード
decoded_token = decode_jwt(token, true, { algorithm: ALGORITHM, verify_iat: true }, certificate.public_key)
{ uid: decoded_token[:payload]['sub'], decoded_token: decoded_token }
end
def decode_jwt(token, verify, options = {}, key = nil)
begin
decoded_token = JWT.decode(token, key, verify, options)
rescue JWT::ExpiredSignature
#有効期限が切れている時のエラー
rescue JWT::InvalidIatError
#発行時が過去ではない場合のエラー
end
{ payload: decoded_token[0], header: decoded_token[1] }
end
def validate(json)
#トークンの情報が正しいフォーマットであるかチェックを行う
end
- payloadとheaderの中身はこんな感じです。
{
"name"=>"",
"picture"=>"",
"iss"=>"",
"aud"=>"",
"auth_time"=>,
"user_id"=>"",
"sub"=>"",
"iat"=>,
"exp"=>,
"firebase"=>{}
}
{"alg"=>"", "kid"=>"", "typ"=>""}
- 次にトークンが正しい鍵から作られているかを確認する
- 公開鍵をGoogleから取得後、複数の公開鍵からkidをキーに持つものを使用して証明書を作成
- 作成後証明書を使用してトークンを再度デコードする
ALGORITHM = 'RS256'.freeze
ISS_URL = 'https://securetoken.google.com/'.freeze
CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'.freeze
def authenticate_token
authenticate_with_http_token do |token, options|
return { data: verify_id_token(token) }
rescue
##エラー時の処理を記載
end
end
private
def verify_id_token(token)
raise 'エラーメッセージ' unless token.is_a?(String)
full_decoded_token = decode_jwt(token, false)
errors = validate(full_decoded_token)
raise #エラー時の処理
public_key = fetch_public_keys[full_decoded_token[:header]['kid']]
unless public_key
#トークンが正しい鍵から作られていない場合のエラー文を記載
end
#公開鍵から証明書を作成
certificate = OpenSSL::X509::Certificate.new(public_key)
#証明書を利用しトークンを再度デコード
decoded_token = decode_jwt(token, true, { algorithm: ALGORITHM, verify_iat: true }, certificate.public_key)
{ uid: decoded_token[:payload]['sub'], decoded_token: decoded_token }
end
def decode_jwt(token, verify, options = {}, key = nil)
begin
decoded_token = JWT.decode(token, key, verify, options)
rescue JWT::ExpiredSignature
#有効期限が切れている時のエラー
rescue JWT::InvalidIatError
#発行時が過去ではない場合のエラー
end
{ payload: decoded_token[0], header: decoded_token[1] }
end
#CERT_URLから証明書リストを取得
def fetch_public_keys
uri = URI.parse(CERT_URL)
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
res = https.start { https.get(uri.request_uri) }
data = JSON.parse(res.body)
return data unless data['error']
##エラー時のメッセージなどを記載
end
- idトークン認証が成功し復号できれば、uidからUserの新規登録か既存であるかを確認する。
module Api
module V1
class UsersController < ApplicationController
include FirebaseAuthConcern
before_action :set_auth, only: %i[create]
def create
create_user(@auth)
end
private
def create_user(auth)
render json: auth, status: :unauthorized and return unless auth[:data]
uid = auth[:data][:uid]
render json: { message: '登録済みです' } and return if User.find_by(uid: uid)
user = User.new(uid: uid)
if user.save
render json: { message: '登録しましました' }
else
render json: user.errors.messages, status: :unprocessable_entity
end
end
def set_auth
@auth = authenticate_token
end
end
end
end
4.最後に
最後までご覧いただきありがとうございました!この記事が誰かの役に立てれば幸いです!
4.参考文献
Next.js編 - Rails + Next.js + Firebase V9 Authentication で認証付きのCRUDアプリを作る
Rails編 - Rails + Next.js + Firebase V9 Authentication で認証付きのCRUDアプリを作る
Firebase ドキュメント
ruby-jwt
React(SPA)での認証についてまとめ
認証用トークン保存先の第4選択肢としての「Auth0」
RubyでFirebaseのidトークンを認証に使ってみる
Rails + Next.js + Firebase Authentication で認証付きアプリを作成する