Skip to content

Commit

Permalink
fix(auth): correct verification of token id
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jul 13, 2023
1 parent 0b83399 commit fd2050b
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 63 deletions.
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
if (nuxt.options.ssr && hasServiceAccount) {
addServerHandler({
route: '/api/__session',
handler: resolve(runtimeDir, './auth/api.session'),
// handler: resolve(runtimeDir, './auth/api.session'),
handler: resolve(runtimeDir, './auth/api.session-verification'),
})

// must be added after (which means before in code) the plugin module
Expand Down
51 changes: 23 additions & 28 deletions packages/nuxt/src/runtime/admin/plugin-auth-user.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
AUTH_COOKIE_NAME,
} from 'vuefire/server'
import { getCookie } from 'h3'
import { UserSymbol } from '../constants'
import { DECODED_ID_TOKEN_SYMBOL, UserSymbol } from '../constants'
import { log } from '../logging'
import { defineNuxtPlugin, useRequestEvent } from '#app'

// TODO: move this to auth and adapt the module to load it in the right order

/**
* Check if there is a cookie and if it is valid, extracts the user from it. This only requires the admin app.
*/
Expand All @@ -25,36 +27,29 @@ export default defineNuxtPlugin(async (nuxtApp) => {
adminApp
)

const user = await Promise.resolve(
decodedToken && adminAuth.getUser(decodedToken.uid)
).catch((err) => {
log('error', 'Error getting user', err)
// consider the user as not logged in and avoid a 500
return null
})
// const user = await Promise.resolve(
// decodedToken && adminAuth.getUser(decodedToken.uid)
// ).catch((err) => {
// log('error', 'Error getting user', err)
// // consider the user as not logged in and avoid a 500
// return null
// })

// // expose the user to code
// event.context.user = user
// // for SSR
// nuxtApp.payload.vuefireUser = user?.toJSON()

// expose the user to code
event.context.user = user
// for SSR
nuxtApp.payload.vuefireUser = user?.toJSON()
// // log('debug', '🧍 User on server', user?.displayName || user?.uid)

// log('debug', '🧍 User on server', user?.displayName || user?.uid)
// // user that has a similar shape for client and server code
// nuxtApp[
// // we cannot use symbol to index
// UserSymbol as unknown as string
// ] = createServerUser(user)

// user that has a similar shape for client and server code
nuxtApp[
// we cannot use symbol to index
UserSymbol as unknown as string
] = createServerUser(user)
DECODED_ID_TOKEN_SYMBOL as unknown as string
] = decodedToken
})

// TODO: should the type extensions be added in a different way to the module?
declare module 'h3' {
interface H3EventContext {
/**
* Firebase Admin User Record. `null` if the user is not logged in or their token is no longer valid and requires a
* refresh.
* @experimental This API is experimental and may change in future releases.
*/
user: UserRecord | null
}
}
14 changes: 12 additions & 2 deletions packages/nuxt/src/runtime/admin/plugin.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { App as AdminApp } from 'firebase-admin/app'
import { getAdminApp } from 'vuefire/server'
import { defineNuxtPlugin, useAppConfig, useRequestEvent } from '#app'
import {
defineNuxtPlugin,
useAppConfig,
useRequestEvent,
useRuntimeConfig,
} from '#app'

