Skip to content

Commit

Permalink
Merge pull request #12 from MIERUNE/demo-email-password
Browse files Browse the repository at this point in the history
demo: signinWithEmailAndPassword
  • Loading branch information
ciscorn authored Dec 3, 2024
2 parents 866dd31 + 67c0810 commit e4ffbe9
Show file tree
Hide file tree
Showing 10 changed files with 13,942 additions and 23 deletions.
13,783 changes: 13,783 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

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);
}
}
await next();
Expand Down
41 changes: 35 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,33 @@ export async function signInWithTwitter() {
await signInWithProvider(provider);
}

// export async function refreshSession() {
// const auth = getAuth();
// await auth.authStateReady();
// if (auth.currentUser) {
// const idToken = await auth.currentUser.getIdToken(true);
// console.log(auth.currentUser.emailVerified, idToken);
// await updateSession(idToken);
// invalidate('auth:session');
// }
// }

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

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

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

let previousIdToken: string | undefined = undefined;

async function updateSession(idToken: string | undefined) {
export async function updateSession(idToken: string | undefined) {
if (idToken === previousIdToken) {
return;
}
Expand All @@ -96,6 +122,9 @@ async function updateSession(idToken: string | undefined) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken })
});
if (previousIdToken) {
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
}
};
};
22 changes: 19 additions & 3 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type { Snippet } from 'svelte';
import { signOut } from '$lib/firebase-auth/client';
import type { PageData } from './$types';
import { getAuth, sendEmailVerification } from 'firebase/auth';
import { page } from '$app/stores';
let {
data,
Expand All @@ -10,6 +12,13 @@
data: PageData;
children: Snippet;
} = $props();
async function _sendEmailVerification() {
const auth = getAuth();
if (auth.currentUser) {
await sendEmailVerification(auth.currentUser, { url: $page.url.origin + '/verify_email' });
}
}
</script>

<p>
Expand All @@ -27,11 +36,18 @@
{/if}
</ul>

<p>
{#if data.currentIdToken !== undefined}
{#if data.currentIdToken !== undefined}
<p>
<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: red;">
Your email address is not verified yet. <button onclick={() => _sendEmailVerification()}
>Resend verification email.</button
>
</p>
{/if}
</p>
{/if}

{@render children()}
63 changes: 61 additions & 2 deletions src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
<script lang="ts">
import { FirebaseError } from 'firebase/app';
import { sendEmailVerification } from 'firebase/auth';
import {
signInWithGoogle,
signInWithTwitter,
waitForRedirectResult
waitForRedirectResult,
signInWithEmailAndPassword,
createUserWithEmailAndPassword
} from '$lib/firebase-auth/client';
import { page } from '$app/stores';
const redirectResult = waitForRedirectResult();
let { data } = $props();
let email = $state('');
let password = $state('');
let errorCode = $state('');
async function signInWithPassword() {
try {
await signInWithEmailAndPassword(email, password);
} catch (error) {
if (error instanceof FirebaseError) {
errorCode = error.code;
}
}
}
async function signUpWithPassword() {
try {
const cred = await createUserWithEmailAndPassword(email, password);
await sendEmailVerification(cred.user, { url: $page.url.origin + '/verify_email' });
} catch (error) {
if (error instanceof FirebaseError) {
errorCode = error.code;
}
}
}
</script>

<h1>Login</h1>
Expand All @@ -20,11 +48,42 @@
{#if $page.url.searchParams.get('next')}
<p>You need to log in.</p>
{/if}

<button onclick={signInWithGoogle} disabled={data.currentIdToken !== undefined}
>Sign-in with Google</button
>
<!--
<button onclick={signInWithTwitter} disabled={data.currentIdToken !== undefined}
>Sign-in with Twitter</button
>
-->
<hr />
<div>
{#if errorCode}
<p style="color: red;">{errorCode}</p>
{/if}
<p>
<label
>Email: <input
type="text"
size="30"
bind:value={email}
placeholder="[email protected]"
/></label
>
<label
>Password: <input
type="password"
size="30"
bind:value={password}
placeholder="your password"
/></label
>
</p>
<p>
<button onclick={signInWithPassword}>Sign-In</button>
<button onclick={signUpWithPassword}>Sign-Up</button>
</p>
</div>
{/if}
{/await}
3 changes: 2 additions & 1 deletion src/routes/private/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from '@sveltejs/kit';

export async function load({ locals, url }) {
export async function load({ locals, url, depends }) {
depends('auth:session');
if (!locals.currentIdToken) {
redirect(303, '/login?next=' + encodeURIComponent(url.pathname + url.search));
}
Expand Down
7 changes: 7 additions & 0 deletions src/routes/verify_email/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
let { data } = $props();
</script>

<h1>Email Verification Page</h1>

<pre>{JSON.stringify(data.currentIdToken)}</pre>

0 comments on commit e4ffbe9

Please sign in to comment.