
シリーズ:SvelteKit 認証完全ガイド
01
bcrypt・Prisma・サインアップ
02
セッションDB永続化・有効期限
03
CSRF 対策・Cookie セキュリティ
04
OAuth・GitHub・本番チェックリスト
第3回までで bcrypt・DB セッション・CSRF 対策・セキュリティヘッダーが揃い、パスワード認証が本番レベルに達しました。最終回の今回は OAuth によるソーシャルログイン を追加し、シリーズ全体を 本番運用チェックリスト で締めくくります。 OAuth の実装はライブラリを使わず fetch で認可コードフローを手書きします。仕組みを理解することで、どのプロバイダーにも対応できる応用力が身につきます。
💡 前提条件
第1〜3回で実装した Prisma・セッション管理・hooks.server.ts が存在する状態で進めます。GitHub アカウントが必要です(OAuth アプリの登録に使います)。
OAuth の仕組み ― 認可コードフロー
OAuth は「外部サービス(GitHub など)のアカウント情報を使ってログインする」仕組みです。パスワードをこちらのサーバーで扱わないため、パスワード管理の責任が外部サービスに移ります。
ブラウザ
GitHub
自サーバー
① ログインボタンをクリック
② GitHub の認可ページへリダイレクト
③ ユーザーがアクセスを許可
④ code 付きでコールバックURL へリダイレクト
⑤ code をサーバーに渡す
⑥ code → access token 交換
⑦ token でユーザー情報を取得 → セッション作成
処理の主役はサーバーです。 ② のリダイレクト先URL の生成 と、 ⑥ の認可コード(code)→ access token の交換 、 ⑦ のユーザー情報取得 を SvelteKit のサーバーで実装します。
GitHub OAuth アプリを登録する
まず GitHub に OAuth アプリを登録して、Client ID と Client Secret を取得します。
1
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
GitHub にログインし、右上のアイコン → Settings → 左サイドバー下部の Developer settings → OAuth Apps の順に進みます。
2
アプリ情報を入力する
Application name に任意の名前、Homepage URL にhttp://localhost:5173 、 Authorization callback URL にhttp://localhost:5173/auth/github/callback を入力して「Register application」をクリックします。
3
Client ID と Client Secret を取得する
登録後の画面に Client ID が表示されます。「Generate a new client secret」をクリックして Client Secret を生成し、 必ず控えてください (このページを離れると二度と表示されません)。
4
.env に追記する
取得した値を.env ファイルに追記します。
DATABASE_URL="file:./dev.db"
# GitHub OAuth
GITHUB_CLIENT_ID="your_client_id_here"
GITHUB_CLIENT_SECRET="your_client_secret_here"
# コールバック URL(本番では本番ドメインに変更する)
GITHUB_REDIRECT_URI="http://localhost:5173/auth/github/callback"
# state 検証用のランダムシークレット
OAUTH_STATE_SECRET="any_random_long_string_here" スキーマに OAuthAccount モデルを追加する
1ユーザーが複数のソーシャルログイン(GitHub・Google など)を持てるよう、OAuthAccount モデルを追加します。
Prisma Schemaprisma/schema.prisma(OAuthAccount を追加)
model User {
id String @id @default(cuid())
email String @unique
name String
// OAuth ユーザーはパスワードを持たないので nullable に
hashedPassword String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
oauthAccounts OAuthAccount[]
}
model Session {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
}
model OAuthAccount {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
provider String // 'github' | 'google' など
providerUserId String // プロバイダー側のユーザーID
createdAt DateTime @default(now())
@@unique([provider, providerUserId]) // 同じプロバイダーの同じIDは1つまで
}$ npx prisma migrate dev --name add-oauth-account
✔ Generated Prisma Client (v6.x.x) GitHub OAuth の実装
Step 1 ― ログインボタン(リダイレクト先URLの生成)
「GitHub でログイン」ボタンをクリックすると、/auth/github に遷移し、GitHub の認可ページへリダイレクトします。 state パラメータ は CSRF 対策として必須です。
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { randomBytes } from 'node:crypto';
export const GET: RequestHandler = async (event) => {
// CSRF 対策用の state をランダム生成してCookieに保存
const state = randomBytes(16).toString('hex');
event.cookies.set('oauth_state', state, {
path: '/', httpOnly: true, sameSite: 'lax',
maxAge: 60 * 10, // 10分で失効
});
// GitHub の認可ページへリダイレクトする URL を組み立てる
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
redirect_uri: process.env.GITHUB_REDIRECT_URI!,
scope: 'read:user user:email', // 取得するスコープ
state,
});
redirect(302, `https://github.com/login/oauth/authorize?${params}`);
}; Step 2 ― コールバック処理(code → token → ユーザー情報)
GitHub がリダイレクトしてくるコールバックURLで、認可コードを受け取り access token に交換し、ユーザー情報を取得します。
import { redirect, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { createSession } from '$lib/server/session';
import { SESSION_COOKIE_OPTIONS } from '$lib/server/cookie';
export const GET: RequestHandler = async (event) => {
const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state');
const savedState = event.cookies.get('oauth_state');
// ── state 検証(CSRF 対策) ──
if (!state || state !== savedState) error(400, 'Invalid state');
event.cookies.delete('oauth_state', { path: '/' });
if (!code) error(400, 'Missing code');
// ── code → access token に交換 ──
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code,
}),
});
const { access_token } = await tokenRes.json();
if (!access_token) error(400, 'Failed to get access token');
// ── GitHub API でユーザー情報を取得 ──
const ghUser = await fetch('https://api.github.com/user', {
headers: { 'Authorization': `Bearer ${access_token}`, 'User-Agent': 'svelte-shop' },
}).then((r) => r.json());
// メールは別エンドポイントで取得(非公開設定のユーザーに対応)
const emails: { email: string; primary: boolean }[] = await fetch(
'https://api.github.com/user/emails',
{ headers: { 'Authorization': `Bearer ${access_token}`, 'User-Agent': 'svelte-shop' } }
).then((r) => r.json());
const primaryEmail = emails.find((e) => e.primary)?.email ?? ghUser.email;
if (!primaryEmail) error(400, 'Email not found');
// ── DB でユーザーを検索 or 作成 ──
let user = await findOrCreateOAuthUser({
provider: 'github',
providerUserId: String(ghUser.id),
email: primaryEmail,
name: ghUser.name || ghUser.login,
});
// ── セッション作成・Cookie 発行 ──
const sessionId = await createSession({
id: user.id, name: user.name, email: user.email
});
event.cookies.set('session_id', sessionId, SESSION_COOKIE_OPTIONS);
redirect(303, '/mypage');
};
// OAuthAccount を使ったユーザー検索 or 作成(既存アカウント連携を含む)
async function findOrCreateOAuthUser(params: {
provider: string; providerUserId: string;
email: string; name: string;
}) {
// ① OAuthAccount から既存ユーザーを検索
const existing = await db.oAuthAccount.findUnique({
where: { provider_providerUserId: { provider: params.provider, providerUserId: params.providerUserId } },
include: { user: true },
});
if (existing) return existing.user;
// ② 同じメールアドレスの既存ユーザーがいれば紐づける(アカウント連携)
const userByEmail = await db.user.findUnique({ where: { email: params.email } });
if (userByEmail) {
await db.oAuthAccount.create({
data: { userId: userByEmail.id, provider: params.provider, providerUserId: params.providerUserId },
});
return userByEmail;
}
// ③ まったく新しいユーザーを作成
const newUser = await db.user.create({
data: {
email: params.email,
name: params.name,
// OAuth ユーザーはパスワードなし(nullable にしたので省略可)
oauthAccounts: {
create: { provider: params.provider, providerUserId: params.providerUserId },
},
},
});
return newUser;
}✨ findOrCreateOAuthUser の 3パターン
① 同じ GitHub アカウントで再ログイン →OAuthAccount に一致レコードがあるのでそのユーザーを返す。② 別の方法(メールアドレス)でアカウントを持っているユーザーが GitHub ログインを試みる → 同じメールで紐づけてアカウント連携。③ 初めて GitHub でログインするユーザー → 新規ユーザーとして登録。
Step 3 ― ログインページにボタンを追加
<!-- 既存のメール・パスワードフォームの下に追加 -->
<div class="divider">または</div>
<!-- href で GET リクエストを送るだけ。フォームは不要 -->
<a href="/auth/github" class="btn-github">
GitHub でログイン
</a>🔍 動作確認
①http://localhost:5173/login を開く。② 「GitHub でログイン」をクリック。③ GitHub の認可ページでアクセスを許可。④ コールバックが処理されてマイページへリダイレクトされることを確認します。初回は新規ユーザーが作成され、npx prisma studio で User・OAuthAccount テーブルを確認できます。
Google OAuth との差分(補足)
GitHub OAuth を理解していれば Google OAuth も同じ構造で実装できます。主な差分は以下の通りです。
| 項目 | GitHub | |
|---|---|---|
| 認可エンドポイント | github.com/login/oauth/authorize | accounts.google.com/o/oauth2/v2/auth |
| token エンドポイント | github.com/login/oauth/access_token | oauth2.googleapis.com/token |
| ユーザー情報 API | api.github.com/user | www.googleapis.com/oauth2/v3/userinfo |
| scope(メール取得) | read:user user:email | openid email profile |
| token_type | bearer(小文字) | Bearer(大文字) |
| メール取得方法 | 別エンドポイント(/user/emails) | userinfo レスポンスに含まれる |
| 追加パラメータ | なし | access_type=offline(任意) |
💡 Google の実装手順
Google Cloud Console でプロジェクトを作成 → OAuth 同意画面を設定 → 認証情報 → OAuth 2.0 クライアント ID を作成 →GOOGLE_CLIENT_ID とGOOGLE_CLIENT_SECRET を取得します。実装は GitHub と同じ構造で、上記の差分箇所を置き換えるだけです。コールバックは/auth/google/callback に向けてください。
本番運用チェックリスト
これまでのシリーズを通じて実装してきた内容の最終確認です。デプロイ前に全項目を確認してください。
🔐 認証・セキュリティ
✓
パスワードは bcrypt でハッシュ化している
平文パスワードを DB に保存していないこと。SALT_ROUNDS は 10 以上(推奨 12)。
✓
セッションは DB に永続化されている
サーバー再起動でセッションが消えないこと。有効期限と自動延長を実装済みであること。
✓
Cookie に httpOnly・sameSite: lax を設定している
SESSION_COOKIE_OPTIONS を全箇所で統一使用していること。
!
本番環境で Cookie の secure: true を確認する
NODE_ENV=production が設定されていれば自動でtrue になる。HTTPS 環境であることを確認。
✓
セキュリティヘッダーを hooks.server.ts で一括設定している
X-Frame-Options・X-Content-Type-Options・Referrer-Policy・CSP が設定済みであること。
✓
OAuth の state パラメータを検証している
コールバックで Cookie の state と URL の state が一致するかを確認していること。
🗄 データベース
!
本番用 DB に切り替えている(SQLite → PostgreSQL など)
SQLite はシングルサーバー向け。複数サーバー構成や高負荷では PostgreSQL / MySQL を使う。schema.prisma のprovider とDATABASE_URL を変更してマイグレーションを実行。
✓
期限切れセッションのクリーンアップ cron を設定している
コールバックで Cookie の state と URL の state が一致するかを確認していること。
!
DB のバックアップを設定している
/api/cleanup-sessions を定期実行する仕組みがあること。
🌍 環境変数・デプロイ
!
.env を .gitignore に追加している
シークレットキーがリポジトリにコミットされていないこと。
!
本番環境の環境変数をホスティングサービスに設定している
DATABASE_URL ・GITHUB_CLIENT_ID ・GITHUB_CLIENT_SECRET ・GITHUB_REDIRECT_URI (本番ドメイン)などを設定。
!
GitHub OAuth アプリのコールバック URL を本番ドメインに更新している
GitHub の OAuth アプリ設定でAuthorization callback URL を本番の URL に変更すること。
!
NODE_ENV=production でビルドを確認している
npm run build && npm run preview で本番相当の動作を確認してからデプロイ。
📋 コード品質
✓
エラーメッセージでメールアドレスの存在を漏洩していない
「メールアドレスまたはパスワードが違います」に統一していること。
✓
handleError でスタックトレースをブラウザに返していない
ユーザーには簡潔なメッセージのみを返し、詳細はサーバーログに記録していること。
✓
src/lib/server/ のファイルをブラウザ側から import していない
DB・セッション・パスワード関連は.server.ts またはsrc/lib/server/ からのみ使用。
トラブルシュート
❓ GitHub OAuth のコールバックで「Invalid state」エラーになる
oauth_state Cookie が届いていない可能性があります。sameSite: 'lax' では GitHub からのリダイレクト(GET)で Cookie は付与されるはずですが、secure: true が開発環境で設定されていると HTTP では Cookie が送られません。開発環境ではsecure: false になっているか確認してください。
❓ GitHub でのログイン後にメールアドレスが取得できない
GitHub の設定でメールアドレスが非公開になっているユーザーは/user API のレスポンスにemail が含まれません。/user/emails エンドポイントを使ってプライマリメールを取得する実装になっているか確認してください(今回の実装では対応済みです)。
❓ 本番デプロイ後に OAuth が動かない
GitHub OAuth アプリの「Authorization callback URL」が本番ドメインに更新されているか確認してください。またGITHUB_REDIRECT_URI 環境変数も本番の URL に変更する必要があります。localhost のままだとエラーになります。
第4回の完成コード一覧
prisma/
└─ schema.prisma UPDATE ← OAuthAccount モデル追加・hashedPassword nullable
src/routes/auth/
├─ github/
├─ +server.ts NEW ← state 生成・GitHub 認可ページへリダイレクト
└─ callback/
└─ +server.ts NEW ← code→token 交換・ユーザー作成・セッション発行
src/routes/login/
└─ +page.svelte UPDATE ← GitHub ログインボタン追加 第4回・シリーズ全体のまとめ
今回学んだこと
- OAuth の認可コードフローは「リダイレクト → 認可 → code → token → ユーザー情報取得」の5ステップ。ライブラリなしで fetch だけで実装できる
stateパラメータは OAuth の CSRF 対策として必須。Cookie に保存してコールバックで照合するOAuthAccountモデルで同じメールアドレスを持つ既存アカウントと連携できる。1ユーザーが複数プロバイダーを持てる構造が重要- Google OAuth は GitHub と同じ構造で、エンドポイント URL・scope・メール取得方法の差分を置き換えるだけで実装できる
- 本番デプロイ前には Cookie の
secure: true・環境変数・OAuth アプリのコールバック URL 更新・DB の本番移行を必ず確認する