1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
import base64url from 'base64url';
import { AssertionCredentialJSON, AuthenticatorDevice } from '@simplewebauthn/typescript-types';
import decodeClientDataJSON from '../helpers/decodeClientDataJSON';
import toHash from '../helpers/toHash';
import convertASN1toPEM from '../helpers/convertASN1toPEM';
import verifySignature from '../helpers/verifySignature';
import parseAuthenticatorData from '../helpers/parseAuthenticatorData';
type Options = {
credential: AssertionCredentialJSON;
expectedChallenge: string;
expectedOrigin: string;
expectedRPID: string;
authenticator: AuthenticatorDevice;
requireUserVerification?: boolean;
};
/**
* Verify that the user has legitimately completed the login process
*
* **Options:**
*
* @param credential Authenticator credential returned by browser's `startAssertion()`
* @param expectedChallenge The random value provided to generateAssertionOptions for the
* authenticator to sign
* @param expectedOrigin Website URL that the attestation should have occurred on
* @param expectedRPID RP ID that was specified in the attestation options
* @param authenticator An internal {@link AuthenticatorDevice} matching the credential's ID
* @param requireUserVerification (Optional) Enforce user verification by the authenticator
* (via PIN, fingerprint, etc...)
*/
export default function verifyAssertionResponse(options: Options): VerifiedAssertion {
const {
credential,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification = false,
} = options;
const { response } = credential;
const clientDataJSON = decodeClientDataJSON(response.clientDataJSON);
const { type, origin, challenge } = clientDataJSON;
// Make sure we're handling an assertion
if (type !== 'webauthn.get') {
throw new Error(`Unexpected assertion type: ${type}`);
}
// Ensure the device provided the challenge we gave it
const encodedExpectedChallenge = base64url.encode(expectedChallenge);
if (challenge !== encodedExpectedChallenge) {
throw new Error(
`Unexpected assertion challenge "${challenge}", expected "${encodedExpectedChallenge}"`,
);
}
// Check that the origin is our site
if (origin !== expectedOrigin) {
throw new Error(`Unexpected assertion origin "${origin}", expected "${expectedOrigin}"`);
}
const authDataBuffer = base64url.toBuffer(response.authenticatorData);
const parsedAuthData = parseAuthenticatorData(authDataBuffer);
const { rpIdHash, flags, counter } = parsedAuthData;
// Make sure the response's RP ID is ours
const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii'));
if (!rpIdHash.equals(expectedRPIDHash)) {
throw new Error(`Unexpected RP ID hash`);
}
// Make sure someone was physically present
if (!flags.up) {
throw new Error('User not present during assertion');
}
// Enforce user verification if specified
if (requireUserVerification && !flags.uv) {
throw new Error('User verification required, but user could not be verified');
}
const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON));
const signatureBase = Buffer.concat([authDataBuffer, clientDataHash]);
const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.publicKey));
const signature = base64url.toBuffer(response.signature);
if ((counter > 0 || authenticator.counter > 0) && counter <= authenticator.counter) {
// Error out when the counter in the DB is greater than or equal to the counter in the
// dataStruct. It's related to how the authenticator maintains the number of times its been
// used for this client. If this happens, then someone's somehow increased the counter
// on the device without going through this site
throw new Error(
`Response counter value ${counter} was lower than expected ${authenticator.counter}`,
);
}
const toReturn = {
verified: verifySignature(signature, signatureBase, publicKey),
authenticatorInfo: {
counter,
base64CredentialID: credential.id,
},
};
return toReturn;
}
/**
* Result of assertion verification
*
* @param verified If the assertion response could be verified
* @param authenticatorInfo.base64CredentialID The ID of the authenticator used during assertion.
* Should be used to identify which DB authenticator entry needs its `counter` updated to the value
* below
* @param authenticatorInfo.counter The number of times the authenticator identified above reported
* it has been used. **Should be kept in a DB for later reference to help prevent replay attacks!**
*/
export type VerifiedAssertion = {
verified: boolean;
authenticatorInfo: {
counter: number;
base64CredentialID: string;
};
};
|