React SPA 連動
React シングルページアプリケーションと Tinyauth 連動する
このガイドは React SPA(シングルページアプリケーション)から tinyauthを OIDC プロバイダとして使用して認証を実装する方法について説明します。 SPAはクライアント側のコードでシークレットを安全に保持できないため、 パブリッククライアント パターンと PKCEを使用します。
事前準備
1. Tinyauth クライアント登録
tinyauthの config.yamlに React SPAを公開クライアントとして登録します。 client_secretは省略します。
# config.yaml
clients:
- id: react-spa
name: My React App
client_id: react-spa-client
redirect_uris:
- http://localhost:3001/callback
response_types:
- code
grant_types:
- authorization_code
- refresh_token
scope: openid profile email
2. 環境変数の設定
Viteベース React プロジェクトの .env ファイルに次の環境変数を設定します。
VITE_OIDC_ISSUER=http://localhost:8080
VITE_OIDC_CLIENT_ID=react-spa-client
VITE_OIDC_REDIRECT_URI=http://localhost:3001/callback
VITE_OIDC_SCOPE=openid profile email
認証フローの概要
React SPA パブリッククライアントの認証フローは次のとおりです。
- ユーザーがログインボタンをクリック
- ブラウザで PKCE ペア、state、nonceを作成してsessionStorageに保存する
- ユーザー
tinyauth認証ページにリダイレクト - 認証完了後
/callbackルートロコールバック - ブラウザから直接 認証コードをトークンに交換する(client_secretなしで、 PKCEとしてセキュリティ)
- トークンを localStorage に保存
- ユーザーをプロフィールページに移動
実装ガイド
OIDC Discovery
アプリ初期化時 OIDC Discoveryエンドポイントから設定を取得します。
// libs/oidc-client.ts
let oidcConfig: OIDCConfig | null = null;
export async function initializeOIDCConfig() {
const issuer = import.meta.env.VITE_OIDC_ISSUER;
const res = await fetch(`${issuer}/.well-known/openid-configuration`);
const discovery = await res.json();
oidcConfig = {
authorizationEndpoint: discovery.authorization_endpoint,
tokenEndpoint: discovery.token_endpoint,
userinfoEndpoint: discovery.userinfo_endpoint,
clientId: import.meta.env.VITE_OIDC_CLIENT_ID,
redirectUri: import.meta.env.VITE_OIDC_REDIRECT_URI,
scope: import.meta.env.VITE_OIDC_SCOPE,
};
}
ログインハンドラ
// routes/index.tsx
async function handleLogin() {
const { codeVerifier, codeChallenge } = await generatePKCE();
const state = crypto.randomUUID();
const nonce = crypto.randomUUID();
// state와 code_verifier를 sessionStorage에 저장
sessionStorage.setItem('oidc_auth_state', JSON.stringify({
state, codeVerifier, nonce,
}));
const params = new URLSearchParams({
client_id: oidcConfig.clientId,
redirect_uri: oidcConfig.redirectUri,
response_type: 'code',
scope: oidcConfig.scope,
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
});
// tinyauth 인증 페이지로 리다이렉트
window.location.href =
`${oidcConfig.authorizationEndpoint}?${params}`;
}
コールバック処理
認証コードをトークンに交換します。パブリッククライアントなので client_secret なし code_verifierだけ転送します。
// routes/callback.tsx
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
// state 검증
const stored = JSON.parse(
sessionStorage.getItem('oidc_auth_state') || '{}'
);
if (state !== stored.state) {
throw new Error('State mismatch');
}
// 토큰 교환 (client_secret 없이)
const res = await fetch(oidcConfig.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: oidcConfig.redirectUri,
client_id: oidcConfig.clientId,
code_verifier: stored.codeVerifier, // PKCE 검증
}),
});
const tokens = await res.json();
// localStorage에 토큰 저장
localStorage.setItem('oidc_tokens', JSON.stringify(tokens));
sessionStorage.removeItem('oidc_auth_state');
// 프로필 페이지로 이동
navigate({ to: '/profile' });
}
トークンアクセスとログアウト
// libs/token-storage.ts
export function getTokens() {
const stored = localStorage.getItem('oidc_tokens');
return stored ? JSON.parse(stored) : null;
}
export function clearTokens() {
localStorage.removeItem('oidc_tokens');
}
ルートガード
認証されていないユーザーが保護されたページにアクセスするのを防ぎます。
// routes/profile.tsx
export const Route = createFileRoute('/profile')({
beforeLoad: () => {
const tokens = getTokens();
if (!tokens) {
throw redirect({ to: '/' });
}
return { tokens };
},
component: ProfilePage,
});
Vite開発サーバープロキシ
開発環境で CORS 問題を回避するには、Viteプロキシを設定してください。
// vite.config.ts
export default defineConfig({
server: {
port: 3001,
proxy: {
'/application': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
コアポイント
client_secretありません。 パブリッククライアントはシークレットの代わりに PKCEで認証コードの消臭を防御します。- トークンはlocalStorageに保存されます。 サーバーサイドアプリのhttpOnly Cookieよりもセキュリティレベルが低いため、 XSS 防御に格別に注意しなければなりません。
- 認証ステータスはsessionStorageに保存されます。 タブが閉じると自動的に整理されます。
- トークン交換はブラウザで直接行われます。 サーバープロキシは必要ありません。
Caution
SPAでのトークンの保存は XSS 攻撃に脆弱です。本番環境では、Content Security Policy(CSP)ヘッダ設定、サードパーティスクリプトの最小化など XSS 防御措置を必ず適用してください。
Note
より完全な実装例はプロジェクトの examples/clients/react-spa ディレクトリを参照してください。 IDトークンデコード、トークンイントロスペクション、トークンキャンセル、エラー処理などの追加機能が含まれています。