File: webauthn.js

package info (click to toggle)
flask-security 5.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,448 kB
  • sloc: python: 23,247; javascript: 204; makefile: 138
file content (193 lines) | stat: -rw-r--r-- 6,633 bytes parent folder | download | duplicates (2)
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
 * Thanks to duo-labs/py_webauthn - this is their OLD flask demo.
 * Modified accordingly...
 * This code should be considered to be licensed:
 * Copyright (c) 2017 Duo Security, Inc. All rights reserved.
 * with the BSD 3-Clause "New" or "Revised" License.
 * Further changes:
 *  :copyright: (c) 2021-2022 by J. Christopher Wagner (jwag).
 *  :license: MIT, see LICENSE for more details.
 */


function b64enc(buf) {
    return base64js.fromByteArray(buf)
                   .replace(/\+/g, "-")
                   .replace(/\//g, "_")
                   .replace(/=/g, "");
}

function b64RawEnc(buf) {
    return base64js.fromByteArray(buf)
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
}

function encode(attr) {
  return Uint8Array.from(
    atob(attr.replace(/_/g, "/").replace(/-/g, "+")),
    c => c.charCodeAt(0));
}

/**
 * REGISTRATION FUNCTIONS
 */

/*
 * handleRegister - given the return from server of credential_options,
 * parse those and pass to browser to create new credential, and transform
 * that new credential for passing/storing to the server.
 */
async function handleRegister(credential_options) {
  const credentialCreateOptionsFromServer = JSON.parse(credential_options)

  // convert certain members of the PublicKeyCredentialCreateOptions into
  // byte arrays as expected by the spec.
  const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(credentialCreateOptionsFromServer)

  // request the authenticator(s) to create a new credential keypair.
  let credential, error_msg
  try {
      credential = await navigator.credentials.create({
          publicKey: publicKeyCredentialCreateOptions
      })
      // we now have a new credential! We now need to encode the byte arrays
      // in the credential into strings, for posting to our server.
      credential = JSON.stringify(transformNewAssertionForServer(credential))
  } catch (err) {
    error_msg = `Error when creating credential: ${err}`
  }
  return {"credential": credential, "error_msg": error_msg}
}

/**
 * Transforms items in the credentialCreateOptions generated on the server
 * into byte arrays expected by the navigator.credentials.create() call
 * @param {Object} credentialCreateOptionsFromServer
 */
const transformCredentialCreateOptions = (credentialCreateOptionsFromServer) => {
    let {challenge, user, excludeCredentials} = credentialCreateOptionsFromServer
    user.id = encode(credentialCreateOptionsFromServer.user.id)
    challenge = encode(credentialCreateOptionsFromServer.challenge)

    excludeCredentials = excludeCredentials.map(credentialDescriptor => {
      let {id} = credentialDescriptor;
      id = encode(id)
      return Object.assign({}, credentialDescriptor, {id})
    })

    const transformedCredentialCreateOptions = Object.assign(
      {},
      credentialCreateOptionsFromServer,
      {challenge, user, excludeCredentials}
    )

    return transformedCredentialCreateOptions
}

/**
 * Transforms the binary data in the credential into base64 strings
 * for posting to the server.
 * @param {PublicKeyCredential} newAssertion
 */
const transformNewAssertionForServer = (newAssertion) => {
    const attObj = new Uint8Array(
        newAssertion.response.attestationObject);
    const clientDataJSON = new Uint8Array(
        newAssertion.response.clientDataJSON);
    const rawId = new Uint8Array(
        newAssertion.rawId);

    const registrationClientExtensions = newAssertion.getClientExtensionResults();

    // Not all browsers support getTransports() (e.g. Firefox)
    let transports = null
    if ("getTransports" in newAssertion.response) {
      transports = newAssertion.response.getTransports()
    }

    return {
        id: newAssertion.id,
        rawId: b64enc(rawId),
        type: newAssertion.type,
        response: {"attestationObject": b64enc(attObj), "clientDataJSON": b64enc(clientDataJSON), "transports": transports},
        extensions: JSON.stringify(registrationClientExtensions),
    }
}



/**
 * AUTHENTICATION FUNCTIONS
 */


async function handleSignin(response) {
  const credentialRequestOptionsFromServer = JSON.parse(response)
  // convert certain members of the PublicKeyCredentialRequestOptions into
  // byte arrays as expected by the spec.
  const transformedCredentialRequestOptions = transformCredentialRequestOptions(
      credentialRequestOptionsFromServer)

  // request the authenticator to create an assertion signature using the
  // credential private key
  let assertion, credential, error_msg
  try {
    assertion = await navigator.credentials.get({
        publicKey: transformedCredentialRequestOptions,
    })
    // we now have an authentication assertion! encode the byte arrays contained
    // in the assertion data as strings for posting to the server
    credential = JSON.stringify(transformAssertionForServer(assertion))
  } catch (err) {
    error_msg = `Error when retrieving credential: ${err}`
  }
  return {"credential": credential, "error_msg": error_msg}
}

const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => {
    let {challenge, allowCredentials} = credentialRequestOptionsFromServer

    challenge = encode(challenge)
    allowCredentials = allowCredentials.map(credentialDescriptor => {
      let {id} = credentialDescriptor
      id = encode(id)
      return Object.assign({}, credentialDescriptor, {id})
    })

    const transformedCredentialRequestOptions = Object.assign(
      {},
      credentialRequestOptionsFromServer,
      {challenge, allowCredentials}
    )

    return transformedCredentialRequestOptions
}



/**
 * Encodes the binary data in the assertion into strings for posting to the server.
 * @param {PublicKeyCredential} newAssertion
 */
const transformAssertionForServer = (newAssertion) => {
    const authData = new Uint8Array(newAssertion.response.authenticatorData);
    const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON)
    const rawId = new Uint8Array(newAssertion.rawId)
    const sig = new Uint8Array(newAssertion.response.signature)
    const userHandle = new Uint8Array(newAssertion.response.userHandle)
    const assertionClientExtensions = newAssertion.getClientExtensionResults()

    return {
        id: newAssertion.id,
        rawId: b64enc(rawId),
        type: newAssertion.type,
        response: {
          authenticatorData: b64RawEnc(authData),
          clientDataJSON: b64RawEnc(clientDataJSON),
          signature: b64RawEnc(sig),
          userHandle: b64RawEnc(userHandle)
        },
        assertionClientExtensions: JSON.stringify(assertionClientExtensions)
    };
};