Skip to content

Commit

Permalink
functions for challenge and origin validation
Browse files Browse the repository at this point in the history
  • Loading branch information
dagnelies committed Nov 23, 2022
1 parent 0ee1903 commit a40c6e6
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 17 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,19 @@ const credentialKey = { // obtained from database by looking up `authentication.
} as const

const expected = {
challenge: "56535b13-5d93-4194-a282-f234c1c24500", // whatever was randomly generated by the server
challenge: "56535b13-5d93-4194-a282-f234c1c24500", // whatever was randomly generated by the server.
origin: "http:https://localhost:8080",
userVerified: true, // should be set if `userVerification` was set to `required` in the authentication options (default)
counter: 0 // for enhanced security, you can store the number of times this authenticator was used and ensure it increases each time
}

const expectedWithFunctions = {
challenge: async (challenge) => { /* async call to DB for example */ return true },
origin: (origin) => { return listOfAllowedOrigins.includes(origin)},
userVerified: true, // no function allowed here
counter: 0 // no function allowed here
}

const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)
```

Expand Down
2 changes: 1 addition & 1 deletion dist/webauthn.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/webauthn.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@passwordless-id/webauthn",
"version": "1.0.0",
"version": "1.1.0",
"description": "A small wrapper around the webauthn protocol to make one's life easier.",
"type": "module",
"main": "dist/esm/index.js",
Expand Down
53 changes: 53 additions & 0 deletions src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,56 @@ test('Test README authentication example', async () => {
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
})
});




test('Test README authentication example with async functions', async () => {
const authentication = {
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAQ==",
"clientData": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiNTY1MzViMTMtNWQ5My00MTk0LWEyODItZjIzNGMxYzI0NTAwIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=",
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
}

const credentialKey = { // obtained from database by looking up `authentication.credentialId`
id: "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
algorithm: "ES256"
} as const

const expected = {
challenge: async (challenge:string) => {return true}, // whatever was randomly generated by the server
origin: async (origin:string) => {return ["http:https://localhost:8080"].includes(origin)},
userVerified: true, // should be set if `userVerification` was set to `required` in the authentication options (default)
counter: 0 // for enhanced security, you can store the number of times this authenticator was used and ensure it increases each time
}

const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)

console.log(authenticationParsed)

expect(authenticationParsed).toMatchObject({
"credentialId": "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
"client": {
"type": "webauthn.get",
"challenge": "56535b13-5d93-4194-a282-f234c1c24500",
"origin": "http:https://localhost:8080",
"crossOrigin": false,
"other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex"
},
"authenticator": {
"rpIdHash": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M=",
"flags": {
"userPresent": true,
"userVerified": true,
"backupEligibility": false,
"backupState": false,
"attestedData": false,
"extensionsIncluded": false
},
"counter": 1
},
"signature": "MEUCIAqtFVRrn7q9HvJCAsOhE3oKJ-Hb4ISfjABu4lH70MKSAiEA666slmop_oCbmNZdc-QemTv2Rq4g_D7UvIhWT_vVp8M="
})
});
39 changes: 28 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,48 @@ import { AuthenticationEncoded, AuthenticationParsed, CredentialKey, NamedAlgo,
import * as utils from './utils'


async function isValid(validator :any, value :any) :Promise<boolean> {
if(typeof validator === 'function') {
const res = validator(value)
if(res instanceof Promise)
return await res
else
return res
}
// the validator can be a single value too
return validator === value
}

async function isNotValid(validator :any, value :any) :Promise<boolean> {
return !(await isValid(validator, value))
}

interface RegistrationChecks {
challenge: string,
origin: string
challenge: string | Function,
origin: string | Function
}


export async function verifyRegistration(registrationRaw: RegistrationEncoded, expected: RegistrationChecks): Promise<RegistrationParsed> {
const registration = parseRegistration(registrationRaw)
registration.client.challenge

if (registration.client.type !== "webauthn.create")
throw new Error(`Unexpected ClientData type: ${registration.client.type}`)

if (registration.client.origin !== expected.origin)
if (await isNotValid(expected.origin, registration.client.origin))
throw new Error(`Unexpected ClientData origin: ${registration.client.origin}`)

if (registration.client.challenge !== expected.challenge)
if (await isNotValid(expected.challenge, registration.client.challenge))
throw new Error(`Unexpected ClientData challenge: ${registration.client.challenge}`)

return registration
}


interface AuthenticationChecks {
challenge: string,
origin: string,
challenge: string | Function,
origin: string | Function,
userVerified: boolean,
counter: number
}
Expand All @@ -53,14 +70,14 @@ export async function verifyAuthentication(authenticationRaw: AuthenticationEnco
if (authentication.client.type !== "webauthn.get")
throw new Error(`Unexpected clientData type: ${authentication.client.type}`)

if (authentication.client.origin !== expected.origin)
throw new Error(`Unexpected clientData origin: ${authentication.client.origin}`)
if (await isNotValid(expected.origin, authentication.client.origin))
throw new Error(`Unexpected ClientData origin: ${authentication.client.origin}`)

if (authentication.client.challenge !== expected.challenge)
throw new Error(`Unexpected clientData challenge: ${authentication.client.challenge}`)
if (await isNotValid(expected.challenge, authentication.client.challenge))
throw new Error(`Unexpected ClientData challenge: ${authentication.client.challenge}`)

// this only works because we consider `rp.origin` and `rp.id` to be the same during authentication/registration
const rpId = new URL(expected.origin).hostname
const rpId = new URL(authentication.client.origin).hostname
const expectedRpIdHash = utils.toBase64url(await utils.sha256(utils.toBuffer(rpId)))
if (authentication.authenticator.rpIdHash !== expectedRpIdHash)
throw new Error(`Unexpected RpIdHash: ${authentication.authenticator.rpIdHash} vs ${expectedRpIdHash}`)
Expand Down

0 comments on commit a40c6e6

Please sign in to comment.