-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12 from Code-Hex/add/cookie-verify
Supported cookie verification
- Loading branch information
Showing
28 changed files
with
1,707 additions
and
181 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,4 +17,5 @@ lerna-debug.log* | |
*.tsbuildinfo | ||
|
||
# Dependency directories | ||
node_modules/ | ||
node_modules/ | ||
.wrangler/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,12 +110,13 @@ $ npm i firebase-auth-cloudflare-workers | |
|
||
## API | ||
|
||
### `Auth.getOrInitialize(projectId: string, keyStore: KeyStorer): Auth` | ||
### `Auth.getOrInitialize(projectId: string, keyStore: KeyStorer, credential?: Credential): Auth` | ||
|
||
Auth is created as a singleton object. This is because the Module Worker syntax only use environment variables at the time of request. | ||
|
||
- `projectId` specifies the ID of the project for which firebase auth is used. | ||
- `keyStore` is used to cache the public key used to validate the Firebase ID token (JWT). | ||
- `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. | ||
|
||
See official document for project ID: https://firebase.google.com/docs/projects/learn-more#project-identifiers | ||
|
||
|
@@ -125,6 +126,7 @@ Verifies a Firebase ID token (JWT). If the token is valid, the promise is fulfil | |
|
||
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. | ||
|
||
- `idToken` The ID token to verify. | ||
- `env` is an optional parameter. but this is using to detect should use emulator or not. | ||
|
||
### `WorkersKVStoreSingle.getOrInitialize(cacheKey: string, cfKVNamespace: KVNamespace): WorkersKVStoreSingle` | ||
|
@@ -138,6 +140,25 @@ This is implemented `KeyStorer` interface. | |
- `cacheKey` specifies the key of the public key cache. | ||
- `cfKVNamespace` specifies the KV namespace which is bound your workers. | ||
|
||
### `createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions, env?: EmulatorEnv): Promise<string>` | ||
|
||
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. | ||
|
||
- `idToken` The Firebase ID token to exchange for a session cookie. | ||
- `sessionCookieOptions` The session cookie options which includes custom session duration. | ||
- `env` is an optional parameter. but this is using to detect should use emulator or not. | ||
|
||
**Required** service acccount credential to use this API. You need to set the credentials with `Auth.getOrInitialize`. | ||
|
||
### `verifySessionCookie(sessionCookie: string, env?: EmulatorEnv): Promise<FirebaseIdToken>` | ||
|
||
Verifies a Firebase session cookie. Returns a Promise with the cookie claims. Rejects the promise if the cookie could not be verified. | ||
|
||
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. | ||
|
||
- `sessionCookie` The session cookie to verify. | ||
- `env` is an optional parameter. but this is using to detect should use emulator or not. | ||
|
||
### `emulatorHost(env?: EmulatorEnv): string | undefined` | ||
|
||
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 | |
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. | ||
|
||
1. Clone this repository and change your directory to it. | ||
2. Install dev dependencies as `yarn` command. | ||
3. Run firebase auth emulator by `$ yarn start-firebase-emulator` | ||
2. Install dev dependencies as `pnpm` command. | ||
3. Run firebase auth emulator by `$ pnpm start-firebase-emulator` | ||
4. Access to Emulator UI in your favorite browser. | ||
5. Create a new user on Emulator UI. (email: `[email protected]` password: `test1234`) | ||
6. Run example code on local (may serve as `localhost:8787`) by `$ yarn start-example` | ||
6. Run example code on local (may serve as `localhost:8787`) by `$ pnpm start-example` | ||
7. Get jwt for created user by `$ curl -s http://localhost:8787/get-jwt | jq .idToken -r` | ||
8. Try authorization with user jwt `$ curl http://localhost:8787/ -H 'Authorization: Bearer PASTE-JWT-HERE'` | ||
|
||
### for Session Cookie | ||
|
||
You can try session cookie with your browser. | ||
|
||
Access to `/admin/login` after started up Emulator and created an account (email: `[email protected]` password: `test1234`). | ||
|
||
## Todo | ||
|
||
### Non-required service account key. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,196 @@ | ||
import type { EmulatorEnv } from '../src'; | ||
import { Auth, emulatorHost, WorkersKVStoreSingle } from '../src'; | ||
import { Hono } from 'hono'; | ||
import { getCookie, setCookie } from 'hono/cookie'; | ||
import { csrf } from 'hono/csrf'; | ||
import { html } from 'hono/html'; | ||
import { Auth, EmulatorCredential, emulatorHost, WorkersKVStoreSingle } from '../src'; | ||
|
||
interface Bindings extends EmulatorEnv { | ||
type Env = { | ||
EMAIL_ADDRESS: string; | ||
PASSWORD: string; | ||
FIREBASE_AUTH_EMULATOR_HOST: string; | ||
PUBLIC_JWK_CACHE_KV: KVNamespace; | ||
PROJECT_ID: string; | ||
PUBLIC_JWK_CACHE_KEY: string; | ||
} | ||
|
||
FIREBASE_AUTH_EMULATOR_HOST: string; // satisfied EmulatorEnv | ||
}; | ||
|
||
const app = new Hono<{ Bindings: Env }>(); | ||
|
||
const signInPath = '/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=test1234'; | ||
|
||
export async function handleRequest(req: Request, env: Bindings) { | ||
const url = new URL(req.url); | ||
const firebaseEmuHost = emulatorHost(env); | ||
if (url.pathname === '/get-jwt' && !!firebaseEmuHost) { | ||
const firebaseEmulatorSignInUrl = 'http://' + firebaseEmuHost + signInPath; | ||
const resp = await fetch(firebaseEmulatorSignInUrl, { | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
email: env.EMAIL_ADDRESS, | ||
password: env.PASSWORD, | ||
returnSecureToken: true, | ||
}), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
return resp; | ||
} | ||
app.get('/get-jwt', async c => { | ||
const firebaseEmuHost = emulatorHost(c.env); | ||
const firebaseEmulatorSignInUrl = 'http://' + firebaseEmuHost + signInPath; | ||
return await fetch(firebaseEmulatorSignInUrl, { | ||
method: 'POST', | ||
body: JSON.stringify({ | ||
email: c.env.EMAIL_ADDRESS, | ||
password: c.env.PASSWORD, | ||
returnSecureToken: true, | ||
}), | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
}); | ||
|
||
const authorization = req.headers.get('Authorization'); | ||
app.post('/verify-header', async c => { | ||
const authorization = c.req.raw.headers.get('Authorization'); | ||
if (authorization === null) { | ||
return new Response(null, { | ||
status: 400, | ||
}); | ||
} | ||
const jwt = authorization.replace(/Bearer\s+/i, ''); | ||
const auth = Auth.getOrInitialize( | ||
env.PROJECT_ID, | ||
WorkersKVStoreSingle.getOrInitialize(env.PUBLIC_JWK_CACHE_KEY, env.PUBLIC_JWK_CACHE_KV) | ||
c.env.PROJECT_ID, | ||
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV) | ||
); | ||
const firebaseToken = await auth.verifyIdToken(jwt, env); | ||
const firebaseToken = await auth.verifyIdToken(jwt, c.env); | ||
|
||
return new Response(JSON.stringify(firebaseToken), { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
} | ||
}); | ||
|
||
app.use('/admin/*', csrf()); | ||
|
||
app.get('/admin/login', async c => { | ||
const content = await html`<html> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<title>Login</title> | ||
</head> | ||
<body> | ||
<h1>Login Page</h1> | ||
<button id="sign-in" type="button">Sign-In</button> | ||
<script type="module"> | ||
// See https://firebase.google.com/docs/auth/admin/manage-cookies | ||
// | ||
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.5.0/firebase-app.js'; | ||
import $ from 'https://cdn.skypack.dev/jquery'; | ||
// Add Firebase products that you want to use | ||
import { | ||
getAuth, | ||
signInWithEmailAndPassword, | ||
onAuthStateChanged, | ||
connectAuthEmulator, | ||
signOut, | ||
setPersistence, | ||
inMemoryPersistence, | ||
} from 'https://www.gstatic.com/firebasejs/10.5.0/firebase-auth.js'; | ||
const app = initializeApp({ | ||
apiKey: 'test1234', | ||
authDomain: 'test', | ||
projectId: 'project12345', | ||
}); | ||
const auth = getAuth(app); | ||
connectAuthEmulator(auth, 'http://127.0.0.1:9099'); | ||
setPersistence(auth, inMemoryPersistence); | ||
/** | ||
* @param {string} name The cookie name. | ||
* @return {?string} The corresponding cookie value to lookup. | ||
*/ | ||
function getCookie(name) { | ||
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)'); | ||
return v ? v[2] : null; | ||
} | ||
/** | ||
* @param {string} url The session login endpoint. | ||
* @param {string} idToken The ID token to post to backend. | ||
* @return {any} A jQuery promise that resolves on completion. | ||
*/ | ||
function postIdTokenToSessionLogin(url, idToken) { | ||
// POST to session login endpoint. | ||
return $.ajax({ | ||
type: 'POST', | ||
url: url, | ||
data: JSON.stringify({ idToken: idToken }), | ||
contentType: 'application/json', | ||
}); | ||
} | ||
$('#sign-in').on('click', function () { | ||
console.log('clicked'); | ||
signInWithEmailAndPassword(auth, '[email protected]', 'test1234') | ||
.then(({ user }) => { | ||
// Get the user's ID token as it is needed to exchange for a session cookie. | ||
const idToken = user.accessToken; | ||
// Session login endpoint is queried and the session cookie is set. | ||
// CSRF protection should be taken into account. | ||
// ... | ||
const csrfToken = getCookie('csrfToken'); | ||
return postIdTokenToSessionLogin('/admin/login_session', idToken, csrfToken); | ||
}) | ||
.then(() => { | ||
// A page redirect would suffice as the persistence is set to NONE. | ||
return signOut(auth); | ||
}) | ||
.then(() => { | ||
window.location.assign('/admin/profile'); | ||
}); | ||
}); | ||
</script> | ||
</body> | ||
</html>`; | ||
return c.html(content); | ||
}); | ||
|
||
app.post('/admin/login_session', async c => { | ||
const json = await c.req.json(); | ||
const idToken = json.idToken; | ||
if (!idToken || typeof idToken !== 'string') { | ||
return c.json({ message: 'invalid idToken' }, 400); | ||
} | ||
// Set session expiration to 5 days. | ||
const expiresIn = 60 * 60 * 24 * 5 * 1000; | ||
// Create the session cookie. This will also verify the ID token in the process. | ||
// The session cookie will have the same claims as the ID token. | ||
// To only allow session cookie setting on recent sign-in, auth_time in ID token | ||
// can be checked to ensure user was recently signed in before creating a session cookie. | ||
const auth = Auth.getOrInitialize( | ||
c.env.PROJECT_ID, | ||
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV), | ||
new EmulatorCredential() // You MUST use ServiceAccountCredential in real world | ||
); | ||
const sessionCookie = await auth.createSessionCookie( | ||
idToken, | ||
{ | ||
expiresIn, | ||
}, | ||
c.env // This valus must be removed in real world | ||
); | ||
setCookie(c, 'session', sessionCookie, { | ||
maxAge: expiresIn, | ||
httpOnly: true, | ||
// secure: true // set this in real world | ||
}); | ||
return c.json({ message: 'success' }); | ||
}); | ||
|
||
app.get('/admin/profile', async c => { | ||
const session = getCookie(c, 'session') ?? ''; | ||
|
||
const auth = Auth.getOrInitialize( | ||
c.env.PROJECT_ID, | ||
WorkersKVStoreSingle.getOrInitialize(c.env.PUBLIC_JWK_CACHE_KEY, c.env.PUBLIC_JWK_CACHE_KV), | ||
new EmulatorCredential() // You MUST use ServiceAccountCredential in real world | ||
); | ||
|
||
try { | ||
const decodedToken = await auth.verifySessionCookie( | ||
session, | ||
c.env // This valus must be removed in real world | ||
); | ||
return c.json(decodedToken); | ||
} catch (err) { | ||
return c.redirect('/admin/login'); | ||
} | ||
}); | ||
|
||
export default { fetch: handleRequest }; | ||
export default app; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
name = "firebase-auth-example" | ||
compatibility_date = "2022-07-05" | ||
compatibility_date = "2023-12-01" | ||
workers_dev = true | ||
main = "index.ts" | ||
|
||
tsconfig = "./tsconfig.json" | ||
|
||
|
@@ -21,12 +22,12 @@ FIREBASE_AUTH_EMULATOR_HOST = "127.0.0.1:9099" | |
EMAIL_ADDRESS = "[email protected]" | ||
PASSWORD = "test1234" | ||
|
||
PROJECT_ID = "example-project12345" # see package.json (for emulator) | ||
PROJECT_ID = "project12345" # see package.json (for emulator) | ||
|
||
# Specify cache key to store and get public jwk. | ||
PUBLIC_JWK_CACHE_KEY = "public-jwk-cache-key" | ||
|
||
[[kv_namespaces]] | ||
binding = "PUBLIC_JWK_CACHE_KV" | ||
id = "" | ||
id = "testingId" | ||
preview_id = "testingId" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.