From 95388860b4934c7f17f30c0586324539e68304a8 Mon Sep 17 00:00:00 2001 From: Nina Satragno Date: Fri, 10 Jan 2020 14:26:35 -0500 Subject: [PATCH] feat(WebAuthn): allow multiple credentials per user This change allows users to associate multiple credentials to their account. When performing assertions, all the credentials are sent on the `allowList`. When registering a new credential, existing credentials are sent on the `excludeList`. Updated the example to show all the credentials associated to a user. This change is not backwards compatible. --- client/Client.js | 3 ++ example/server.js | 6 ++-- example/src/AuthenticatorCard.js | 22 ------------- example/src/CredentialCard.js | 22 +++++++++++++ example/src/User.js | 22 +++++++------ src/AttestationChallengeBuilder.js | 8 +++-- src/Webauthn.js | 52 +++++++++++++++--------------- 7 files changed, 71 insertions(+), 64 deletions(-) delete mode 100644 example/src/AuthenticatorCard.js create mode 100644 example/src/CredentialCard.js diff --git a/client/Client.js b/client/Client.js index f40305a..b6c59ee 100644 --- a/client/Client.js +++ b/client/Client.js @@ -68,6 +68,9 @@ class Client { static preformatMakeCredReq (makeCredReq) { makeCredReq.challenge = base64url.decode(makeCredReq.challenge) makeCredReq.user.id = base64url.decode(makeCredReq.user.id) + for (let excludeCred of makeCredReq.excludeCredentials) { + excludeCred.id = base64url.decode(excludeCred.id) + } return makeCredReq } diff --git a/example/server.js b/example/server.js index d7e0e3d..4c501e7 100644 --- a/example/server.js +++ b/example/server.js @@ -62,10 +62,8 @@ const webauthn = new Webauthn({ app.use('/webauthn', webauthn.initialize()) // Endpoint without passport -app.get('/authenticators', webauthn.authenticate(), async (req, res) => { - res.status(200).json([ - await webauthn.store.get(req.session.username) - ].map(user => user.authenticator)) +app.get('/credentials', webauthn.authenticate(), async (req, res) => { + res.status(200).json((await webauthn.store.get(req.session.username)).credentials) }) // Debug diff --git a/example/src/AuthenticatorCard.js b/example/src/AuthenticatorCard.js deleted file mode 100644 index cf298be..0000000 --- a/example/src/AuthenticatorCard.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { Card } from 'react-bootstrap' -import 'bootstrap/dist/css/bootstrap.min.css' - -class AuthenticatorCard extends React.Component { - render () { - return ( - - - {this.props.authenticator.credID} - -

Format: {this.props.authenticator.fmt}

-

Counter: {this.props.authenticator.counter}

-

Public key: {this.props.authenticator.publicKey}

-
-
-
- ) - } -} - -export default AuthenticatorCard; diff --git a/example/src/CredentialCard.js b/example/src/CredentialCard.js new file mode 100644 index 0000000..1cab762 --- /dev/null +++ b/example/src/CredentialCard.js @@ -0,0 +1,22 @@ +import React from 'react' +import { Card } from 'react-bootstrap' +import 'bootstrap/dist/css/bootstrap.min.css' + +class CredentialCard extends React.Component { + render () { + return ( + + + {this.props.credential.credID} + +

Format: {this.props.credential.fmt}

+

Counter: {this.props.credential.counter}

+

Public key: {this.props.credential.publicKey}

+
+
+
+ ) + } +} + +export default CredentialCard; diff --git a/example/src/User.js b/example/src/User.js index 477299d..50842f4 100644 --- a/example/src/User.js +++ b/example/src/User.js @@ -1,6 +1,6 @@ import React from 'react' -import { Container, Row, Col, Button } from 'react-bootstrap' -import AuthenticatorCard from './AuthenticatorCard' +import { Container, Row, Col, Button, CardColumns } from 'react-bootstrap' +import CredentialCard from './CredentialCard' import Client from 'webauthn/client' import 'bootstrap/dist/css/bootstrap.min.css' @@ -9,10 +9,10 @@ class User extends React.Component { super(props) this.state = { - authenticators: [] + credentials: [] } - fetch('authenticators', { + fetch('credentials', { method: 'GET', credentials: 'include', }).then(response => { @@ -21,8 +21,8 @@ class User extends React.Component { return } return response.json() - }).then(authenticators => { - this.setState({ authenticators }) + }).then(credentials => { + this.setState({ credentials }) }) } @@ -36,15 +36,17 @@ class User extends React.Component {

Welcome {this.props.user.username}

-

Your authenticators:

+

Your credentials:

- {this.state.authenticators.map(authenticator => - - )} + + {this.state.credentials.map(credential => + + )} + ) } diff --git a/src/AttestationChallengeBuilder.js b/src/AttestationChallengeBuilder.js index 055d303..e8e82e3 100644 --- a/src/AttestationChallengeBuilder.js +++ b/src/AttestationChallengeBuilder.js @@ -96,11 +96,15 @@ class AttestationChallengeBuilder { } if (Array.isArray(options)) { - options.forEach(option => this.addCredentialRequest(option)) + options.forEach(option => this.addCredentialExclusion(option)) return this } - const { type, id, transports = [] } = options + const { + id, + type = PublicKeyCredentialType.PUBLIC_KEY, + transports = Object.values(AuthenticatorTransport), + } = options if ( !type diff --git a/src/Webauthn.js b/src/Webauthn.js index bc54e25..a2cee8f 100644 --- a/src/Webauthn.js +++ b/src/Webauthn.js @@ -94,31 +94,26 @@ class Webauthn { return res.status(400).json({ message: 'bad request' }) } - const user = { - id: base64url(crypto.randomBytes(32)), - [usernameField]: username, - } - - Object.entries(this.config.userFields).forEach(([bodyKey, dbKey]) => { - user[dbKey] = req.body[bodyKey] - }) - - const existing = await this.store.get(username) - if (existing && existing.authenticator) { - return res.status(403).json({ - 'status': 'failed', - 'message': `${usernameField} ${username} already exists`, + let user = await this.store.get(username) + if (!user) { + user = { + id: base64url(crypto.randomBytes(32)), + [usernameField]: username, + credentials: [], + } + Object.entries(this.config.userFields).forEach(([bodyKey, dbKey]) => { + user[dbKey] = req.body[bodyKey] }) + if (this.config.enableLogging) console.log('PUT', user) + await this.store.put(username, user) + if (this.config.enableLogging) console.log('STORED') } - if (this.config.enableLogging) console.log('PUT', user) - await this.store.put(username, user) - - if (this.config.enableLogging) console.log('STORED') - const attestation = new AttestationChallengeBuilder(this) .setUserInfo(user) .setAttestationType(this.config.attestation) + .addCredentialExclusion( + user.credentials.map(credential => ({ id: credential.credID }))) // .setAuthenticator() // Forces TPM .setRelyingPartyInfo({ name: this.config.rpName || options.rpName }) .build({ status: 'ok' }) @@ -159,15 +154,16 @@ class Webauthn { }) } - if (!user.authenticator) { + if (user.credentials.length === 0) { return res.status(401).json({ message: 'user has not registered an authenticator', }) } const assertion = new AssertionChallengeBuilder(this) - .addAllowedCredential({ id: user.authenticator.credID }) - .build({ status: 'ok' }) + .addAllowedCredential( + user.credentials.map(credential => ({ id: credential.credID }))) + .build({ status: 'ok' }) req.session.challenge = assertion.challenge req.session[usernameField] = username @@ -255,18 +251,22 @@ class Webauthn { result = this.verifyAuthenticatorAttestationResponse(response); if (result.verified) { - user.authenticator = result.authrInfo + user.credentials.push(result.authrInfo) await this.store.put(username, user) } } else if (response.authenticatorData !== undefined) { - result = Webauthn.verifyAuthenticatorAssertionResponse(response, user.authenticator, this.config.enableLogging) + let authenticator = + user.credentials.find(credential => credential.credID === id) + + result = Webauthn.verifyAuthenticatorAssertionResponse( + response, authenticator, this.config.enableLogging) if (result.verified) { - if (result.counter <= user.authenticator.counter) + if (result.counter <= authenticator.counter) throw new Error('Authr counter did not increase!') - user.authenticator.counter = result.counter + authenticator.counter = result.counter await this.store.put(username, user) }