Skip to content

Commit d66186a

Browse files
authored
Merge pull request #12 from Code-Hex/add/cookie-verify
Supported cookie verification
2 parents 5f62fa6 + 5742997 commit d66186a

28 files changed

+1707
-181
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
node-version: 18
1919
cache: 'pnpm'
2020
- run: pnpm install --frozen-lockfile
21-
- run: pnpm test
21+
- run: pnpm test-with-emulator
2222
env:
2323
CI: true
2424
eslint:

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ lerna-debug.log*
1717
*.tsbuildinfo
1818

1919
# Dependency directories
20-
node_modules/
20+
node_modules/
21+
.wrangler/

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,13 @@ $ npm i firebase-auth-cloudflare-workers
110110

111111
## API
112112

113-
### `Auth.getOrInitialize(projectId: string, keyStore: KeyStorer): Auth`
113+
### `Auth.getOrInitialize(projectId: string, keyStore: KeyStorer, credential?: Credential): Auth`
114114

115115
Auth is created as a singleton object. This is because the Module Worker syntax only use environment variables at the time of request.
116116

117117
- `projectId` specifies the ID of the project for which firebase auth is used.
118118
- `keyStore` is used to cache the public key used to validate the Firebase ID token (JWT).
119+
- `credential` is an optional. This is used to utilize Admin APIs such as `createSessionCookie`. Currently, you can specify `ServiceAccountCredential` class, which allows you to use a service account.
119120

120121
See official document for project ID: https://firebase.google.com/docs/projects/learn-more#project-identifiers
121122

@@ -125,6 +126,7 @@ Verifies a Firebase ID token (JWT). If the token is valid, the promise is fulfil
125126

