1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React(Next.js)のStatic Exports設定で認証ロジックを実装する⑤

Posted at

株式会社 ONE WEDGE@Shankouです。
今回は最終回として、APIの呼び出し時に自動でTokenを組み込んだりとか、refreshIfExpired関数とか細かいところをやっていきます。
今回でこのシリーズもの?は終わりとなります。

JWT(JSON Web Token)

Tokenの設定を組み込む前に、JWT認証について少し話をしたいと思います。
JWT(JSON Web Token)というのは、認証に用いるトークンの種類となるわけですが、簡単に説明するとヘッダー, ペイロード, 署名の3つを繋げたトークンとなります。

まず最初に、トークンのタイプ、署名の方式(暗号方式)等をJSON形式で記載し、URLエンコードに掛けます。これがヘッダー部分になります。
次にトークンのID、認証の有効期間等をJSON形式で記載し、URLエンコードに掛けます。これがペイロード部分です。
最後に、この2つを「.(ドット)」で結合したものに対して署名(暗号化)をし、URLエンコードに掛けたものが署名部分となります。
これらを全てを「.(ドット)」で結合したものがJWTというものになります。

認証が必要なAPIを呼び出す際にはこのトークンを一緒に送付し、バックエンド側では署名部分を復号化、比較を行うことで改ざんを検知できるという仕組みです。
注意が必要なのは、これはあくまでも改ざんを検知するための仕組みであるため、中身をみることは容易(URLデコードをするだけ)であるという点です。

フロントからJWTを送付する際には、概ねAuthorizationヘッダに対し、Bearerスキームとして付与するのが一般的にです。
詳細は次の項で!

Bearer認証

Bearer認証は、HTTP認証の一種で、クライアントがサーバーにリソースへのアクセスを要求する際に、アクセストークンを使用して認証を行う方法です。
このトークンは、通常、ユーザーがログインした後にサーバーから発行され、特定の期間有効となり、サーバーにリソースを要求する際に、HTTPヘッダーにトークンを含めます。
具体的には、以下のような形式でトークンを付与します。

Authorization: Bearer <アクセストークン>

これを、第③回で作成したmyAxios.tsを改修して組み込んでいきます。
axiosインスタンスのinterceptorsを利用することで、全リクエストに同一の処理を注入することができます。

refreshIfExpired関数は、アクセストークンが存在しないか、または有効期限が切れている場合に、トークンを更新するための非同期関数です(次の項で作ります)。

src/libs/myAxios.ts
import axios, { AxiosInstance } from 'axios'
import { API_BASE_URL } from '@/config'
import { getAccessToken, refreshIfExpired } from '@/features/Auth/hooks/useAccessToken'

const createMyAxios = (): AxiosInstance => {
  const instance = axios.create({
    baseURL: API_BASE_URL,
    headers: {},
  })

  // 追加実装部分
  instance.interceptors.request.use(async (requestConfig) => {
    await refreshIfExpired()
    const accessToken = getAccessToken()

    // configを直接変更するのではなく、スプレッド構文を使用して新しいオブジェクトを作成
    return {
      ...requestConfig,
      headers: {
        ...requestConfig.headers,
        ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
      },
    }
  })

  return instance
}

const myAxios = createMyAxios()
export default myAxios

refreshIfExpired

これまで何度か出現しているこの関数を最後に実装していきたいと思います。
こちらの関数はJWTをデコードしていく必要があったので、ここまで引き伸ばすことになりました。
付随してもろもろの内部関数も実装していきます。
この内refresh関数については、先程のmyAxios.tsを使ってしまうと無限ループ処理となってしまうので、素のaxiosを用いてトークンの更新処理を行います。

src/features/Auth/hooks/useAccessToken.ts
import axios from 'axios'
import { useSyncExternalStore } from 'react'
import { API_BASE_URL } from '@/config'
import { getRefreshToken, setRefreshToken } from '@/features/Auth/hooks/useRefreshToken'
import { zPostLoginResponse } from '@/features/Auth/types/response/PostLoginResponse.type'

const EVENT_NAME = 'updateaccesstoken'
const subscribe = (callback: () => void) => {
  window.addEventListener(EVENT_NAME, callback)
  return () => {
    window.removeEventListener(EVENT_NAME, callback)
  }
}

export const ACCESS_TOKEN_KEY_NAME = 'accessToken'

export const getAccessToken = () => {
  if (typeof window !== 'undefined') {
    return window.sessionStorage.getItem(ACCESS_TOKEN_KEY_NAME) || ''
  }
  return ''
}

export const setAccessToken = (token: string) => {
  if (typeof window !== 'undefined') {
    window.sessionStorage.setItem(ACCESS_TOKEN_KEY_NAME, token)
    // AccessTokenの更新イベントを発火させる
    window.dispatchEvent(new Event(EVENT_NAME))
  }
}

export const useAccessToken = () => {
  const accessToken = useSyncExternalStore(
    subscribe,
    () => getAccessToken(),
    () => ''
  )

  return { accessToken, setAccessToken }
}

export const refreshIfExpired = async (): Promise<boolean> => {
  const accessToken = getAccessToken()
  if (!accessToken || shouldRefresh(accessToken)) {
    return refresh()
  }

  return true
}

const parseJwt = (token: string) => {
  const base64Url = token.split('.')[1]
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
  const jsonPayload = atob(base64)

  return JSON.parse(decodeURIComponent(jsonPayload))
}

const shouldRefresh = (accessToken: string): boolean => {
  try {
    const payload = parseJwt(accessToken)
    // 有効期限が現在の時刻から300秒(5分)未満であれば更新の必要性有りとする
    return payload.exp - Math.floor(Date.now() / 1000) < 300
  } catch (e) {
    // tokenが破損している場合も更新の必要性有りとする
    return true
  }
}

const refresh = async (): Promise<boolean> => {
  const refreshToken = getRefreshToken()

  if (!refreshToken) return false

  return axios
    .post(`${API_BASE_URL}/app/token/refresh`, {
      refreshToken,
    })
    .then((res) => {
      const response = zPostLoginResponse.parse(res.data)
      setAccessToken(response.accessToken)
      setRefreshToken(response.refreshToken)
      return true
    })
    .catch((e) => {
      console.error(e)
      setAccessToken('')
      setRefreshToken('')
      return false
    })
}

まとめ

これで、5回に渡って行ってきた解説は終了です。
Static Exportsに関する実装はまとまった情報があまり出てこない(たぶん需要が少ない、、、)ので、あれこれ調べ回ったことを自分の中で整理する意味でも、文章にまとめるのはよかったかなと思います。
何かここは違うよってことがあれば指摘いただけると幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?