$ npm install @passkeys/passport-simple-webauthnPassport strategy that uses @simplewebauthn/server to authenticate users based on the webauthn spec.
The strategy requires a store to persist challenges. It should implement the following methods:
interface ChallengeStore {
set(req: Request, challenge: string): Promise<void> // store the challenge for the given request
verify(req: Request, signedChallenge: string): Promise<boolean> // verify the signed challenge for the given request
}
The ChallengeSessionStore is shipped with the package and can be used as a store. It stores challenges in the session and therefore requires a session middleware such as express-session to be used.
Initialize the challenge store and the strategy and implement the register and getAuthenticator methods which are
called when a new credential is registered, and when a user authenticates respectively:
// auth.js
const challengeStore = new ChallengeSessionStore()
export const webauthnStrategy = new SimpleWebauthnStrategy({
challengeStore,
relyingParties: [{ id: new URL(env.SIGNER_ORIGIN).hostname, origin: env.SIGNER_ORIGIN }],
allowLocalOrigin: true, // set this to true if you want to allow localhost as a valid origin
register: async ({ registrationInfo, request }) => {
const [passkey] = await db.passkey
.insert({
id: request.session.userId,
credentialId: Buffer.from(registrationInfo.credentialID).toString('base64url'),
publicKey: registrationInfo.credentialPublicKey,
})
.transacting(transaction)
// return whatever you want to use as req.user
return { id: passkey.id }
},
getAuthenticator: async ({ credential }) => {
const passkey = await db.passkey
.where('passkey.id', '=', credential.response.userHandle)
.first()
return {
// these properties will be made available as req.user
user: { id: passkey.id },
// the authenticator is required to verify the signed challenge, both credentialPublicKey and credentialID are required
authenticator: {
credentialPublicKey: passkey.publicKey,
credentialID: Buffer.from(passkey.credentialId, 'base64url'),
},
}
},
})
Apply the strategy to your express app:
// server.js
import passport from 'passport'
import express from 'express'
import session from 'express-session'
import { webauthnStrategy } from './auth.js'
const app = express()
app.use(session({ secret: process.env.SESSION_SECRET }))
app.use(passport.initialize())
app.use(passport.authenticate('session'))
passport.use(webauthnStrategy)
Create a route each to return registration options including a challenge and login challenges:
router.post('/auth/challenge/register', async (req, res) => {
const options = await webauthnStrategy.registrationChallenge({
request: req,
userId: someUserId,
userName,
rpName,
rpID,
})
return res.json(options)
})
router.post('/auth/challenge/login', async (req, res) => {
const options = await webauthnStrategy.loginChallenge({ request: req, rpID })
return res.json(options)
})
And last a route to authenticate which can be used for both registration and login:
import { ALL_CLIENT_DATA_TYPES } from '@passkeys/passport-simple-webauthn'
router.post('/auth', async (req, res, next) =>
passport.authenticate(
'simple-webauthn',
{ type: ALL_CLIENT_DATA_TYPES },
async (err, user, message, status) => {
if (!user || err) {
return res.status(status).json({ error: err ?? message })
}
return req.login(user, async (err) => {
if (err) {
return next(err)
}
return res.json({ id: user.id })
})
}
)(req, res, next)
)
Or alternatively a separate route for registration and login:
router.post('/auth/register', async (req, res, next) =>
passport.authenticate(
'simple-webauthn',
{ type: 'webauthn.create' },
async (err, user, message, status) => {
// ...
}
)(req, res, next)
)
router.post('/auth/login', async (req, res, next) =>
passport.authenticate(
'simple-webauthn',
{ type: 'webauthn.get' },
async (err, user, message, status) => {
// ...
}
)(req, res, next)
)
To protect a route you can use a simple authentication middleware:
// middleware.js
export function ensureLoggedIn() {
return function (req, res, next) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
return res.sendStatus(401)
}
next()
}
}
And apply it where needed:
router.get('/whoami', ensureLoggedIn(), (req, res) => {
return res.json({ id: req.user.id })
})
If you require logging, you can pass a logger instance complying with the Logger interface in /src/utils/logger.ts to the strategy and/or the challenge store.