126127
See the [ID Token section of the OpenID Connect spec](http://openid.net/specs/openid-connect-core-1_0.html#IDToken) for more information about the specific properties below.
127128

129+
- `idToken` The ID token to verify.
128130
- `env` is an optional parameter. but this is using to detect should use emulator or not.
129131

130132
### `WorkersKVStoreSingle.getOrInitialize(cacheKey: string, cfKVNamespace: KVNamespace): WorkersKVStoreSingle`
@@ -138,6 +140,25 @@ This is implemented `KeyStorer` interface.
138140
- `cacheKey` specifies the key of the public key cache.
139141
- `cfKVNamespace` specifies the KV namespace which is bound your workers.
140142

143+
### `createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions, env?: EmulatorEnv): Promise<string>`
144+
145+
Creates a new Firebase session cookie with the specified options. The created JWT string can be set as a server-side session cookie with a custom cookie policy, and be used for session management. The session cookie JWT will have the same payload claims as the provided ID token. See [Manage Session Cookies](https://firebase.google.com/docs/auth/admin/manage-cookies) for code samples and detailed documentation.
146+
147+
- `idToken` The Firebase ID token to exchange for a session cookie.
148+
- `sessionCookieOptions` The session cookie options which includes custom session duration.
149+
- `env` is an optional parameter. but this is using to detect should use emulator or not.
150+
151+
**Required** service acccount credential to use this API. You need to set the credentials with `Auth.getOrInitialize`.
152+
153+
### `verifySessionCookie(sessionCookie: string, env?: EmulatorEnv): Promise<FirebaseIdToken>`
154+
155+
Verifies a Firebase session cookie. Returns a Promise with the cookie claims. Rejects the promise if the cookie could not be verified.
156+
157+
See [Verify Session Cookies](https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions) for code samples and detailed documentation.
158+
159+
- `sessionCookie` The session cookie to verify.
160+
- `env` is an optional parameter. but this is using to detect should use emulator or not.
161+
141162
### `emulatorHost(env?: EmulatorEnv): string | undefined`
142163

143164
Returns the host of your Firebase Auth Emulator. For example, this case returns `"127.0.0.1:9099"` if you configured like below.
@@ -185,14 +206,20 @@ Interface representing a decoded Firebase ID token, returned from the `authObj.v
185206
I put an [example](https://github.com/Code-Hex/firebase-auth-cloudflare-workers/tree/master/example) directory as Module Worker Syntax. this is explanation how to run the code.
186207

187208
1. Clone this repository and change your directory to it.
188-
2. Install dev dependencies as `yarn` command.
189-
3. Run firebase auth emulator by `$ yarn start-firebase-emulator`
209+
2. Install dev dependencies as `pnpm` command.
210+
3. Run firebase auth emulator by `$ pnpm start-firebase-emulator`
190211
4. Access to Emulator UI in your favorite browser.
191212
5. Create a new user on Emulator UI. (email: `[email protected]` password: `test1234`)
192-
6. Run example code on local (may serve as `localhost:8787`) by `$ yarn start-example`
213+
6. Run example code on local (may serve as `localhost:8787`) by `$ pnpm start-example`
193214
7. Get jwt for created user by `$ curl -s http://localhost:8787/get-jwt | jq .idToken -r`
194215
8. Try authorization with user jwt `$ curl http://localhost:8787/ -H 'Authorization: Bearer PASTE-JWT-HERE'`
195216

217+
### for Session Cookie
218+
219+
You can try session cookie with your browser.
220+
221+
Access to `/admin/login` after started up Emulator and created an account (email: `[email protected]` password: `test1234`).
222+
196223
## Todo
197224

198225
### Non-required service account key.

example/index.ts

Lines changed: 171 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,196 @@
1-
import type { EmulatorEnv } from '../src';
2-
import { Auth, emulatorHost, WorkersKVStoreSingle } from '../src';
1+
import { Hono } from 'hono';
2+
import { getCookie, setCookie } from 'hono/cookie';
3+
import { csrf } from 'hono/csrf';
4+
import { html } from 'hono/html';
5+
import { Auth, EmulatorCredential, emulatorHost, WorkersKVStoreSingle } from '../src';
36

4-
interface Bindings extends EmulatorEnv {
7+
type Env = {
58
EMAIL_ADDRESS: string;
69
PASSWORD: string;
7-
FIREBASE_AUTH_EMULATOR_HOST: string;
810
PUBLIC_JWK_CACHE_KV: KVNamespace;
911
PROJECT_ID: string;
1012
PUBLIC_JWK_CACHE_KEY: string;
11-
}
13+
14+
FIREBASE_AUTH_EMULATOR_HOST: string; // satisfied EmulatorEnv
15+
};
16+
17+
const app = new Hono<{ Bindings: Env }>();
1218

1319
const signInPath = '/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=test1234';
1420

15-
export async function handleRequest(req: Request, env: Bindings) {
16-
const url = new URL(req.url);
17-
const firebaseEmuHost = emulatorHost(env);
18-
if (url.pathname === '/get-jwt' && !!firebaseEmuHost) {
19-
const firebaseEmulatorSignInUrl = 'http://' + firebaseEmuHost + signInPath;
20-
const resp = await fetch(firebaseEmulatorSignInUrl, {
21-
method: 'POST',
22-
body: JSON.stringify({
23-
email: env.EMAIL_ADDRESS,
24-
password: env.PASSWORD,
25-
returnSecureToken: true,
26-
}),
27-
headers: {
28-
'Content-Type': 'application/json',
29-
},
30-
});
31-
return resp;
32-
}
21+
app.get('/get-jwt', async c => {
22+
const firebaseEmuHost = emulatorHost(c.env);
23+
const firebaseEmulatorSignInUrl = 'http://' + firebaseEmuHost + signInPath;
24+
return await fetch(firebaseEmulatorSignInUrl, {
25+
method: 'POST',
26+
body: JSON.stringify({
27+
email: c.env.EMAIL_ADDRESS,
28+
password: c.env.PASSWORD,
29+
returnSecureToken: true,
30+
}),
31+
headers: {
32+
'Content-Type': 'application/json',
33+
},
34+
});
35+
});
3336

34-
const authorization = req.headers.get('Authorization');
37+
app.post('/verify-header', async c => {
38+
const authorization = c.req.raw.headers.get('Authorization');
3539
if (authorization === null) {
3640
return new Response(null, {
3741
status: 400,
3842
});
3943
}
4044
const jwt = authorization.replace(/Bearer\s+/i, '');
4145
const auth = Auth.getOrInitialize(
42-
env.PROJECT_ID,
43-
WorkersKVStoreSingle.getOrInitialize(env.PUBLIC_JWK_CACHE_KEY, env.PUBLIC_JWK_CACHE_KV)
46+
c.env.PROJECT_ID,
47+
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV)
4448
);
45-
const firebaseToken = await auth.verifyIdToken(jwt, env);
49+
const firebaseToken = await auth.verifyIdToken(jwt, c.env);
4650

4751
return new Response(JSON.stringify(firebaseToken), {
4852
headers: {
4953
'Content-Type': 'application/json',
5054
},
5155
});
52-
}
56+
});
57+
58+
app.use('/admin/*', csrf());
59+
60+
app.get('/admin/login', async c => {
61+
const content = await html`<html>
62+
<head>
63+
<meta charset="UTF-8" />
64+
<title>Login</title>
65+
</head>
66+
<body>
67+
<h1>Login Page</h1>
68+
<button id="sign-in" type="button">Sign-In</button>
69+
<script type="module">
70+
// See https://firebase.google.com/docs/auth/admin/manage-cookies
71+
//
72+
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.5.0/firebase-app.js';
73+
import $ from 'https://cdn.skypack.dev/jquery';
74+
// Add Firebase products that you want to use
75+
import {
76+
getAuth,
77+
signInWithEmailAndPassword,
78+
onAuthStateChanged,
79+
connectAuthEmulator,
80+
signOut,
81+
setPersistence,
82+
inMemoryPersistence,
83+
} from 'https://www.gstatic.com/firebasejs/10.5.0/firebase-auth.js';
84+
const app = initializeApp({
85+
apiKey: 'test1234',
86+
authDomain: 'test',
87+
projectId: 'project12345',
88+
});
89+
const auth = getAuth(app);
90+
connectAuthEmulator(auth, 'http://127.0.0.1:9099');
91+
setPersistence(auth, inMemoryPersistence);
92+
93+
/**
94+
* @param {string} name The cookie name.
95+
* @return {?string} The corresponding cookie value to lookup.
96+
*/
97+
function getCookie(name) {
98+
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
99+
return v ? v[2] : null;
100+
}
101+
102+
/**
103+
* @param {string} url The session login endpoint.
104+
* @param {string} idToken The ID token to post to backend.
105+
* @return {any} A jQuery promise that resolves on completion.
106+
*/
107+
function postIdTokenToSessionLogin(url, idToken) {
108+
// POST to session login endpoint.
109+
return $.ajax({
110+
type: 'POST',
111+
url: url,
112+
data: JSON.stringify({ idToken: idToken }),
113+
contentType: 'application/json',
114+
});
115+
}
116+
117+
$('#sign-in').on('click', function () {
118+
console.log('clicked');
119+
120+
signInWithEmailAndPassword(auth, '[email protected]', 'test1234')
121+
.then(({ user }) => {
122+
// Get the user's ID token as it is needed to exchange for a session cookie.
123+
const idToken = user.accessToken;
124+
// Session login endpoint is queried and the session cookie is set.
125+
// CSRF protection should be taken into account.
126+
// ...
127+
const csrfToken = getCookie('csrfToken');
128+
return postIdTokenToSessionLogin('/admin/login_session', idToken, csrfToken);
129+
})
130+
.then(() => {
131+
// A page redirect would suffice as the persistence is set to NONE.
132+
return signOut(auth);
133+
})
134+
.then(() => {
135+
window.location.assign('/admin/profile');
136+
});
137+
});
138+
</script>
139+
</body>
140+
</html>`;
141+
return c.html(content);
142+
});
143+
144+
app.post('/admin/login_session', async c => {
145+
const json = await c.req.json();
146+
const idToken = json.idToken;
147+
if (!idToken || typeof idToken !== 'string') {
148+
return c.json({ message: 'invalid idToken' }, 400);
149+
}
150+
// Set session expiration to 5 days.
151+
const expiresIn = 60 * 60 * 24 * 5 * 1000;
152+
// Create the session cookie. This will also verify the ID token in the process.
153+
// The session cookie will have the same claims as the ID token.
154+
// To only allow session cookie setting on recent sign-in, auth_time in ID token
155+
// can be checked to ensure user was recently signed in before creating a session cookie.
156+
const auth = Auth.getOrInitialize(
157+
c.env.PROJECT_ID,
158+
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV),
159+
new EmulatorCredential() // You MUST use ServiceAccountCredential in real world
160+
);
161+
const sessionCookie = await auth.createSessionCookie(
162+
idToken,
163+
{
164+
expiresIn,
165+
},
166+
c.env // This valus must be removed in real world
167+
);
168+
setCookie(c, 'session', sessionCookie, {
169+
maxAge: expiresIn,
170+
httpOnly: true,
171+
// secure: true // set this in real world
172+
});
173+
return c.json({ message: 'success' });
174+
});
175+
176+
app.get('/admin/profile', async c => {
177+
const session = getCookie(c, 'session') ?? '';
178+
179+
const auth = Auth.getOrInitialize(
180+
c.env.PROJECT_ID,
181+
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV),
182+
new EmulatorCredential() // You MUST use ServiceAccountCredential in real world
183+
);
184+
185+
try {
186+
const decodedToken = await auth.verifySessionCookie(
187+
session,
188+
c.env // This valus must be removed in real world
189+
);
190+
return c.json(decodedToken);
191+
} catch (err) {
192+
return c.redirect('/admin/login');
193+
}
194+
});
53195

