npm package: @passkeys/passport-simple-webauthn
Description: Passport authentication strategy that uses @simplewebauthn/server
Latest version: 3.0.0

# @passkeys/passport-simple-webauthn

Passport strategy that uses `@simplewebauthn/server` to authenticate users based on the webauthn spec.

## Usage

The strategy requires a store to persist challenges. It should implement the following methods:

```ts
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.

### Usage example with express and the session challenge store

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:

```js
// 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:

```js
// 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:

```js
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:

```js
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:

```js
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:

```js
// 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:

```js
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.