Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(WebAuthn): allow multiple credentials per user #29

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 2 additions & 4 deletions example/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 0 additions & 22 deletions example/src/AuthenticatorCard.js

This file was deleted.

22 changes: 22 additions & 0 deletions example/src/CredentialCard.js
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<Card.Body>
<Card.Title>{this.props.credential.credID}</Card.Title>
<Card.Text>
<p><strong>Format: </strong> {this.props.credential.fmt}</p>
<p><strong>Counter: </strong> {this.props.credential.counter}</p>
<p><strong>Public key: </strong> {this.props.credential.publicKey}</p>
</Card.Text>
</Card.Body>
</Card>
)
}
}

export default CredentialCard;
22 changes: 12 additions & 10 deletions example/src/User.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 => {
Expand All @@ -21,8 +21,8 @@ class User extends React.Component {
return
}
return response.json()
}).then(authenticators => {
this.setState({ authenticators })
}).then(credentials => {
this.setState({ credentials })
})
}

Expand All @@ -36,15 +36,17 @@ class User extends React.Component {
<Row style={{ paddingTop: 80}}>
<Col>
<h2>Welcome {this.props.user.username}</h2>
<h3>Your authenticators:</h3>
<h3>Your credentials:</h3>
</Col>
<Col className="text-right">
<Button variant="primary" onClick={this.logout}>Log Out</Button>
</Col>
</Row>
{this.state.authenticators.map(authenticator => <Row key={authenticator.credID}>
<Col><AuthenticatorCard authenticator={authenticator} /></Col>
</Row>)}
<CardColumns>
{this.state.credentials.map(credential =>
<Col key={credential.credID}><CredentialCard credential={credential} /></Col>
)}
</CardColumns>
</Container>
)
}
Expand Down
8 changes: 6 additions & 2 deletions src/AttestationChallengeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 26 additions & 26 deletions src/Webauthn.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,27 @@ 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC I think level will throw if something doesn't exist in the DB

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like that's being caught by the adapter

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)
.setAuthenticator(this.config.authenticator)
.addCredentialExclusion(
user.credentials.map(credential => ({ id: credential.credID })))
.setRelyingPartyInfo({ name: this.config.rpName || options.rpName })
.build({ status: 'ok' })

Expand Down Expand Up @@ -160,15 +155,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
Expand Down Expand Up @@ -256,18 +252,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)
}

Expand Down