summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src/assertion/verifyAssertionResponse.ts
blob: a3b631bcc1a1ef8dcd2e937186f056b751a2d9c1 (plain)
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
import base64url from 'base64url';
import {
  AuthenticatorAssertionResponseJSON,
  AuthenticatorDevice,
  VerifiedAssertion,
} from "@webauthntine/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';

/**
 * Verify that the user has legitimately completed the login process
 *
 * @param response Authenticator attestation response with base64-encoded values
 * @param expectedOrigin Expected URL of website attestation should have occurred on
 */
export default function verifyAssertionResponse(
  response: AuthenticatorAssertionResponseJSON,
  expectedOrigin: string,
  authenticator: AuthenticatorDevice,
): VerifiedAssertion {
  const { base64AuthenticatorData, base64ClientDataJSON, base64Signature } = response;
  const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON);

  const { type, origin } = clientDataJSON;

  // Check that the origin is our site
  if (origin !== expectedOrigin) {
    throw new Error(`Unexpected assertion origin: ${origin}`);
  }

  // Make sure we're handling an assertion
  if (type !== 'webauthn.get') {
    throw new Error(`Unexpected assertion type: ${type}`);
  }

  const authDataBuffer = base64url.toBuffer(base64AuthenticatorData);
  const authDataStruct = parseAuthenticatorData(authDataBuffer);
  const { credentialID, flags, counter } = authDataStruct;

  if (!(flags.up)) {
    throw new Error('User was NOT present during assertion!');
  }

  if (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 {
    rpIdHash,
    flagsBuf,
    counterBuf,
  } = authDataStruct;

  const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON));
  const signatureBase = Buffer.concat([
    rpIdHash,
    flagsBuf,
    counterBuf,
    clientDataHash,
  ]);

  const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.base64PublicKey));
  const signature = base64url.toBuffer(base64Signature);

  const toReturn = {
    verified: verifySignature(signature, signatureBase, publicKey),
    authenticatorInfo: {
      counter,
      base64CredentialID: response.base64CredentialID,
    },
  };

  return toReturn;
}