diff --git a/php/laravel/api/composer.json b/php/laravel/api/composer.json index 0981f5f..546797c 100644 --- a/php/laravel/api/composer.json +++ b/php/laravel/api/composer.json @@ -10,7 +10,7 @@ "laravel/framework": "^9.2", "laravel/sanctum": "^2.14.1", "laravel/tinker": "^2.7", - "saasus-platform/saasus-sdk-php": "v1.1.0" + "saasus-platform/saasus-sdk-php": "1.2.0" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/php/laravel/api/composer.lock b/php/laravel/api/composer.lock index b7499e6..c78ae9c 100644 --- a/php/laravel/api/composer.lock +++ b/php/laravel/api/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9ba99ffbbc5805b4cfc55d540334873b", + "content-hash": "a6612d538413c5973b0038d881fa5872", "packages": [ { "name": "brick/math", @@ -3659,16 +3659,16 @@ }, { "name": "saasus-platform/saasus-sdk-php", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/saasus-platform/saasus-sdk-php.git", - "reference": "c51bb0d0ad7806e310ef90293690002f7d583179" + "reference": "933914440ef68575a0dd842af83401494fad89c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/saasus-platform/saasus-sdk-php/zipball/c51bb0d0ad7806e310ef90293690002f7d583179", - "reference": "c51bb0d0ad7806e310ef90293690002f7d583179", + "url": "https://api.github.com/repos/saasus-platform/saasus-sdk-php/zipball/933914440ef68575a0dd842af83401494fad89c3", + "reference": "933914440ef68575a0dd842af83401494fad89c3", "shasum": "" }, "require": { @@ -3717,10 +3717,10 @@ ], "description": "SaaSus SDK for PHP", "support": { - "source": "https://github.com/saasus-platform/saasus-sdk-php/tree/v1.1.0", + "source": "https://github.com/saasus-platform/saasus-sdk-php/tree/v1.2.0", "issues": "https://github.com/saasus-platform/saasus-sdk-php/issues" }, - "time": "2023-02-12T06:23:39+00:00" + "time": "2023-03-17T11:40:59+00:00" }, { "name": "symfony/console", diff --git a/php/laravel/api/routes/api.php b/php/laravel/api/routes/api.php index 370df81..4fbfeb9 100644 --- a/php/laravel/api/routes/api.php +++ b/php/laravel/api/routes/api.php @@ -17,6 +17,7 @@ // 一時コードからIDトークンなどの認証情報を取得するコントローラを登録 Route::get('/callback', 'AntiPatternInc\Saasus\Laravel\Controllers\CallbackApiController@index'); +Route::get('/token/refresh', 'AntiPatternInc\Saasus\Laravel\Controllers\TokenRefreshApiController@index'); // SaaSus SDK標準のAuth Middlewareを利用する Route::middleware(\AntiPatternInc\Saasus\Laravel\Middleware\Auth::class)->group(function () { diff --git a/php/laravel/front/src/lib/axios.ts b/php/laravel/front/src/lib/axios.ts index 3308043..7252cb8 100644 --- a/php/laravel/front/src/lib/axios.ts +++ b/php/laravel/front/src/lib/axios.ts @@ -1,4 +1,103 @@ -import Axios from 'axios' +import Axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios' + +const sleep = (second: number) => + new Promise((resolve) => setTimeout(resolve, second * 1000)) + +type Jwt = { + [name: string]: string | number | boolean +} + +const decodeJwt = (token: string): Jwt => { + const base64Url = token.split('.')[1] + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') + return JSON.parse(decodeURIComponent(escape(window.atob(base64)))) as Jwt +} + +const getNewIdToken = async (): Promise => { + try { + const { data } = await Axios.get( + `${process.env.NEXT_PUBLIC_API_URL}/api/token/refresh`, + { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + withCredentials: true, + } + ) + if (!data?.id_token || !data?.access_token) { + throw new Error('failed to get new credentials') + } + return data.id_token as string + } catch (err) { + console.error(err) + throw new Error('failed to get new credentials') + } +} + +const onReqFulfilled = async (config: AxiosRequestConfig) => { + if (!config?.headers) { + return config + } + const authorizationHeader = config.headers.Authorization as string + const idToken = authorizationHeader?.split(' ')[1] + if (!idToken) { + return config + } + + let decodedIdToken: Jwt + try { + decodedIdToken = decodeJwt(idToken) + } catch (err) { + console.error(err) + // リダイレクト処理などを入れる + return + } + const expireDate = decodedIdToken['exp'] as number + const timestamp = parseInt(Date.now().toString().slice(0, 10)) + // トークンの有効期限が切れている場合は新しいトークンを取得する + if (expireDate <= timestamp) { + try { + const newIdToken = await getNewIdToken() + config.headers.Authorization = `Bearer ${newIdToken}` + window.localStorage.setItem('SaaSusIdToken', newIdToken) + // JWTを更新してすぐ使用すると、Token used before used エラーになるため。 + // ref: https://github.com/dgrijalva/jwt-go/issues/383 + await sleep(1) + return config + } catch (err) { + console.error(err) + // リダイレクト処理などを入れる + } + } + return config +} + +const onResFulfilled = (res: AxiosResponse) => res +const onResRejected = async (error: AxiosError) => { + if (!error?.response) { + return Promise.reject(error) + } + if (error?.response.status !== 401) { + return Promise.reject(error) + } + const config = error.config + if (!config.headers) { + return Promise.reject(error) + } + try { + const newIdToken = await getNewIdToken() + config.headers.Authorization = `Bearer ${newIdToken}` + window.localStorage.setItem('SaaSusIdToken', newIdToken) + // JWTを更新してすぐ使用すると、Token used before used エラーになるため。 + // ref: https://github.com/dgrijalva/jwt-go/issues/383 + await sleep(1) + return config + } catch (err) { + console.error(err) + // リダイレクト処理などを入れる + } + return +} const axios = Axios.create({ baseURL: process.env.NEXT_PUBLIC_API_URL, @@ -8,4 +107,7 @@ const axios = Axios.create({ withCredentials: true, }) +axios.interceptors.request.use(onReqFulfilled) +axios.interceptors.response.use(onResFulfilled, onResRejected) + export default axios diff --git a/php/laravel/front/src/pages/callback/index.tsx b/php/laravel/front/src/pages/callback/index.tsx index 38290c2..ea3889d 100644 --- a/php/laravel/front/src/pages/callback/index.tsx +++ b/php/laravel/front/src/pages/callback/index.tsx @@ -1,7 +1,7 @@ import Container from '@mui/material/Container' import { useRouter } from 'next/router' import { useEffect } from 'react' -import axios from '@/lib/axios' +import Axios from 'axios' const Callback = () => { const router = useRouter() @@ -9,7 +9,15 @@ const Callback = () => { const code = query.code as string const fetchAuthCredentials = async () => { - const res = await axios.get(`/api/callback?code=${code}`) + const res = await Axios.get( + `${process.env.NEXT_PUBLIC_API_URL}/api/callback?code=${code}`, + { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + withCredentials: true, + }, + ) // 渡ってきたJWTをLocal Storageに保存する const idToken = res.data.id_token as string localStorage.setItem('SaaSusIdToken', idToken)