Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

password reset and magic link #14

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dotenv
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ node_modules
/dist
.wrangler

# npm
package-lock.json

# OS
.DS_Store
Thumbs.db
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ npm install -D @mierune/sveltekit-firebase-auth-ssr
3. **Implement sign-in and sign-out functionality** in your application. (Example: TODO)
4. Use the user information and implement database integration if needed.
5. Ensure that the required environment variables are set in the execution environment.


## Development

```bash
direnv allow
pnpm dev-in-emulator
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"test": "npm run test:integration && npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"start-firebase-emulator": "firebase emulators:start --project fukada-delete-me",
"start-emulator": "firebase emulators:start --project fukada-delete-me",
"dev-in-emulator": "firebase emulators:exec --project fukada-delete-me 'pnpm run dev'",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:integration": "playwright test",
Expand Down
38 changes: 30 additions & 8 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ import type { Context } from 'hono';
import { getCookie } from 'hono/cookie';
import { createMiddleware } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
import { env } from 'hono/adapter';
import {
getAuth,
InMemoryStore,
ServiceAccountCredential,
WorkersKVStoreSingle
} from '$lib/firebase-auth/server';

import { PUBLIC_FIREBASE_PROJECT_ID } from '$env/static/public';
import { env } from '$env/dynamic/private';

const serviceAccountCredential = new ServiceAccountCredential(env.GOOGLE_SERVICE_ACCOUNT_KEY);

export type CurrentUser = {
uid: string;
name: string;
Expand All @@ -25,21 +21,47 @@ export interface AuthVariables {

const memKeyStore = new InMemoryStore();

export const authMiddleware = createMiddleware(async (c, next) => {
export const authMiddleware = createMiddleware<{
Bindings: Env & {
PUBLIC_FIREBASE_PROJECT_ID: string;
GOOGLE_SERVICE_ACCOUNT_KEY: string;
PUBLIC_FIREBASE_AUTH_EMULATOR_HOST: string;
};
Variables: AuthVariables;
}>(async (c, next) => {
// 環境変数
const {
PUBLIC_FIREBASE_PROJECT_ID,
GOOGLE_SERVICE_ACCOUNT_KEY,
PUBLIC_FIREBASE_AUTH_EMULATOR_HOST
} = env(c);

let serviceAccountCredential: ServiceAccountCredential | undefined;
try {
serviceAccountCredential = new ServiceAccountCredential(GOOGLE_SERVICE_ACCOUNT_KEY);
} catch {
if (!PUBLIC_FIREBASE_AUTH_EMULATOR_HOST) {
console.error('FIREBASE_SERVICE_ACCOUNT_KEY is not set. Authentication will not work.');
}
}

const sessionCookie = getCookie(c, 'session');
if (sessionCookie) {
const kv = c.env?.KV;
const keyStore = kv ? WorkersKVStoreSingle.getOrInitialize('pubkeys', kv) : memKeyStore;
const auth = getAuth(PUBLIC_FIREBASE_PROJECT_ID, keyStore, serviceAccountCredential);

try {
const idToken = await auth.verifySessionCookie(sessionCookie, false);
const idToken = await auth.verifySessionCookie(sessionCookie, false, {
FIREBASE_AUTH_EMULATOR_HOST: PUBLIC_FIREBASE_AUTH_EMULATOR_HOST || undefined
});
c.set('currentUser', {
uid: idToken.uid,
name: idToken.name
});
} catch {
} catch (error) {
// ignore
console.log('error', error);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

エラー情報のログ出力に関するセキュリティ考慮

console.log('error', error); でエラーオブジェクトをそのままログ出力していますが、これにより機密情報が漏洩する可能性があります。特に本番環境では、エラーメッセージにユーザー情報やシステムの詳細が含まれる場合があります。エラーメッセージを一般化するか、適切なログレベルを使用して詳細情報の露出を防いでください。

以下の差分を適用してログ出力を修正してください:

- console.log('error', error);
+ console.error('Authentication error occurred.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log('error', error);
console.error('Authentication error occurred.');

}
}
await next();
Expand Down
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const app = new Hono<{ Bindings: Env; Variables: AuthVariables }>()
const currentUser = ensureUser(c);
const posts = Array.from({ length: 20 }, () => ({
title: 'Great Article',
author: currentUser.name
author: currentUser.name ?? 'Unknown'
}));
return c.json(posts);
});
Expand Down
25 changes: 19 additions & 6 deletions src/lib/firebase-auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import {
signInWithPopup,
signInWithRedirect,
getRedirectResult,
signInWithEmailAndPassword as _signInWithEmailAndPassword,
createUserWithEmailAndPassword as _createUserWithEmailAndPassword,
connectAuthEmulator,
type UserCredential,
type AuthProvider
} from 'firebase/auth';
import { invalidate } from '$app/navigation';
import { getApp } from 'firebase/app';

/** re-export the official firebase/auth for convenience */
export * from 'firebase/auth';

let redirectResultPromise: Promise<UserCredential | null>;

export function setupAuthClient(options: { emulatorHost?: string }) {
Expand All @@ -33,7 +32,7 @@ export function setupAuthClient(options: { emulatorHost?: string }) {
// Update the session cookie when the idToken changes
auth.onIdTokenChanged(async (user) => {
if (user) {
updateSession(await user.getIdToken());
updateSession(await user.getIdToken(true));
}
});
}
Expand Down Expand Up @@ -61,6 +60,20 @@ export async function signInWithTwitter() {
await signInWithProvider(provider);
}

export async function signInWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _signInWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

エラーハンドリングの追加を推奨

メールパスワード認証にエラーハンドリングを追加することを推奨します:

 export async function signInWithEmailAndPassword(email: string, password: string) {
   const auth = getAuth();
-  const cred = await _signInWithEmailAndPassword(auth, email, password);
-  await updateSession(await cred.user.getIdToken());
-  return cred;
+  try {
+    const cred = await _signInWithEmailAndPassword(auth, email, password);
+    await updateSession(await cred.user.getIdToken());
+    return cred;
+  } catch (error: any) {
+    // Firebase Auth エラーコードに基づいてユーザーフレンドリーなメッセージを返す
+    switch (error.code) {
+      case 'auth/invalid-email':
+        throw new Error('メールアドレスの形式が正しくありません。');
+      case 'auth/user-disabled':
+        throw new Error('このアカウントは無効化されています。');
+      case 'auth/user-not-found':
+      case 'auth/wrong-password':
+        throw new Error('メールアドレスまたはパスワードが正しくありません。');
+      default:
+        throw new Error('ログインに失敗しました。しばらく経ってから再度お試しください。');
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function signInWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _signInWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
}
export async function signInWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
try {
const cred = await _signInWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
} catch (error: any) {
// Firebase Auth エラーコードに基づいてユーザーフレンドリーなメッセージを返す
switch (error.code) {
case 'auth/invalid-email':
throw new Error('メールアドレスの形式が正しくありません。');
case 'auth/user-disabled':
throw new Error('このアカウントは無効化されています。');
case 'auth/user-not-found':
case 'auth/wrong-password':
throw new Error('メールアドレスまたはパスワードが正しくありません。');
default:
throw new Error('ログインに失敗しました。しばらく経ってから再度お試しください。');
}
}
}


export async function createUserWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _createUserWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

アカウント作成時のバリデーションとエラーハンドリングの追加を推奨

アカウント作成機能にバリデーションとエラーハンドリングを追加することを推奨します:

 export async function createUserWithEmailAndPassword(email: string, password: string) {
   const auth = getAuth();
-  const cred = await _createUserWithEmailAndPassword(auth, email, password);
-  await updateSession(await cred.user.getIdToken());
-  return cred;
+  // パスワードの強度チェック
+  if (password.length < 8) {
+    throw new Error('パスワードは8文字以上である必要があります。');
+  }
+  if (!/[A-Z]/.test(password)) {
+    throw new Error('パスワードは少なくとも1つの大文字を含む必要があります。');
+  }
+  if (!/[0-9]/.test(password)) {
+    throw new Error('パスワードは少なくとも1つの数字を含む必要があります。');
+  }
+
+  try {
+    const cred = await _createUserWithEmailAndPassword(auth, email, password);
+    await updateSession(await cred.user.getIdToken());
+    return cred;
+  } catch (error: any) {
+    switch (error.code) {
+      case 'auth/email-already-in-use':
+        throw new Error('このメールアドレスは既に使用されています。');
+      case 'auth/invalid-email':
+        throw new Error('メールアドレスの形式が正しくありません。');
+      case 'auth/operation-not-allowed':
+        throw new Error('メール/パスワードでの認証が有効になっていません。');
+      case 'auth/weak-password':
+        throw new Error('パスワードが脆弱です。より強力なパスワードを設定してください。');
+      default:
+        throw new Error('アカウントの作成に失敗しました。しばらく経ってから再度お試しください。');
+    }
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function createUserWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
const cred = await _createUserWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
}
export async function createUserWithEmailAndPassword(email: string, password: string) {
const auth = getAuth();
// パスワードの強度チェック
if (password.length < 8) {
throw new Error('パスワードは8文字以上である必要があります。');
}
if (!/[A-Z]/.test(password)) {
throw new Error('パスワードは少なくとも1つの大文字を含む必要があります。');
}
if (!/[0-9]/.test(password)) {
throw new Error('パスワードは少なくとも1つの数字を含む必要があります。');
}
try {
const cred = await _createUserWithEmailAndPassword(auth, email, password);
await updateSession(await cred.user.getIdToken());
return cred;
} catch (error: any) {
switch (error.code) {
case 'auth/email-already-in-use':
throw new Error('このメールアドレスは既に使用されています。');
case 'auth/invalid-email':
throw new Error('メールアドレスの形式が正しくありません。');
case 'auth/operation-not-allowed':
throw new Error('メール/パスワードでの認証が有効になっていません。');
case 'auth/weak-password':
throw new Error('パスワードが脆弱です。より強力なパスワードを設定してください。');
default:
throw new Error('アカウントの作成に失敗しました。しばらく経ってから再度お試しください。');
}
}
}


export async function signInWithProvider(provider: AuthProvider, withRedirect = true) {
const auth = getAuth();
const app = getApp();
Expand All @@ -71,7 +84,6 @@ export async function signInWithProvider(provider: AuthProvider, withRedirect =
// Fall back to sign-in by popup method if authDomain is different from location.host
const cred = await signInWithPopup(auth, provider);
await updateSession(await cred.user.getIdToken());
invalidate('auth:session');
}
}

Expand All @@ -80,8 +92,8 @@ export async function signInWithProvider(provider: AuthProvider, withRedirect =
*/
export async function signOut() {
await updateSession(undefined);
invalidate('auth:session');
await getAuth().signOut();
invalidate('auth:session');
resetRedirectResultHandler();
}

Expand All @@ -96,6 +108,7 @@ async function updateSession(idToken: string | undefined) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken })
});
invalidate('auth:session');
previousIdToken = idToken;
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/firebase-auth/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { env } from '$env/dynamic/public';

export {
InMemoryStore,
type FirebaseIdToken,
ServiceAccountCredential,
WorkersKVStoreSingle
WorkersKVStoreSingle,
type FirebaseIdToken
} from 'firebase-auth-cloudflare-workers-x509';

export type AuthHandleOptions = {
Expand Down
1 change: 1 addition & 0 deletions src/lib/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type PublicUserInfo = {
export type BasicPrivateUserInfo = {
name: string;
email?: string;
email_verified?: boolean;
} & PublicUserInfo;
3 changes: 2 additions & 1 deletion src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export const load = async ({ locals, depends }) => {
currentIdToken: locals.currentIdToken,
currentUser: locals.currentIdToken && {
uid: locals.currentIdToken.uid,
email: locals.currentIdToken.email
email: locals.currentIdToken.email,
email_verified: locals.currentIdToken.email_verified
}
};
};
32 changes: 25 additions & 7 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { getAuth, sendEmailVerification } from 'firebase/auth';
import { signOut } from '$lib/firebase-auth/client';
import type { PageData } from './$types';
import { page } from '$app/stores';

let {
data,
Expand All @@ -10,8 +12,31 @@
data: PageData;
children: Snippet;
} = $props();

async function _sendEmailVerification() {
const auth = getAuth();
if (auth.currentUser) {
await sendEmailVerification(auth.currentUser, { url: $page.url.origin + '/verify_email' });
alert('Verification email sent!');
}
}
</script>

{#if data.currentIdToken !== undefined}
<p>
Current User: <code>{JSON.stringify(data.currentUser)}</code>
<button onclick={signOut} disabled={data.currentUser === undefined}>Logout</button>
</p>
{#if data.currentUser?.email_verified === false}
<p style="color: white; background-color: brown; padding: 0.3em;">
Your email isn’t verified yet. Check your inbox for the verification link. <button
onclick={() => _sendEmailVerification()}>Resend verification email.</button
>
</p>
{/if}
<hr />
{/if}

<p>
GitHub: <a href="https://github.com/MIERUNE/sveltekit-firebase-auth-ssr" target="_blank"
>MIERUNE/sveltekit-firebase-auth-ssr</a
Expand All @@ -27,11 +52,4 @@
{/if}
</ul>

<p>
{#if data.currentIdToken !== undefined}
<code>{JSON.stringify(data.currentUser)}</code>
<button onclick={signOut} disabled={data.currentUser === undefined}>Logout</button>
{/if}
</p>

{@render children()}
3 changes: 2 additions & 1 deletion src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>

<p>Home</p>
Loading