export default defineNuxtPlugin(() => {
const event = useRequestEvent()
const { firebaseAdmin } = useAppConfig()
const runtimeConfig = useRuntimeConfig()

const firebaseAdminApp = getAdminApp(firebaseAdmin?.options)
const firebaseAdminApp = getAdminApp(
firebaseAdmin?.options,
// @ts-expect-error: FIXME:
runtimeConfig.googleApplicationCredentials
)

// TODO: Is this accessible within middlewares and api routes? or should we use a middleware to add it
event.context.firebaseApp = firebaseAdminApp
Expand Down
85 changes: 64 additions & 21 deletions packages/nuxt/src/runtime/app/plugin.server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { deleteApp, type FirebaseApp, initializeApp } from 'firebase/app'
import { getAuth, signInWithCustomToken, type User } from 'firebase/auth'
import { type App as AdminApp } from 'firebase-admin/app'
import { getAuth as getAdminAuth } from 'firebase-admin/auth'
import { DecodedIdToken, getAuth as getAdminAuth } from 'firebase-admin/auth'
import { LRUCache } from 'lru-cache'
import { log } from '../logging'
import { UserSymbol } from '../constants'
import { defineNuxtPlugin, useAppConfig } from '#app'
import { DECODED_ID_TOKEN_SYMBOL, UserSymbol } from '../constants'
import { defineNuxtPlugin, useAppConfig, useRequestEvent } from '#app'

// TODO: allow customizing
// TODO: find sensible defaults. Should they change depending on the platform?
Expand All @@ -27,12 +27,19 @@ const appCache = new LRUCache<string, FirebaseApp>({
*/
export default defineNuxtPlugin(async (nuxtApp) => {
const appConfig = useAppConfig()
const event = useRequestEvent()

const user = nuxtApp[
const decodedToken = nuxtApp[
// we cannot use a symbol to index
UserSymbol as unknown as string
] as User | undefined | null
const uid = user?.uid
DECODED_ID_TOKEN_SYMBOL as unknown as string
] as DecodedIdToken | null | undefined

const uid = decodedToken?.uid

// // expose the user to code
// event.context.user = user
// // for SSR
// nuxtApp.payload.vuefireUser = user?.toJSON()

let firebaseApp: FirebaseApp | undefined

Expand All @@ -42,29 +49,53 @@ export default defineNuxtPlugin(async (nuxtApp) => {
if (!firebaseApp) {
const randomId = Math.random().toString(36).slice(2)
// TODO: do we need a randomId?
const appName = `auth:${user.uid}:${randomId}`
const appName = `auth:${uid}:${randomId}`

// log('log', '👤 creating new app', appName)
log('debug', '👤 creating new app', appName)

appCache.set(uid, initializeApp(appConfig.firebaseConfig, appName))
firebaseApp = appCache.get(uid)!
const firebaseAdminApp = nuxtApp.$firebaseAdminApp as AdminApp
const adminAuth = getAdminAuth(firebaseAdminApp)
// console.time('token')
const customToken = await adminAuth.createCustomToken(user.uid)
} else {
log('debug', '👤 reusing authenticated app', uid)
}
// TODO: this should only happen if user enabled auth in config
const firebaseAdminApp = nuxtApp.$firebaseAdminApp as AdminApp
const adminAuth = getAdminAuth(firebaseAdminApp)
const auth = getAuth(firebaseApp)

// reauthenticate if the user is not the same (e.g. invalidated)
if (auth.currentUser?.uid !== uid) {
const customToken = await adminAuth
.createCustomToken(uid)
.catch((err) => {
log('error', 'Error creating custom token', err)
return null
})
// console.timeLog('token', `got token for ${user.uid}`)
const credentials = await signInWithCustomToken(
getAuth(firebaseApp),
customToken
)
// console.timeLog('token', `signed in with token for ${user.uid}`)
// console.timeEnd('token')
// TODO: token expiration (1h)
if (customToken) {
const auth = getAuth(firebaseApp)
await signInWithCustomToken(auth, customToken)
// console.timeLog('token', `signed in with token for ${user.uid}`)
// console.timeEnd('token')
// TODO: token expiration (1h)
}
}
nuxtApp[
// we cannot use a symbol to index
UserSymbol as unknown as string
] = auth.currentUser
// expose the user to code
event.context.user = auth.currentUser
// for SSR
nuxtApp.payload.vuefireUser = auth.currentUser?.toJSON()
} else {
if (!appCache.has('')) {
appCache.set('', initializeApp(appConfig.firebaseConfig))
}
firebaseApp = appCache.get('')!
// anonymous session, just create a new app
// log('log', '🥸 anonymous session')
firebaseApp = initializeApp(appConfig.firebaseConfig)
log('debug', '🥸 anonymous session')
}

return {
Expand All @@ -73,3 +104,15 @@ export default defineNuxtPlugin(async (nuxtApp) => {
},
}
})

// TODO: should the type extensions be added in a different way to the module?
declare module 'h3' {
interface H3EventContext {
/**
* Firebase Admin User Record. `null` if the user is not logged in or their token is no longer valid and requires a
* refresh.
* @experimental This API is experimental and may change in future releases.
*/
user: User | null
}
}
11 changes: 4 additions & 7 deletions packages/nuxt/src/runtime/auth/api.session-verification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getApp } from 'firebase-admin/app'
import { getApp as getAdminApp } from 'firebase-admin/app'
import { getAuth as getAdminAuth } from 'firebase-admin/auth'
import {
readBody,
Expand All @@ -19,17 +19,14 @@ export default defineEventHandler(async (event) => {
const { token } = await readBody(event)

// log('debug', 'minting a session cookie')
const adminApp = getApp()
const adminApp = getAdminApp()
const adminAuth = getAdminAuth(adminApp)

// log('debug', 'read idToken from Authorization header', token)
const verifiedIdToken = token ? await adminAuth.verifyIdToken(token) : null

if (verifiedIdToken) {
if (
new Date().getTime() / 1_000 - verifiedIdToken.auth_time >
ID_TOKEN_MAX_AGE
) {
if (new Date().getTime() / 1_000 - verifiedIdToken.iat > ID_TOKEN_MAX_AGE) {
event.node.res.statusCode = 301
return ''
} else {
Expand All @@ -39,7 +36,7 @@ export default defineEventHandler(async (event) => {
log('error', 'Error minting the cookie -', e.message)
})
if (cookie) {
// log('debug', 'minted a session cookie', cookie)
log('debug', `minted a session cookie for user ${verifiedIdToken.uid}`)
setCookie(event, AUTH_COOKIE_NAME, cookie, {
maxAge: AUTH_COOKIE_MAX_AGE,
secure: true,
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/runtime/auth/plugin.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export default defineNuxtPlugin((nuxtApp) => {
const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp

// @ts-expect-error: FIXME: type it
console.log('🔥 Plugin auth client', nuxtApp.payload.vuefireUser)
VueFireAuth(nuxtApp.payload.vuefireUser)(firebaseApp, nuxtApp.vueApp)
})
1 change: 1 addition & 0 deletions packages/nuxt/src/runtime/auth/plugin.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { User } from 'firebase/auth'
import { VueFireAuthServer } from 'vuefire/server'
import type { App } from 'vue'
import { UserSymbol } from '../constants'
import { log } from '../logging'
import { defineNuxtPlugin } from '#app'

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/nuxt/src/runtime/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
* @internal Gets access to the user within the application. This is a symbol to keep it private for the moment.
*/
export const UserSymbol = Symbol('user')

/**
* @internal Gets access to the decoded token within the application. This is a symbol to keep it private for the moment.
*/
export const DECODED_ID_TOKEN_SYMBOL = Symbol('decodedToken')
4 changes: 2 additions & 2 deletions src/server/admin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
initializeApp,
initializeApp as initializeAdminApp,
cert,
getApp,
getApps,
Expand Down Expand Up @@ -102,7 +102,7 @@ export function getAdminApp(
// )
// throw new Error('admin-app/missing-credentials')

initializeApp({
initializeAdminApp({
// TODO: is this really going to be used?
...firebaseAdminOptions,
credential,
Expand Down
5 changes: 3 additions & 2 deletions src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export async function decodeUserToken(
try {
// TODO: should we check for the revoked status of the token here?
// we await to try/catch
return await adminAuth.verifyIdToken(token /*, checkRevoked */)
// return await adminAuth.verifyIdToken(token /*, checkRevoked */)
return await adminAuth.verifySessionCookie(token /** checkRevoked */)
} catch (err) {
// TODO: some errors should probably go higher
// ignore the error and consider the user as not logged in
Expand All @@ -127,7 +128,7 @@ export async function decodeUserToken(
// TODO: this error should be accessible somewhere to instruct the user to renew their access token
} else {
// ignore the error and consider the user as not logged in
log('error', 'Unknown Error -', err)
log('error', 'Unknown Error verifying session cookie -', err)
}
}
}
Expand Down

0 comments on commit fd2050b

Please sign in to comment.