SolidJS で認証機能を実装する:JWT・OAuth 入門

現代の Web アプリケーション開発において、ユーザー認証は不可欠な機能です。特に SPA(Single Page Application)では、セキュアで効率的な認証システムの実装が求められますね。
SolidJS は、そのリアクティブなシグナルシステムと軽量な設計により、認証機能の実装に優れた特性を持っています。本記事では、JWT(JSON Web Token)と OAuth を活用した本格的な認証システムの構築方法をご紹介いたします。
背景
SolidJS の認証実装の特徴
SolidJS での認証実装には、他のフレームワークとは異なる独特な特徴があります。最も重要なのは、SolidJS のシグナルベースのリアクティビティシステムを活用できることでしょう。
認証状態の変更が自動的に UI に反映される仕組みを構築できるため、ユーザー体験の向上につながります。
mermaidflowchart TD
User[ユーザー] -->|ログイン要求| Auth[認証コンテキスト]
Auth -->|シグナル更新| Signal[認証シグナル]
Signal -->|自動再レンダリング| UI[UI コンポーネント]
Signal -->|状態変更| Route[ルートガード]
Route -->|アクセス制御| Protected[保護されたページ]
図で理解できる要点:
- シグナルによる自動的な状態同期
- コンポーネント間でのリアルタイム認証状態共有
- 効率的なルーティング制御
JWT と OAuth の基本概念
JWT(JSON Web Token)は、JSON 形式でエンコードされたトークンベースの認証方式です。サーバーレスアーキテクチャや API 通信において、ステートレスな認証を実現できる点が大きな魅力ですね。
OAuth は、第三者認証プロバイダー(Google、GitHub など)を活用してユーザー認証を行う仕組みです。ユーザーは既存のアカウントを使用でき、開発者は認証システムの構築コストを削減できます。
認証方式 | メリット | デメリット | 使用場面 |
---|---|---|---|
JWT | ステートレス、スケーラブル | トークン管理が複雑 | API 認証、マイクロサービス |
OAuth | ユーザビリティ向上、セキュリティ強化 | 外部依存、設定が複雑 | SNS 連携、エンタープライズ |
現代的な認証システムの要件
現代の認証システムには以下の要件が求められます:
セキュリティ要件
- HTTPS 通信の徹底
- CSRF・XSS 攻撃への対策
- トークンの適切な管理と更新
ユーザビリティ要件
- シームレスなログイン体験
- マルチデバイス対応
- 自動ログアウト機能
これらの要件を SolidJS で効率的に実装する方法を、本記事で詳しく解説していきます。
課題
従来のフレームワークとの認証実装の違い
React や Vue.js での認証実装に慣れている開発者の方は、SolidJS での実装に戸惑うことがあるかもしれません。
主な違いは、状態管理のアプローチにあります。React の useState
や Vue の reactive
とは異なり、SolidJS では createSignal
を使用したシグナルベースの管理が基本となりますね。
mermaidgraph LR
subgraph React
State[useState] --> Component1[コンポーネント]
State --> Component2[コンポーネント]
end
subgraph SolidJS
Signal[createSignal] --> Comp1[コンポーネント]
Signal --> Comp2[コンポーネント]
end
React -->|学習コスト| SolidJS
補足:シグナルベースの状態管理は、より直感的で効率的な再レンダリングを実現します。
SolidJS でのステート管理における認証の課題
SolidJS での認証状態管理には、以下の課題があります:
グローバル状態の共有 認証情報を複数のコンポーネントで共有する必要がありますが、適切なコンテキスト設計が重要です。誤った実装は、不要な再レンダリングや状態の不整合を引き起こす可能性があります。
永続化とセキュリティの両立 トークンをブラウザに保存する際、セキュリティと利便性のバランスを取る必要があります。localStorage は XSS 攻撃のリスクがあり、セッションストレージは永続性に劣るという課題があります。
セキュリティ面での注意点
認証実装において最も重要なのは、セキュリティの確保です。特に注意すべき点をご紹介します:
トークンストレージのセキュリティ JWT トークンの保存場所は慎重に選択する必要があります。以下の表で各ストレージの特徴をまとめました。
ストレージ | セキュリティレベル | 永続性 | XSS 耐性 | 推奨度 |
---|---|---|---|---|
localStorage | 低 | 高 | × | △ |
sessionStorage | 中 | 低 | × | △ |
httpOnly Cookie | 高 | 高 | ○ | ○ |
メモリ(State) | 高 | 低 | ○ | ○ |
CSRF 攻撃への対策 Cookie ベースの認証を使用する場合、CSRF トークンの実装が不可欠です。SolidJS では、フォーム送信時に自動的に CSRF トークンを含める仕組みを構築できます。
これらの課題を解決するための具体的なアプローチを、次の章で詳しく見ていきましょう。
解決策
SolidJS での認証アーキテクチャ設計
SolidJS で効果的な認証システムを構築するには、適切なアーキテクチャの設計が不可欠です。コンテキストベースの認証管理を中心とした設計パターンをご紹介いたします。
認証アーキテクチャの全体像を図で示すと以下のようになります:
mermaidflowchart TB
subgraph App[アプリケーション層]
Router[SolidJS Router]
Pages[ページコンポーネント]
end
subgraph Auth[認証層]
Context[認証コンテキスト]
Guards[ルートガード]
Hooks[認証フック]
end
subgraph Storage[ストレージ層]
Memory[メモリストレージ]
Cookie[HttpOnly Cookie]
Local[LocalStorage]
end
subgraph External[外部サービス]
JWT_API[JWT API]
OAuth[OAuth プロバイダー]
end
Router --> Guards
Pages --> Hooks
Context --> Memory
Context --> Cookie
Guards --> Context
Hooks --> Context
Context --> JWT_API
Context --> OAuth
図で理解できる要点:
- レイヤー化された認証アーキテクチャ
- 各層の責務分離による保守性向上
- 外部サービスとの明確な境界
JWT トークン管理の実装方法
JWT トークンの安全で効率的な管理は、認証システムの核心部分です。SolidJS でのベストプラクティスをステップごとに解説いたします。
トークン管理の基本方針
- アクセストークンはメモリ上で管理
- リフレッシュトークンは HttpOnly Cookie で保存
- 自動リフレッシュ機能の実装
この方針により、XSS 攻撃のリスクを最小化しながら、ユーザビリティを維持できます。
OAuth プロバイダーとの連携手法
OAuth 2.0 / OpenID Connect を活用した外部プロバイダーとの連携は、現代的な認証システムの重要な要素です。
代表的なプロバイダーとの連携パターンをご紹介します:
プロバイダー | 特徴 | 実装の複雑さ | 推奨用途 |
---|---|---|---|
高い普及率、豊富な API | 中 | 一般向けアプリ | |
GitHub | 開発者向け、シンプル | 低 | 開発ツール |
Auth0 | 統合認証プラットフォーム | 低 | エンタープライズ |
Firebase Auth | Google 製、多機能 | 中 | モバイル連携 |
OAuth フローの概要を図解すると以下のようになります:
mermaidsequenceDiagram
participant User as ユーザー
participant App as SolidJS App
participant Provider as OAuth プロバイダー
participant API as バックエンド API
User->>App: ログインボタンクリック
App->>Provider: 認証リクエスト (client_id, redirect_uri)
Provider->>User: ログイン画面表示
User->>Provider: 認証情報入力
Provider->>App: 認可コード返却
App->>API: 認可コード送信
API->>Provider: アクセストークン要求
Provider->>API: アクセストークン返却
API->>App: JWT トークン発行
App->>User: ログイン完了
補足:このフローにより、ユーザーの認証情報が SolidJS アプリに直接渡されることなく、安全な認証が実現されます。
具体例
JWT ベース認証の実装
トークンの生成・保存・検証
まず、認証コンテキストの基本構造を実装していきます。SolidJS のシグナルを活用した効率的な状態管理が特徴です。
typescript// types/auth.ts
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
}
export interface AuthState {
user: User | null;
accessToken: string | null;
isLoading: boolean;
isAuthenticated: boolean;
}
認証状態の型定義では、ユーザー情報とトークン情報を明確に分離しています。これにより、型安全性を保ちながら開発を進められますね。
typescript// contexts/AuthContext.tsx
import {
createContext,
useContext,
ParentComponent,
} from 'solid-js';
import { createSignal, createMemo } from 'solid-js';
import type { User, AuthState } from '../types/auth';
// 認証コンテキストの型定義
interface AuthContextValue {
state: () => AuthState;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<boolean>;
}
// コンテキストの作成
const AuthContext = createContext<AuthContextValue>();
コンテキストの設計では、認証に必要な主要な操作を明確に定義しています。非同期操作には Promise を使用し、エラーハンドリングも考慮した設計となっています。
typescript// contexts/AuthContext.tsx (続き)
export const AuthProvider: ParentComponent = (props) => {
const [user, setUser] = createSignal<User | null>(null);
const [accessToken, setAccessToken] = createSignal<
string | null
>(null);
const [isLoading, setIsLoading] = createSignal(false);
// 認証状態の計算プロパティ
const state = createMemo(
(): AuthState => ({
user: user(),
accessToken: accessToken(),
isLoading: isLoading(),
isAuthenticated: !!accessToken() && !!user(),
})
);
return (
<AuthContext.Provider
value={{ state, login, logout, refreshToken }}
>
{props.children}
</AuthContext.Provider>
);
};
createMemo
を使用した認証状態の計算により、依存する値が変更された場合のみ再計算が行われます。これにより、パフォーマンスの最適化が図られています。
JWT トークンの検証ロジックを実装します:
typescript// utils/jwt.ts
import { jwtDecode } from 'jwt-decode';
interface JwtPayload {
sub: string;
email: string;
name: string;
exp: number;
iat: number;
}
export const validateToken = (token: string): boolean => {
try {
const decoded = jwtDecode<JwtPayload>(token);
const currentTime = Math.floor(Date.now() / 1000);
// トークンの有効期限をチェック
if (decoded.exp < currentTime) {
return false;
}
return true;
} catch (error) {
console.error('Token validation failed:', error);
return false;
}
};
トークン検証では、有効期限のチェックを中心としたシンプルな実装としています。実際のプロダクションでは、さらに詳細な検証ロジックが必要になる場合があります。
ルートガードの実装
認証が必要なページへのアクセスを制御するルートガードを実装します。SolidJS Router と連携した効果的な実装方法をご紹介いたします。
typescript// components/RouteGuard.tsx
import { Component, JSX, Show } from 'solid-js';
import { Navigate } from '@solidjs/router';
import { useAuth } from '../contexts/AuthContext';
interface RouteGuardProps {
children: JSX.Element;
fallback?: JSX.Element;
redirectTo?: string;
}
export const RouteGuard: Component<RouteGuardProps> = (
props
) => {
const auth = useAuth();
return (
<Show
when={auth.state().isAuthenticated}
fallback={
props.fallback || (
<Navigate href={props.redirectTo || '/login'} />
)
}
>
{props.children}
</Show>
);
};
ルートガードコンポーネントは、認証状態に基づいてコンテンツの表示/非表示を制御します。認証されていない場合は、指定されたページへのリダイレクトが実行されます。
typescript// components/PublicRoute.tsx
import { Component, JSX, Show } from 'solid-js';
import { Navigate } from '@solidjs/router';
import { useAuth } from '../contexts/AuthContext';
interface PublicRouteProps {
children: JSX.Element;
redirectTo?: string;
}
export const PublicRoute: Component<PublicRouteProps> = (
props
) => {
const auth = useAuth();
return (
<Show
when={!auth.state().isAuthenticated}
fallback={
<Navigate href={props.redirectTo || '/dashboard'} />
}
>
{props.children}
</Show>
);
};
逆に、ログイン済みユーザーがアクセスすべきでないページ(ログインページなど)用のコンポーネントも用意します。これにより、適切なユーザーフローを実現できます。
ログイン・ログアウト機能
実際のログイン・ログアウト処理を実装していきましょう。API 通信とトークン管理を組み合わせた実装例です。
typescript// services/authService.ts
interface LoginRequest {
email: string;
password: string;
}
interface LoginResponse {
accessToken: string;
refreshToken: string;
user: User;
}
export const authService = {
async login(
credentials: LoginRequest
): Promise<LoginResponse> {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
credentials: 'include', // Cookie を含める
});
if (!response.ok) {
throw new Error('ログインに失敗しました');
}
return response.json();
},
async logout(): Promise<void> {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
},
};
API サービスでは、認証に関するすべての HTTP 通信を一元管理しています。credentials: 'include'
により、HttpOnly Cookie の送信が確保されます。
typescript// contexts/AuthContext.tsx (login/logout 実装)
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await authService.login({
email,
password,
});
// アクセストークンをメモリに保存
setAccessToken(response.accessToken);
setUser(response.user);
// リフレッシュトークンは HttpOnly Cookie で自動管理
console.log('ログインが完了しました');
} catch (error) {
console.error('ログインエラー:', error);
throw error;
} finally {
setIsLoading(false);
}
};
const logout = async () => {
try {
await authService.logout();
} catch (error) {
console.error('ログアウトエラー:', error);
} finally {
// 状態をクリア
setAccessToken(null);
setUser(null);
}
};
ログイン成功時は、アクセストークンをメモリに保存し、ユーザー情報を状態に設定します。ログアウト時は、サーバー側でのセッション無効化と合わせて、クライアント側の状態もクリアします。
OAuth 連携の実装
Google OAuth の実装例
Google OAuth 2.0 を使用した認証実装を具体的に見ていきましょう。OAuth フローの実装には、細かな設定と適切なエラーハンドリングが重要です。
typescript// config/oauth.ts
export const googleOAuthConfig = {
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
redirectUri: `${window.location.origin}/auth/google/callback`,
scope: 'openid email profile',
responseType: 'code',
state: generateRandomState(), // CSRF 対策
};
function generateRandomState(): string {
return Math.random().toString(36).substring(2, 15);
}
OAuth 設定では、セキュリティ強化のために State パラメータを使用しています。これにより、CSRF 攻撃を防ぐことができますね。
typescript// services/googleAuth.ts
export const googleAuthService = {
// Google 認証ページへのリダイレクト
initiateLogin(): void {
const params = new URLSearchParams({
client_id: googleOAuthConfig.clientId,
redirect_uri: googleOAuthConfig.redirectUri,
response_type: googleOAuthConfig.responseType,
scope: googleOAuthConfig.scope,
state: googleOAuthConfig.state,
});
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
window.location.href = authUrl;
},
// 認可コードの処理
async handleCallback(
code: string,
state: string
): Promise<LoginResponse> {
// State の検証
if (state !== googleOAuthConfig.state) {
throw new Error('Invalid state parameter');
}
const response = await fetch(
'/api/auth/google/callback',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('Google OAuth authentication failed');
}
return response.json();
},
};
Google OAuth の実装では、認証フローを 2 段階に分けています。最初に Google の認証ページにリダイレクトし、コールバック時にサーバーサイドで認可コードを処理します。
GitHub OAuth の実装例
GitHub OAuth の実装も同様のパターンで構築できます。GitHub API の特性を活かした実装例をご紹介いたします。
typescript// config/github-oauth.ts
export const githubOAuthConfig = {
clientId: import.meta.env.VITE_GITHUB_CLIENT_ID,
redirectUri: `${window.location.origin}/auth/github/callback`,
scope: 'user:email',
state: generateRandomState(),
};
GitHub OAuth では、必要最小限のスコープを指定しています。ユーザーのメールアドレスのみを取得し、プライバシーを保護します。
typescript// services/githubAuth.ts
export const githubAuthService = {
initiateLogin(): void {
const params = new URLSearchParams({
client_id: githubOAuthConfig.clientId,
redirect_uri: githubOAuthConfig.redirectUri,
scope: githubOAuthConfig.scope,
state: githubOAuthConfig.state,
});
const authUrl = `https://github.com/login/oauth/authorize?${params}`;
window.location.href = authUrl;
},
async handleCallback(
code: string,
state: string
): Promise<LoginResponse> {
if (state !== githubOAuthConfig.state) {
throw new Error('Invalid state parameter');
}
const response = await fetch(
'/api/auth/github/callback',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('GitHub OAuth authentication failed');
}
return response.json();
},
};
GitHub の実装では、よりシンプルなスコープ設定により、開発者向けのアプリケーションに適した認証が実現されています。
認証情報の状態管理
複数の OAuth プロバイダーに対応した統合的な状態管理を実装します。プロバイダーの種類に関わらず、一貫した認証体験を提供することが重要です。
typescript// types/auth.ts (拡張)
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
provider: 'google' | 'github' | 'email'; // プロバイダー情報を追加
}
export interface OAuthProvider {
name: string;
displayName: string;
iconUrl: string;
initiateLogin: () => void;
}
ユーザー情報にプロバイダー情報を含めることで、どの認証方法を使用したかを追跡できます。これは、アカウント管理や統計情報の収集に役立ちます。
typescript// hooks/useOAuth.ts
import { createSignal } from 'solid-js';
import { googleAuthService } from '../services/googleAuth';
import { githubAuthService } from '../services/githubAuth';
export const useOAuth = () => {
const [isLoading, setIsLoading] = createSignal(false);
const providers: OAuthProvider[] = [
{
name: 'google',
displayName: 'Google',
iconUrl: '/icons/google.svg',
initiateLogin: googleAuthService.initiateLogin,
},
{
name: 'github',
displayName: 'GitHub',
iconUrl: '/icons/github.svg',
initiateLogin: githubAuthService.initiateLogin,
},
];
const loginWithProvider = (provider: string) => {
setIsLoading(true);
const oauthProvider = providers.find(
(p) => p.name === provider
);
if (oauthProvider) {
oauthProvider.initiateLogin();
} else {
setIsLoading(false);
throw new Error(`Unknown provider: ${provider}`);
}
};
return {
providers,
loginWithProvider,
isLoading,
};
};
OAuth フックにより、複数プロバイダーへの対応が簡素化されます。新しいプロバイダーの追加も、配列に新しい設定を追加するだけで対応可能です。
認証システムの実装において最も重要なのは、セキュリティとユーザビリティのバランスを適切に取ることです。今回ご紹介した実装例を参考に、プロジェクトの要件に応じてカスタマイズしてください。
まとめ
本記事では、SolidJS での JWT・OAuth 認証システムの実装について詳しく解説いたしました。
SolidJS の特徴であるシグナルベースのリアクティビティを活用することで、効率的で保守性の高い認証システムを構築できることをご理解いただけたでしょうか。特に、認証状態の変更が自動的に UI に反映される仕組みは、優れたユーザー体験の実現につながります。
重要なポイントの振り返り
項目 | 実装のポイント | セキュリティ考慮事項 |
---|---|---|
JWT 管理 | メモリ + HttpOnly Cookie の組み合わせ | XSS 攻撃の防止、適切な有効期限設定 |
OAuth 連携 | State パラメータによる CSRF 対策 | 最小権限の原則に基づくスコープ設定 |
状態管理 | シグナルベースの効率的な更新 | 認証情報の適切な初期化とクリア |
ルートガード | 宣言的なアクセス制御 | 認証状態の確実な検証 |
認証システムの実装において最も重要なのは、セキュリティファーストの設計思想です。利便性を追求しつつも、ユーザーの情報を適切に保護する実装を心がけましょう。
今回ご紹介した実装パターンは、小規模から中規模のアプリケーションに適用可能です。大規模なエンタープライズアプリケーションでは、さらに高度なセキュリティ要件や監査機能が必要になる場合がありますので、要件に応じて拡張してください。
SolidJS での認証実装を通じて、モダンな Web アプリケーション開発のスキル向上につながれば幸いです。
関連リンク
- article
SolidJS で認証機能を実装する:JWT・OAuth 入門
- article
SolidJS で SVG や Canvas を自在に操る
- article
SolidJS アドオン&エコシステム最新事情
- article
SolidJS のカスタムフック(create*系)活用事例集
- article
SolidJS で多言語化(i18n)対応を行う
- article
SolidJS のパフォーマンス計測&プロファイリング
- article
Svelte と GraphQL:最速データ連携のススメ
- article
Lodash の throttle・debounce でパフォーマンス最適化
- article
LangChain で RAG 構築:Retriever・VectorStore の設計ベストプラクティス
- article
Storybook で学ぶコンポーネントテスト戦略
- article
状態遷移を明文化する:XState × Jotai の堅牢な非同期フロー設計
- article
Jest で DOM 操作をテストする方法:document・window の扱い方まとめ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来