54-
export default { fetch: handleRequest };
196+
export default app;

example/wrangler.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name = "firebase-auth-example"
2-
compatibility_date = "2022-07-05"
2+
compatibility_date = "2023-12-01"
33
workers_dev = true
4+
main = "index.ts"
45

56
tsconfig = "./tsconfig.json"
67

@@ -21,12 +22,12 @@ FIREBASE_AUTH_EMULATOR_HOST = "127.0.0.1:9099"
2122
EMAIL_ADDRESS = "[email protected]"
2223
PASSWORD = "test1234"
2324

24-
PROJECT_ID = "example-project12345" # see package.json (for emulator)
25+
PROJECT_ID = "project12345" # see package.json (for emulator)
2526

2627
# Specify cache key to store and get public jwk.
2728
PUBLIC_JWK_CACHE_KEY = "public-jwk-cache-key"
2829

2930
[[kv_namespaces]]
3031
binding = "PUBLIC_JWK_CACHE_KV"
31-
id = ""
32+
id = "testingId"
3233
preview_id = "testingId"

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@
1414
],
1515
"scripts": {
1616
"test": "vitest run",
17+
"test-with-emulator": "firebase emulators:exec --project project12345 'vitest run'",
1718
"build": "run-p build:*",
1819
"build:main": "tsc -p tsconfig.main.json",
1920
"build:module": "tsc -p tsconfig.module.json",
20-
"start-firebase-emulator": "firebase emulators:start --project example-project12345",
21+
"start-firebase-emulator": "firebase emulators:start --project project12345",
2122
"start-example": "wrangler dev example/index.ts --config=example/wrangler.toml --local=true",
2223
"prettier": "prettier --write --list-different \"**/*.ts\"",
2324
"prettier:check": "prettier --check \"**/*.ts\"",
2425
"lint": "eslint --ext .ts .",
2526
"lint-fix": "eslint --fix --ext .ts .",
26-
"prepublish": "run-p build:*"
27+
"prepublish": "run-p build:*",
28+
"wrangler": "wrangler"
2729
},
2830
"devDependencies": {
2931
"@cloudflare/workers-types": "^4.20240208.0",
@@ -36,10 +38,12 @@
3638
"eslint-plugin-eslint-comments": "^3.2.0",
3739
"eslint-plugin-import": "^2.29.1",
3840
"firebase-tools": "^13.3.0",
41+
"hono": "^4.0.4",
3942
"miniflare": "^3.20240129.3",
4043
"npm-run-all": "^4.1.5",
4144
"prettier": "^3.2.5",
4245
"typescript": "^5.3.3",
46+
"undici": "^6.6.2",
4347
"vitest": "^1.3.0",
4448
"wrangler": "^3.28.3"
4549
},

0 commit comments

Comments
 (0)