├── .eslintrc.json
├── .gitignore
├── .jsdoc-conf.json
├── .travis.yml
├── GruntFile.js
├── LICENSE.md
├── README.md
├── classes
├── CreateOptions.js
├── CreateOptionsRequest.js
├── CredentialAssertion.js
├── CredentialAttestation.js
├── GetOptions.js
├── GetOptionsRequest.js
├── Msg.js
├── ServerResponse.js
└── WebAuthnApp.js
├── index.js
├── lib
├── browser
│ ├── detect.js
│ └── utils.js
├── default-routes.js
├── input-validation.js
├── node
│ ├── detect.js
│ └── utils.js
└── utils.js
├── package.json
├── rollup.config.js
└── test
├── browser
├── css
│ └── mocha.css
├── js
│ ├── chai.js
│ ├── mocha.js
│ └── sinon-1.17.1.js
├── test-setup.js
├── test.html
└── test.js
├── common
├── create-options-request-test.js
├── create-options-test.js
├── credential-assertion-test.js
├── credential-attestation-test.js
├── get-options-request-test.js
├── get-options-test.js
├── helpers-test.js
├── index-test.js
├── msg-test.js
├── server-response-test.js
└── webauthn-options-test.js
└── node
├── test-setup.js
└── test.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true,
6 | "mocha": true
7 | },
8 | "parserOptions": {
9 | "ecmaVersion": 2017,
10 | "sourceType": "module"
11 | },
12 | "extends": "eslint:recommended",
13 | "rules": {
14 | "no-console": "warn",
15 | "no-unused-vars": [
16 | "warn", {
17 | "vars": "local",
18 | "args": "after-used"
19 | }
20 | ],
21 | "accessor-pairs": "error",
22 | "array-bracket-newline": "off",
23 | "array-bracket-spacing": [
24 | "error",
25 | "never"
26 | ],
27 | "array-callback-return": "error",
28 | "array-element-newline": "off",
29 | "arrow-body-style": [
30 | "warn",
31 | "as-needed"
32 | ],
33 | "arrow-parens": [
34 | "error",
35 | "always"
36 | ],
37 | "arrow-spacing": [
38 | "error", {
39 | "after": true,
40 | "before": true
41 | }
42 | ],
43 | "block-scoped-var": "error",
44 | "block-spacing": "error",
45 | "brace-style": [
46 | "error",
47 | "1tbs",
48 | { "allowSingleLine": false }
49 | ],
50 | "callback-return": "error",
51 | "camelcase": "error",
52 | "capitalized-comments": "off",
53 | "class-methods-use-this": "off",
54 | "comma-dangle": ["warn", "only-multiline"],
55 | "comma-spacing": [
56 | "error", {
57 | "after": true,
58 | "before": false
59 | }
60 | ],
61 | "comma-style": [
62 | "error",
63 | "last"
64 | ],
65 | "complexity": "error",
66 | "computed-property-spacing": [
67 | "error",
68 | "never"
69 | ],
70 | "consistent-return": "off",
71 | "consistent-this": ["error", "self"],
72 | "curly": "off",
73 | "default-case": "error",
74 | "dot-location": [
75 | "error",
76 | "property"
77 | ],
78 | "dot-notation": [
79 | "error", {
80 | "allowKeywords": true
81 | }
82 | ],
83 | "eol-last": [
84 | "error",
85 | "always"
86 | ],
87 | "eqeqeq": "off",
88 | "for-direction": "error",
89 | "func-call-spacing": "error",
90 | "func-name-matching": "error",
91 | "func-names": [
92 | "error",
93 | "never"
94 | ],
95 | "func-style": [
96 | "error",
97 | "declaration"
98 | ],
99 | "function-paren-newline": "error",
100 | "generator-star-spacing": "error",
101 | "getter-return": "error",
102 | "global-require": "error",
103 | "guard-for-in": "error",
104 | "handle-callback-err": "error",
105 | "id-blacklist": "error",
106 | "id-length": "off",
107 | "id-match": "error",
108 | "implicit-arrow-linebreak": [
109 | "error",
110 | "beside"
111 | ],
112 | "indent": [ "warn", 4, { "SwitchCase": 1 }],
113 | "indent-legacy": "off",
114 | "init-declarations": "off",
115 | "jsx-quotes": "error",
116 | "key-spacing": "error",
117 | "keyword-spacing": [
118 | "error", {
119 | "after": true,
120 | "before": true
121 | }
122 | ],
123 | "line-comment-position": "off",
124 | "linebreak-style": [
125 | "error",
126 | "unix"
127 | ],
128 | "lines-around-comment": "off",
129 | "lines-around-directive": "error",
130 | "lines-between-class-members": [
131 | "error",
132 | "always"
133 | ],
134 | "max-depth": "error",
135 | "max-len": "off",
136 | "max-lines": "off",
137 | "max-nested-callbacks": "error",
138 | "max-params": "off",
139 | "max-statements": "off",
140 | "max-statements-per-line": "error",
141 | "multiline-comment-style": [
142 | "off"
143 | ],
144 | "new-cap": "error",
145 | "new-parens": "error",
146 | "newline-after-var": "off",
147 | "newline-before-return": "off",
148 | "newline-per-chained-call": "off",
149 | "no-alert": "error",
150 | "no-array-constructor": "error",
151 | "no-await-in-loop": "error",
152 | "no-bitwise": "off",
153 | "no-buffer-constructor": "off",
154 | "no-caller": "error",
155 | "no-catch-shadow": "error",
156 | "no-confusing-arrow": "error",
157 | "no-continue": "off",
158 | "no-div-regex": "off",
159 | "no-duplicate-imports": "error",
160 | "no-else-return": "error",
161 | "no-empty-function": "off",
162 | "no-eq-null": "error",
163 | "no-eval": "error",
164 | "no-extend-native": "error",
165 | "no-extra-bind": "error",
166 | "no-extra-label": "error",
167 | "no-extra-parens": "off",
168 | "no-floating-decimal": "error",
169 | "no-implicit-globals": "error",
170 | "no-implied-eval": "error",
171 | "no-inline-comments": "off",
172 | "no-inner-declarations": [
173 | "error",
174 | "functions"
175 | ],
176 | "no-invalid-this": "error",
177 | "no-iterator": "error",
178 | "no-label-var": "error",
179 | "no-labels": "error",
180 | "no-lone-blocks": "error",
181 | "no-lonely-if": "error",
182 | "no-loop-func": "error",
183 | "no-magic-numbers": "off",
184 | "no-mixed-operators": "error",
185 | "no-mixed-requires": "error",
186 | "no-multi-assign": "error",
187 | "no-multi-spaces": "error",
188 | "no-multi-str": "error",
189 | "no-multiple-empty-lines": "off",
190 | "no-native-reassign": "error",
191 | "no-negated-condition": "error",
192 | "no-negated-in-lhs": "error",
193 | "no-nested-ternary": "error",
194 | "no-new": "off",
195 | "no-new-func": "error",
196 | "no-new-object": "error",
197 | "no-new-require": "error",
198 | "no-new-wrappers": "error",
199 | "no-octal-escape": "error",
200 | "no-param-reassign": "off",
201 | "no-path-concat": "error",
202 | "no-plusplus": "off",
203 | "no-process-env": "error",
204 | "no-process-exit": "error",
205 | "no-proto": "error",
206 | "no-prototype-builtins": "error",
207 | "no-restricted-globals": "error",
208 | "no-restricted-imports": "error",
209 | "no-restricted-modules": "error",
210 | "no-restricted-properties": "error",
211 | "no-restricted-syntax": "error",
212 | "no-return-assign": "error",
213 | "no-return-await": "error",
214 | "no-script-url": "error",
215 | "no-self-compare": "error",
216 | "no-sequences": "error",
217 | "no-shadow": "error",
218 | "no-shadow-restricted-names": "error",
219 | "no-spaced-func": "error",
220 | "no-sync": "off",
221 | "no-tabs": "error",
222 | "no-template-curly-in-string": "error",
223 | "no-ternary": "off",
224 | "no-throw-literal": "error",
225 | "no-trailing-spaces": "error",
226 | "no-undef-init": "warn",
227 | "no-undefined": "off",
228 | "no-underscore-dangle": "off",
229 | "no-unmodified-loop-condition": "error",
230 | "no-unneeded-ternary": "error",
231 | "no-unused-expressions": "error",
232 | "no-use-before-define": "off",
233 | "no-useless-call": "error",
234 | "no-useless-computed-key": "error",
235 | "no-useless-concat": "error",
236 | "no-useless-constructor": "error",
237 | "no-useless-rename": "error",
238 | "no-useless-return": "error",
239 | "no-var": "off",
240 | "no-void": "error",
241 | "no-warning-comments": "off",
242 | "no-whitespace-before-property": "error",
243 | "no-with": "error",
244 | "nonblock-statement-body-position": "error",
245 | "object-curly-newline": "off",
246 | "object-curly-spacing": [
247 | "error",
248 | "always"
249 | ],
250 | "object-property-newline": "error",
251 | "object-shorthand": "off",
252 | "one-var": "off",
253 | "one-var-declaration-per-line": "off",
254 | "operator-assignment": [
255 | "error",
256 | "always"
257 | ],
258 | "operator-linebreak": "error",
259 | "padded-blocks": "off",
260 | "padding-line-between-statements": "error",
261 | "prefer-arrow-callback": "off",
262 | "prefer-const": "off",
263 | "prefer-destructuring": "off",
264 | "prefer-numeric-literals": "error",
265 | "prefer-promise-reject-errors": "off",
266 | "prefer-reflect": "off",
267 | "prefer-rest-params": "error",
268 | "prefer-spread": "error",
269 | "prefer-template": "off",
270 | "quote-props": "off",
271 | "quotes": [
272 | "warn",
273 | "double",
274 | { "allowTemplateLiterals": true }
275 | ],
276 | "radix": "error",
277 | "require-await": "off",
278 | "require-jsdoc": "off",
279 | "rest-spread-spacing": [
280 | "error",
281 | "never"
282 | ],
283 | "semi": "warn",
284 | "semi-spacing": [
285 | "error", {
286 | "after": true,
287 | "before": false
288 | }
289 | ],
290 | "semi-style": [
291 | "error",
292 | "last"
293 | ],
294 | "sort-imports": "error",
295 | "sort-keys": "off",
296 | "sort-vars": "off",
297 | "space-before-blocks": "error",
298 | "space-before-function-paren": "off",
299 | "space-in-parens": [
300 | "error",
301 | "never"
302 | ],
303 | "space-infix-ops": "error",
304 | "space-unary-ops": "error",
305 | "spaced-comment": [
306 | "off"
307 | ],
308 | "strict": [
309 | "error",
310 | "global"
311 | ],
312 | "switch-colon-spacing": "error",
313 | "symbol-description": "off",
314 | "template-curly-spacing": [
315 | "error",
316 | "never"
317 | ],
318 | "template-tag-spacing": "error",
319 | "unicode-bom": [
320 | "error",
321 | "never"
322 | ],
323 | "valid-jsdoc": "off",
324 | "vars-on-top": "off",
325 | "wrap-iife": "error",
326 | "wrap-regex": "error",
327 | "yield-star-spacing": "error",
328 | "yoda": [
329 | "error",
330 | "never"
331 | ]
332 | }
333 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | docs
3 | .DS_Store
4 | npm-debug.log
5 | coverage
6 | package-lock.json
7 | dist
8 |
--------------------------------------------------------------------------------
/.jsdoc-conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags": {
3 | "allowUnknownTags": false
4 | },
5 | "source": {
6 | "include": [ "classes", "lib" ],
7 | "includePattern": "\\.js$",
8 | "exclude": [ "node_modules", "test", "dist", "rollup.config.js", "GruntFile.js" ]
9 | },
10 | "plugins": [
11 | "plugins/markdown"
12 | ],
13 | "opts": {
14 | "systemName": "WebAuthn Simple Application",
15 | "copyright": "Copyright 2018, Adam Powers",
16 | "template": "node_modules/docdash",
17 | "readme": "README.md",
18 | "encoding": "utf8",
19 | "destination": "docs/",
20 | "recurse": true,
21 | "verbose": true
22 | },
23 | "templates": {
24 | "cleverLinks": false,
25 | "monospaceLinks": false
26 | },
27 | "docdash": {
28 | "static": true,
29 | "sort": true
30 | }
31 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | cache:
3 | directories:
4 | - node_modules
5 | notifications:
6 | email: true
7 | language: node_js
8 | node_js:
9 | - '8'
10 | addons:
11 | sauce_connect: true
12 | hosts:
13 | - saucelabs.test
14 | before_script:
15 | - npm prune
16 | - 'curl -Lo travis_after_all.py https://git.io/vLSON'
17 | after_success:
18 | - python travis_after_all.py
19 | - export $(cat .to_export_back)
20 | - npm run docs
21 | - git config --global user.name "Adam Powers"
22 | - git config --global user.email "apowers@ato.ms"
23 | - npm run publish-docs
24 |
--------------------------------------------------------------------------------
/GruntFile.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | module.exports = function(grunt) {
4 | var browsers = [{
5 | browserName: "firefox",
6 | platform: "Windows 10"
7 | }, {
8 | browserName: "chrome",
9 | platform: "Windows 10"
10 | }, {
11 | browserName: "MicrosoftEdge",
12 | platform: "Windows 10"
13 | }, {
14 | browserName: "chrome",
15 | platform: "macOS 10.12"
16 | }, {
17 | browserName: "firefox",
18 | platform: "macOS 10.12"
19 | }, {
20 | browserName: "safari",
21 | platform: "macOS 10.12",
22 | }];
23 |
24 | grunt.initConfig({
25 | pkg: grunt.file.readJSON("package.json"),
26 | connect: {
27 | server: {
28 | options: {
29 | base: "",
30 | port: 9999
31 | }
32 | }
33 | },
34 |
35 | "saucelabs-mocha": {
36 | all: {
37 | options: {
38 | urls: [
39 | "http://localhost:9999/test/browser/test.html"
40 | ],
41 | browsers: browsers,
42 | build: process.env.TRAVIS_JOB_ID,
43 | testname: "mocha tests",
44 | throttled: 3,
45 | sauceConfig: {
46 | "video-upload-on-pass": false
47 | }
48 | }
49 | }
50 | },
51 | watch: {}
52 | });
53 |
54 | grunt.loadNpmTasks("grunt-contrib-connect");
55 | grunt.loadNpmTasks("grunt-saucelabs");
56 |
57 | grunt.registerTask("default", ["connect", "saucelabs-mocha"]);
58 | };
59 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright © `2018` `Adam Powers`
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the “Software”), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://saucelabs.com/u/apowers313)
2 |
3 | node.js: [](https://travis-ci.org/apowers313/webauthn-simple-app)
4 |
5 | This module makes passwordless (or second-factor) [W3C's](https://www.w3.org/TR/webauthn/) [Web Authentication](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) simple. The primary interface is the [WebAuthnApp](https://apowers313.github.io/webauthn-simple-app/WebAuthnApp.html) class, with the `register()` and `login()` methods for registering new devices and / or logging in via WebAuthn. The interface takes care of communicating with your WebAuthn server, validating server responses, calling the browser's WebAuthn API with the right options, and everything else.
6 |
7 | There is much more functionality available for debugging or more granular control, but it probably isn't needed for most applications.
8 |
9 | The module is also exported as a `npm` module, allowing the [Msg](https://apowers313.github.io/webauthn-simple-app/Msg.html) class to be used in `node.js` servers for creating, validating, and converting all the communications with a browser.
10 |
11 | For a live demo of this project, see [webauthn.org](https://webauthn.org).
12 |
13 | Documentation for all classes and advanced options is [available online](https://apowers313.github.io/webauthn-simple-app).
14 |
15 | ## Install
16 |
17 | **npm**
18 | ``` js
19 | npm install webauthn-simple-app
20 | ```
21 |
22 | **CDN**
23 |
24 | **ES6 Module**
25 | ``` html
26 |
27 | ```
28 |
29 | **Universial Module (UMD)**
30 | ``` html
31 |
32 | ```
33 |
34 | **GitHub**
35 | ```
36 | git clone https://github.com/apowers313/webauthn-simple-app
37 | ```
38 |
39 | **Download**
40 | .zip and .tgz downloads are available from the [releases page](https://github.com/apowers313/webauthn-simple-app/releases).
41 |
42 | ## Simple Example
43 |
44 | **Register:**
45 | ``` js
46 | // register a new device / account
47 | var waApp = new WebAuthnApp()
48 | waApp.username = "me";
49 | waApp.register()
50 | .then(() => {
51 | alert("You are now registered!");
52 | })
53 | .catch((err) => {
54 | alert("Registration error: " + err.message);
55 | });
56 | ```
57 |
58 | **Log in:**
59 | ``` js
60 | // log in to a previously registered account
61 | var waApp = new WebAuthnApp()
62 | waApp.username = "me";
63 | waApp.login()
64 | .then(() => {
65 | alert("You are now logged in!");
66 | })
67 | .catch((err) => {
68 | alert("Log in error: " + err.message);
69 | });
70 | ```
71 |
72 | ## Real Example
73 |
74 | Here is a more complete example, using [jQuery](https://jquery.com/) to do things like get inputs from forms and respond to various events that are fired.
75 |
76 | **JavaScript**
77 | ``` js
78 | // override some of the default configuration options
79 | // see the docs for a full list of configuration options
80 | var webAuthnConfig = {
81 | timeout: 30000
82 | };
83 |
84 | // when user clicks submit in the register form, start the registration process
85 | $("#register-form").submit(function(event) {
86 | event.preventDefault();
87 | webAuthnConfig.username = $(event.target).children("input[name=username]")[0].value
88 | new WebAuthnApp(webAuthnConfig).register();
89 | });
90 |
91 | // when user clicks submit in the login form, start the log in process
92 | $("#login-form").submit(function(event) {
93 | event.preventDefault();
94 | webAuthnConfig.username = $(event.target).children("input[name=username]")[0].value
95 | new WebAuthnApp(webAuthnConfig).login();
96 | });
97 |
98 | // do something when registration is successful
99 | $(document).on("webauthn-register-success", () => {
100 | window.location = "https://example.com/sign-in-page";
101 | });
102 |
103 | // do something when registration fails
104 | $(document).on("webauthn-register-error", (err) => {
105 | // probably do something nice like a toast or a modal...
106 | alert("Registration error: " + err.message);
107 | });
108 |
109 | // do something when log in is successful
110 | $(document).on("webauthn-login-success", () => {
111 | window.location = "https://example.com/my-profile-page";
112 | });
113 |
114 | // do something when log in fails
115 | $(document).on("webauthn-login-error", (err) => {
116 | // probably do something nice like a toast or a modal...
117 | alert("Log in error: " + err.message);
118 | });
119 |
120 | // gently remind the user to authenticate when it's time
121 | $(document).on("webauthn-user-presence-start", (err) => {
122 | // probably do something nice like a toast or a modal...
123 | alert("Please perform user verification on your authenticator now!");
124 | });
125 | ```
126 |
127 | **HTML**
128 | ``` html
129 |
130 |
131 |
135 |
136 |
137 |
141 |
142 | ```
143 |
144 | ## Complete Example
145 | For a complete example using jQuery and Bootstrap, refer to the code at the [webauthn-yubiclone](https://github.com/apowers313/webauthn-yubiclone) project, specifically [index.html](https://github.com/apowers313/webauthn-yubiclone/blob/master/index.html) and [ux-events.js](https://github.com/apowers313/webauthn-yubiclone/blob/master/js/ux-events.js).
146 |
147 | ## Theory of Operation
148 |
149 | Here's what's going on inside when you call `register` or `login`:
150 |
151 | **WebAuthnApp.register():**
152 | * getRegisterOptions()
153 | * client --> CreateOptionsRequest --> server
154 | * client <-- CreateOptions <-- server
155 | * create()
156 | * CredentialAttestation = navigator.credentials.create(CreateOptions)
157 | * sendRegisterResult()
158 | * client --> CredentialAttestation --> server
159 | * client <-- ServerResponse <-- server
160 |
161 | **WebAuthnApp.login():**
162 | * getLoginOptions()
163 | * client --> GetOptionsRequest --> server
164 | * client <-- GetOptions <-- server
165 | * get()
166 | * CredentialAssertion = navigator.credentials.get(GetOptions)
167 | * sendLoginResult()
168 | * client --> CredentialAssertion --> server
169 | * client <-- ServerResponse <-- server
170 |
171 | ## Sponsor
172 | Note that while I used to be Technical Director for FIDO Alliance (and I am currently the Technical Advisor for FIDO Alliance), THIS PROJECT IS NOT ENDORSED OR SPONSORED BY FIDO ALLIANCE.
173 |
174 | Work for this project is supported by my consulting company: [WebAuthn Consulting](https://webauthn.consulting/).
175 |
--------------------------------------------------------------------------------
/classes/CreateOptions.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkAttestation,
3 | checkAuthenticatorSelection,
4 | checkCredentialDescriptorList,
5 | checkFormat,
6 | checkOptionalFormat,
7 | checkOptionalType,
8 | checkTrue,
9 | checkType
10 | } from "../lib/input-validation.js";
11 |
12 | import {
13 | coerceToArrayBuffer,
14 | coerceToBase64Url
15 | } from "../lib/utils.js";
16 |
17 | import { ServerResponse } from "./ServerResponse.js";
18 |
19 | /**
20 | * The options to be used for WebAuthn `create()`
21 | * @extends {ServerResponse}
22 | */
23 | export class CreateOptions extends ServerResponse {
24 | constructor() {
25 | super();
26 |
27 | this.propList = this.propList.concat([
28 | "rp",
29 | "user",
30 | "challenge",
31 | "pubKeyCredParams",
32 | "timeout",
33 | "excludeCredentials",
34 | "authenticatorSelection",
35 | "attestation",
36 | "extensions",
37 | "rawChallenge"
38 | ]);
39 | }
40 |
41 | validate() {
42 | super.validate();
43 |
44 | // check types
45 | checkType(this, "rp", Object);
46 | checkFormat(this.rp, "name", "non-empty-string");
47 | checkOptionalFormat(this.rp, "id", "non-empty-string");
48 | checkOptionalFormat(this.rp, "icon", "non-empty-string");
49 |
50 | checkType(this, "user", Object);
51 | checkFormat(this.user, "name", "non-empty-string");
52 | checkFormat(this.user, "id", "base64url");
53 | checkFormat(this.user, "displayName", "non-empty-string");
54 | checkOptionalFormat(this.user, "icon", "non-empty-string");
55 |
56 | checkFormat(this, "challenge", "base64url");
57 | checkType(this, "pubKeyCredParams", Array);
58 | this.pubKeyCredParams.forEach((cred) => {
59 | checkType(cred, "alg", "number");
60 | checkTrue(cred.type === "public-key", "credential type must be 'public-key'");
61 | });
62 | checkOptionalFormat(this, "timeout", "positive-integer");
63 | checkOptionalType(this, "excludeCredentials", Array);
64 | if (this.excludeCredentials) checkCredentialDescriptorList(this.excludeCredentials);
65 |
66 | checkAuthenticatorSelection(this);
67 | checkAttestation(this);
68 |
69 | checkOptionalType(this, "extensions", Object);
70 | checkOptionalFormat(this, "rawChallenge", "base64url");
71 | }
72 |
73 | decodeBinaryProperties() {
74 | if (this.user && this.user.id) {
75 | this.user.id = coerceToArrayBuffer(this.user.id, "user.id");
76 | }
77 |
78 | this.challenge = coerceToArrayBuffer(this.challenge, "challenge");
79 | if (this.rawChallenge) {
80 | this.rawChallenge = coerceToArrayBuffer(this.rawChallenge, "rawChallenge");
81 | }
82 |
83 | if (this.excludeCredentials) {
84 | this.excludeCredentials.forEach((cred, idx) => {
85 | cred.id = coerceToArrayBuffer(cred.id, "excludeCredentials[" + idx + "].id");
86 | });
87 | }
88 | }
89 |
90 | encodeBinaryProperties() {
91 | if (this.user && this.user.id) {
92 | this.user.id = coerceToBase64Url(this.user.id, "user.id");
93 | }
94 |
95 | this.challenge = coerceToBase64Url(this.challenge, "challenge");
96 | if (this.rawChallenge) {
97 | this.rawChallenge = coerceToBase64Url(this.rawChallenge, "rawChallenge");
98 | }
99 |
100 | if (this.excludeCredentials) {
101 | this.excludeCredentials.forEach((cred, idx) => {
102 | cred.id = coerceToBase64Url(cred.id, "excludeCredentials[" + idx + "].id");
103 | });
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/classes/CreateOptionsRequest.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkAttestation,
3 | checkAuthenticatorSelection,
4 | checkFormat,
5 | checkOptionalFormat
6 | } from "../lib/input-validation.js";
7 |
8 | import { Msg } from "./Msg.js";
9 |
10 | /**
11 | * A {@link Msg} object that the browser sends to the server to request
12 | * the options to be used for the WebAuthn `create()` call.
13 | * @extends {Msg}
14 | */
15 | export class CreateOptionsRequest extends Msg {
16 | constructor() {
17 | super();
18 |
19 | this.propList = [
20 | "username",
21 | "displayName",
22 | "authenticatorSelection",
23 | "attestation",
24 | "extraData"
25 | ];
26 | }
27 |
28 | validate() {
29 | checkFormat(this, "username", "non-empty-string");
30 | checkFormat(this, "displayName", "non-empty-string");
31 | checkAuthenticatorSelection(this);
32 | checkAttestation(this);
33 | checkOptionalFormat(this, "extraData", "base64url");
34 | }
35 |
36 | decodeBinaryProperties() {}
37 |
38 | encodeBinaryProperties() {}
39 | }
40 |
--------------------------------------------------------------------------------
/classes/CredentialAssertion.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkFormat,
3 | checkOptionalFormat,
4 | checkOptionalType,
5 | checkType
6 | } from "../lib/input-validation.js";
7 |
8 | import {
9 | coerceToArrayBuffer,
10 | coerceToBase64Url
11 | } from "../lib/utils.js";
12 |
13 | import { Msg } from "./Msg.js";
14 |
15 | /**
16 | * This is the `PublicKeyCredential` that was the result of the `get()` call.
17 | * @extends {Msg}
18 | */
19 | export class CredentialAssertion extends Msg {
20 | constructor() {
21 | super();
22 |
23 | this.propList = [
24 | "rawId",
25 | "id",
26 | "response",
27 | "getClientExtensionResults"
28 | ];
29 | }
30 |
31 | static from(obj) {
32 | obj = super.from(obj);
33 |
34 | // original response object is probably read-only
35 | if (typeof obj.response === "object") {
36 | var origResponse = obj.response;
37 |
38 | obj.response = {
39 | clientDataJSON: origResponse.clientDataJSON,
40 | authenticatorData: origResponse.authenticatorData,
41 | signature: origResponse.signature,
42 | userHandle: origResponse.userHandle,
43 | };
44 | }
45 |
46 | return obj;
47 | }
48 |
49 | validate() {
50 | checkFormat(this, "rawId", "base64url");
51 | checkOptionalFormat(this, "id", "base64url");
52 | checkType(this, "response", Object);
53 | checkFormat(this.response, "authenticatorData", "base64url");
54 | checkFormat(this.response, "clientDataJSON", "base64url");
55 | checkFormat(this.response, "signature", "base64url");
56 | checkOptionalFormat(this.response, "userHandle", "nullable-base64");
57 | checkOptionalType(this, "getClientExtensionResults", Object);
58 | }
59 |
60 | decodeBinaryProperties() {
61 | this.rawId = coerceToArrayBuffer(this.rawId, "rawId");
62 | if (this.id) this.id = coerceToArrayBuffer(this.id, "id");
63 | this.response.clientDataJSON = coerceToArrayBuffer(this.response.clientDataJSON, "response.clientDataJSON");
64 | this.response.signature = coerceToArrayBuffer(this.response.signature, "response.signature");
65 | this.response.authenticatorData = coerceToArrayBuffer(this.response.authenticatorData, "response.authenticatorData");
66 | if (this.response.userHandle) {
67 | this.response.userHandle = coerceToArrayBuffer(this.response.userHandle, "response.authenticatorData");
68 | }
69 | if (this.response.userHandle === null || this.response.userHandle === "") {
70 | this.response.userHandle = new ArrayBuffer();
71 | }
72 | }
73 |
74 | encodeBinaryProperties() {
75 | this.rawId = coerceToBase64Url(this.rawId, "rawId");
76 | if (this.id) this.id = coerceToBase64Url(this.id, "id");
77 | this.response.clientDataJSON = coerceToBase64Url(this.response.clientDataJSON, "response.clientDataJSON");
78 | this.response.signature = coerceToBase64Url(this.response.signature, "response.signature");
79 | this.response.authenticatorData = coerceToBase64Url(this.response.authenticatorData, "response.authenticatorData");
80 | if (this.response.userHandle) {
81 | if (this.response.userHandle.byteLength > 0) this.response.userHandle = coerceToBase64Url(this.response.userHandle, "response.authenticatorData");
82 | else this.response.userHandle = null;
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/classes/CredentialAttestation.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkFormat,
3 | checkOptionalFormat,
4 | checkOptionalType,
5 | checkType
6 | } from "../lib/input-validation.js";
7 |
8 | import {
9 | coerceToArrayBuffer,
10 | coerceToBase64Url
11 | } from "../lib/utils.js";
12 |
13 | import { Msg } from "./Msg.js";
14 |
15 | /**
16 | * This is the `PublicKeyCredential` that was the result of the `create()` call.
17 | * @extends {Msg}
18 | */
19 | export class CredentialAttestation extends Msg {
20 | constructor() {
21 | super();
22 |
23 | this.propList = [
24 | "rawId",
25 | "id",
26 | "response",
27 | "getClientExtensionResults"
28 | ];
29 | }
30 |
31 | static from(obj) {
32 | obj = super.from(obj);
33 |
34 | // original response object is probably read-only
35 | if (typeof obj.response === "object") {
36 | var origResponse = obj.response;
37 |
38 | obj.response = {
39 | clientDataJSON: origResponse.clientDataJSON,
40 | attestationObject: origResponse.attestationObject,
41 | };
42 | }
43 |
44 | return obj;
45 | }
46 |
47 | validate() {
48 | checkFormat(this, "rawId", "base64url");
49 | checkOptionalFormat(this, "id", "base64url");
50 | checkType(this, "response", Object);
51 | checkFormat(this.response, "attestationObject", "base64url");
52 | checkFormat(this.response, "clientDataJSON", "base64url");
53 | checkOptionalType(this, "getClientExtensionResults", Object);
54 | }
55 |
56 | decodeBinaryProperties() {
57 | this.rawId = coerceToArrayBuffer(this.rawId, "rawId");
58 | if (this.id) this.id = coerceToArrayBuffer(this.id, "id");
59 | this.response.attestationObject = coerceToArrayBuffer(this.response.attestationObject, "response.attestationObject");
60 | this.response.clientDataJSON = coerceToArrayBuffer(this.response.clientDataJSON, "response.clientDataJSON");
61 | }
62 |
63 | encodeBinaryProperties() {
64 | this.rawId = coerceToBase64Url(this.rawId, "rawId");
65 | if (this.id) this.id = coerceToBase64Url(this.id, "id");
66 | this.response.attestationObject = coerceToBase64Url(this.response.attestationObject, "response.attestationObject");
67 | this.response.clientDataJSON = coerceToBase64Url(this.response.clientDataJSON, "response.clientDataJSON");
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/classes/GetOptions.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkCredentialDescriptorList,
3 | checkFormat,
4 | checkOptionalFormat,
5 | checkOptionalType,
6 | checkUserVerification
7 | } from "../lib/input-validation.js";
8 |
9 | import {
10 | coerceToArrayBuffer,
11 | coerceToBase64Url
12 | } from "../lib/utils.js";
13 |
14 | import { ServerResponse } from "./ServerResponse.js";
15 |
16 | /**
17 | * The options to be used for WebAuthn `get()`
18 | * @extends {ServerResponse}
19 | */
20 | export class GetOptions extends ServerResponse {
21 | constructor() {
22 | super();
23 |
24 | this.propList = this.propList.concat([
25 | "challenge",
26 | "timeout",
27 | "rpId",
28 | "allowCredentials",
29 | "userVerification",
30 | "extensions",
31 | "rawChallenge"
32 | ]);
33 | }
34 |
35 | validate() {
36 | super.validate();
37 | checkFormat(this, "challenge", "base64url");
38 | checkOptionalFormat(this, "timeout", "positive-integer");
39 | checkOptionalFormat(this, "rpId", "non-empty-string");
40 | checkOptionalType(this, "allowCredentials", Array);
41 | if (this.allowCredentials) checkCredentialDescriptorList(this.allowCredentials);
42 | if (this.userVerification) checkUserVerification(this.userVerification);
43 | checkOptionalType(this, "extensions", Object);
44 | checkOptionalFormat(this, "rawChallenge", "base64url");
45 | }
46 |
47 | decodeBinaryProperties() {
48 | this.challenge = coerceToArrayBuffer(this.challenge, "challenge");
49 | if (this.rawChallenge) {
50 | this.rawChallenge = coerceToArrayBuffer(this.rawChallenge, "rawChallenge");
51 | }
52 |
53 | if (this.allowCredentials) {
54 | this.allowCredentials.forEach((cred) => {
55 | cred.id = coerceToArrayBuffer(cred.id, "cred.id");
56 | });
57 | }
58 | }
59 |
60 | encodeBinaryProperties() {
61 | this.challenge = coerceToBase64Url(this.challenge, "challenge");
62 | if (this.rawChallenge) {
63 | this.rawChallenge = coerceToBase64Url(this.rawChallenge, "rawChallenge");
64 | }
65 |
66 | if (this.allowCredentials) {
67 | this.allowCredentials.forEach((cred, idx) => {
68 | cred.id = coerceToBase64Url(cred.id, "allowCredentials[" + idx + "].id");
69 | });
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/classes/GetOptionsRequest.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkFormat,
3 | checkOptionalFormat
4 | } from "../lib/input-validation.js";
5 | import { Msg } from "./Msg.js";
6 |
7 | /**
8 | * A {@link Msg} object that the browser sends to the server to request
9 | * the options to be used for the WebAuthn `get()` call.
10 | * @extends {Msg}
11 | */
12 | export class GetOptionsRequest extends Msg {
13 | constructor() {
14 | super();
15 |
16 | this.propList = [
17 | "username",
18 | "displayName",
19 | "extraData"
20 | ];
21 | }
22 |
23 | validate() {
24 | checkFormat(this, "username", "non-empty-string");
25 | checkFormat(this, "displayName", "non-empty-string");
26 | checkOptionalFormat(this, "extraData", "base64url");
27 | }
28 |
29 | decodeBinaryProperties() {}
30 |
31 | encodeBinaryProperties() {}
32 | }
33 |
--------------------------------------------------------------------------------
/classes/Msg.js:
--------------------------------------------------------------------------------
1 | import {
2 | copyPropList,
3 | stringifyObj,
4 | } from "../lib/utils.js";
5 |
6 | /**
7 | * Virtual class for messages that serves as the base
8 | * for all other messages.
9 | */
10 | export class Msg {
11 | constructor() {
12 | /** @type {Array} The list of "official" properties that are managed for this object and sent over the wire. */
13 | this.propList = [];
14 | }
15 |
16 | /**
17 | * Converts the `Msg` to an `Object` containing all the properties in `propList` that have been defined on the `Msg`
18 | * @return {Object} An `Object` that contains all the properties to be sent over the wire.
19 | */
20 | toObject() {
21 | var obj = {};
22 | copyPropList(this, obj, this.propList);
23 | return obj;
24 | }
25 |
26 | /**
27 | * Converts the `Msg` to a JSON string containing all the properties in `propList` that have been defined on the `Msg`
28 | * @return {String} A JSON `String` that contains all the properties to be sent over the wire.
29 | */
30 | toString() {
31 | return JSON.stringify(this.toObject());
32 | }
33 |
34 | /**
35 | * Converts the `Msg` to a human-readable string. Useful for debugging messages as they are being sent / received.
36 | * @return {String} The human-readable message, probably multiple lines.
37 | */
38 | toHumanString() {
39 | var constructMe = Object.getPrototypeOf(this).constructor;
40 | var retObj = constructMe.from(this);
41 | retObj.decodeBinaryProperties();
42 | retObj = retObj.toObject();
43 | var ret = `[${constructMe.name}] ` + stringifyObj(retObj, 0);
44 | return ret;
45 | }
46 |
47 | /**
48 | * Converts the provided `obj` to this class and then returns a human
49 | * readable form of the object as interpreted by that class.
50 | * @param {Object} obj Any object
51 | * @return {String} A human-readable string as interpreteed by this class.
52 | */
53 | static toHumanString(obj) {
54 | var retObj = this.from(obj);
55 | retObj.decodeBinaryProperties();
56 | retObj = retObj.toObject();
57 | var ret = `[${this.name}] ` + stringifyObj(retObj, 0);
58 | return ret;
59 | }
60 |
61 | /**
62 | * Converts the `Msg` to a human-readable string (via {@link toHumanString}) and then replaces whitespace (" " and "\n") with
63 | * HTML compatible interpetations of whitespace (" " and "
").
64 | * @return {String} The HTML compatible representation of this Msg that should be easy for people to read
65 | */
66 | toHumanHtml() {
67 | return this.toHumanString().replace(/ /g, " ").replace(/\n/g, "
");
68 | }
69 |
70 | /**
71 | * Ensures that all the required properties in the object are defined, and all defined properties are of the correct format.
72 | * @throws {Error} If any required field is undefined, or any defined field is of the wrong format.
73 | */
74 | validate() {
75 | throw new Error("not implemented");
76 | }
77 |
78 | /**
79 | * Any fields that are known to be encoded as `base64url` are decoded to an `ArrayBuffer`
80 | */
81 | decodeBinaryProperties() {
82 | // throw new Error("not implemented");
83 | }
84 |
85 | /**
86 | * Any fields that are known to be encoded as an `ArrayBuffer` are encoded as `base64url`
87 | */
88 | encodeBinaryProperties() {
89 | // throw new Error("not implemented");
90 | }
91 |
92 | /**
93 | * Creates a new `Msg` object from the specified parameter. Note that the resulting `Msg` is not validated
94 | * and all fields are their original values (call {@link decodeBinaryProperties} to convert fields to ArrayBuffers)
95 | * if needed.
96 | * @param {String|Object} json The JSON encoded string, or already parsed JSON message in an `Object`
97 | * @return {Msg} The newly created message from the Object.
98 | */
99 | static from(json) {
100 | var obj;
101 | if (typeof json === "string") {
102 | try {
103 | obj = JSON.parse(json);
104 | } catch (err) {
105 | throw new TypeError("error parsing JSON string");
106 | }
107 | }
108 |
109 | if (typeof json === "object") {
110 | obj = json;
111 | }
112 |
113 | if (typeof obj !== "object") {
114 | throw new TypeError("could not coerce 'json' argument to an object: '" + json + "'");
115 | }
116 |
117 | var msg = new this.prototype.constructor();
118 | copyPropList(obj, msg, msg.propList);
119 |
120 | // if (obj.preferences) {
121 | // msg.preferences = WebAuthnOptions.from(obj.preferences);
122 | // }
123 |
124 | return msg;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/classes/ServerResponse.js:
--------------------------------------------------------------------------------
1 | import {
2 | ab2str,
3 | coerceToArrayBuffer,
4 | coerceToBase64Url,
5 | mapToObj,
6 | str2ab
7 | } from "../lib/utils.js";
8 |
9 | import {
10 | checkOptionalType,
11 | checkTrue,
12 | checkType
13 | } from "../lib/input-validation.js";
14 |
15 | import { Msg } from "./Msg.js";
16 |
17 | /**
18 | * Generic {@link Msg} from server to indicate success or failure. Used by
19 | * itself for simple responses, or extended for more complex responses.
20 | * @extends {Msg}
21 | */
22 | export class ServerResponse extends Msg {
23 | constructor() {
24 | super();
25 |
26 | this.propList = [
27 | "status",
28 | "errorMessage",
29 | "debugInfo"
30 | ];
31 | }
32 |
33 | validate() {
34 | switch (this.status) {
35 | case "ok":
36 | if (this.errorMessage === undefined) {
37 | this.errorMessage = "";
38 | }
39 |
40 | // if status is "ok", errorMessage must be ""
41 | checkTrue(this.errorMessage === "", "errorMessage must be empty string when status is 'ok'");
42 | checkOptionalType(this, "debugInfo", "object");
43 | break;
44 |
45 | case "failed":
46 | // if status is "failed", errorMessage must be non-zero-length string
47 | checkType(this, "errorMessage", "string");
48 | checkTrue(
49 | this.errorMessage.length > 0,
50 | "errorMessage must be non-zero length when status is 'failed'"
51 | );
52 | checkOptionalType(this, "debugInfo", "object");
53 | break;
54 |
55 | // status is string, either "ok" or "failed"
56 | default:
57 | throw new Error("'expected 'status' to be 'string', got: " + this.status);
58 | }
59 | }
60 |
61 | decodeBinaryProperties() {
62 | function decodeAb(obj, key) {
63 | obj[key] = coerceToArrayBuffer(obj[key], key);
64 | }
65 |
66 | function decodeOptionalAb(obj, key) {
67 | if (obj[key] !== undefined) decodeAb(obj, key);
68 | }
69 |
70 | function objToMap(o) {
71 | var m = new Map();
72 | Object.keys(o).forEach((k) => {
73 | m.set(k, o[k]);
74 | });
75 | return m;
76 | }
77 |
78 | if (typeof this.debugInfo === "object") {
79 | decodeAb(this.debugInfo.clientData, "rawId");
80 | decodeAb(this.debugInfo.authnrData, "rawAuthnrData");
81 | decodeAb(this.debugInfo.authnrData, "rpIdHash");
82 | decodeOptionalAb(this.debugInfo.authnrData, "aaguid");
83 | decodeOptionalAb(this.debugInfo.authnrData, "credId");
84 | decodeOptionalAb(this.debugInfo.authnrData, "credentialPublicKeyCose");
85 | decodeOptionalAb(this.debugInfo.authnrData, "sig");
86 | decodeOptionalAb(this.debugInfo.authnrData, "attCert");
87 |
88 | this.debugInfo.clientData.rawClientDataJson = str2ab(this.debugInfo.clientData.rawClientDataJson);
89 | this.debugInfo.authnrData.flags = new Set([...this.debugInfo.authnrData.flags]);
90 | this.debugInfo.audit.warning = objToMap(this.debugInfo.audit.warning);
91 | this.debugInfo.audit.info = objToMap(this.debugInfo.audit.info);
92 | }
93 | }
94 |
95 | encodeBinaryProperties() {
96 | function encodeAb(obj, key) {
97 | obj[key] = coerceToBase64Url(obj[key], key);
98 | }
99 |
100 | function encodeOptionalAb(obj, key) {
101 | if (obj[key] !== undefined) encodeAb(obj, key);
102 | }
103 |
104 | if (typeof this.debugInfo === "object") {
105 | encodeAb(this.debugInfo.clientData, "rawId");
106 | encodeAb(this.debugInfo.authnrData, "rawAuthnrData");
107 | encodeAb(this.debugInfo.authnrData, "rpIdHash");
108 | encodeOptionalAb(this.debugInfo.authnrData, "aaguid");
109 | encodeOptionalAb(this.debugInfo.authnrData, "credId");
110 | encodeOptionalAb(this.debugInfo.authnrData, "credentialPublicKeyCose");
111 | encodeOptionalAb(this.debugInfo.authnrData, "sig");
112 | encodeOptionalAb(this.debugInfo.authnrData, "attCert");
113 |
114 | this.debugInfo.clientData.rawClientDataJson = ab2str(this.debugInfo.clientData.rawClientDataJson, "clientData.rawClientDataJson");
115 | this.debugInfo.authnrData.flags = [...this.debugInfo.authnrData.flags];
116 | this.debugInfo.audit.warning = mapToObj(this.debugInfo.audit.warning);
117 | this.debugInfo.audit.info = mapToObj(this.debugInfo.audit.info);
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/classes/WebAuthnApp.js:
--------------------------------------------------------------------------------
1 | import * as defaultRoutes from "../lib/default-routes.js";
2 | import * as utils from "../lib/utils.js";
3 | import { CreateOptions } from "./CreateOptions.js";
4 | import { CreateOptionsRequest } from "./CreateOptionsRequest.js";
5 | import { CredentialAssertion } from "./CredentialAssertion.js";
6 | import { CredentialAttestation } from "./CredentialAttestation.js";
7 | import { GetOptions } from "./GetOptions.js";
8 | import { GetOptionsRequest } from "./GetOptionsRequest.js";
9 | import { Msg } from "./Msg.js";
10 | import { ServerResponse } from "./ServerResponse.js";
11 |
12 | /**
13 | * The main class for registering and logging in via WebAuthn. This class wraps all server communication,
14 | * as well as calls to `credentials.navigator.create()` (registration) and `credentials.navigator.get()` (login)
15 | *
16 | * @param {Object} config The configuration object for WebAuthnApp
17 | */
18 | export class WebAuthnApp {
19 | constructor(config) {
20 | // check for browser; throw error and fail if not browser
21 | if (!utils.isBrowser()) throw new Error("WebAuthnApp must be run from a browser");
22 |
23 | // check for secure context
24 | if (!window.isSecureContext) {
25 | fireNotSupported("This web page was not loaded in a secure context (https). Please try loading the page again using https or make sure you are using a browser with secure context support.");
26 | return null;
27 | }
28 |
29 | // check for WebAuthn CR features
30 | if (window.PublicKeyCredential === undefined ||
31 | typeof window.PublicKeyCredential !== "function" ||
32 | typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable !== "function") {
33 | console.log("PublicKeyCredential not found");
34 | fireNotSupported("WebAuthn is not currently supported by this browser. See this webpage for a list of supported browsers: Web Authentication: Browser Compatibility");
35 | return null;
36 | }
37 |
38 | // Useful constants for working with COSE key objects
39 | const coseAlgECDSAWithSHA256 = -7;
40 |
41 | // configure or defaults
42 | config = config || {};
43 | this.registerChallengeEndpoint = config.registerChallengeEndpoint || defaultRoutes.attestationOptions;
44 | this.registerResponseEndpoint = config.registerResponseEndpoint || defaultRoutes.attestationResult;
45 | this.loginChallengeEndpoint = config.loginChallengeEndpoint || defaultRoutes.assertionOptions;
46 | this.loginResponseEndpoint = config.loginResponseEndpoint || defaultRoutes.assertionResult;
47 | this.registerChallengeMethod = config.registerChallengeMethod || "POST";
48 | this.registerResponseMethod = config.registerResponseMethod || "POST";
49 | this.loginChallengeMethod = config.loginChallengeMethod || "POST";
50 | this.loginResponseMethod = config.loginResponseMethod || "POST";
51 | this.timeout = config.timeout || 60000; // one minute
52 | this.alg = config.alg || coseAlgECDSAWithSHA256;
53 | this.binaryEncoding = config.binaryEncoding;
54 | // TODO: relying party name
55 | this.appName = config.appName || window.location.hostname;
56 | this.username = config.username;
57 | }
58 |
59 | /**
60 | * Perform WebAuthn registration, including getting options from the server
61 | * calling `navigator.credentials.create()`, sending the result to the server,
62 | * and validating the end result. Note that this is a convenience wrapper around
63 | * {@link requestRegisterOptions}, {@link create}, and {@link sendRegisterResult}.
64 | * Each of those classes fires events for various state changes or errors that
65 | * can be captured for more advanced applications.
66 | *
67 | * @return {Promise.} Returns a promise that resolves to
68 | * a {@link ServerResponse} on success, or rejects with an `Error` on failure.
69 | */
70 | register() {
71 | fireRegister("start");
72 | // get challenge
73 | return this.requestRegisterOptions()
74 | .then((serverMsg) => this.create(serverMsg))
75 | .then((newCred) => this.sendRegisterResult(newCred))
76 | .then((msg) => {
77 | fireRegister("success");
78 | return msg;
79 | })
80 | .catch((err) => {
81 | fireRegister("error", err);
82 | return Promise.reject(err);
83 | });
84 | }
85 |
86 | /**
87 | * Perform WebAuthn authentication, including getting options from the server
88 | * calling `navigator.credentials.get()`, sending the result to the server,
89 | * and validating the end result. Note that this is a convenience wrapper around
90 | * {@link requestLoginOptions}, {@link get}, and {@link sendLoginResult}.
91 | * Each of those classes fires events for various state changes or errors that
92 | * can be captured for more advanced applications.
93 | *
94 | * @return {Promise.} Returns a promise that resolves to
95 | * a {@link ServerResponse} on success, or rejects with an `Error` on failure.
96 | */
97 | login() {
98 | fireLogin("start");
99 | var self = this;
100 | // get challenge
101 | return this.requestLoginOptions()
102 | .then((serverMsg) => self.get(serverMsg))
103 | .then((assn) => self.sendLoginResult(assn))
104 | .then((msg) => {
105 | fireLogin("success");
106 | return msg;
107 | })
108 | .catch((err) => {
109 | fireLogin("error", err);
110 | return Promise.reject(err);
111 | });
112 | }
113 |
114 | /**
115 | * A wrapper around a call to `navigator.credentials.create()`,
116 | * which is WebAuthn's way of registering a new device with a service.
117 | *
118 | * @param {CreateOptions} options The desired options for the `navigator.credentials.create()`
119 | * call. May be the return value from {@link requestRegisterOptions} or a modified version thereof.
120 | * Note that this object contains a `challenge` property which MUST come from the server and that
121 | * the server will use to make sure that the credential isn't part of a replay attack.
122 | * @return {Promise.} Returns a Promise that resolves to a
123 | * {@link PublicKeyCredentialAttestation} on success (i.e. - the actual return value from `navigator.credentials.create()`),
124 | * or rejects with an Error on failure.
125 | * @fires WebAuthnApp#userPresenceEvent
126 | */
127 | create(options) {
128 | if (!(options instanceof CreateOptions)) {
129 | throw new Error("expected 'options' to be instance of CreateOptions");
130 | }
131 | options.decodeBinaryProperties();
132 |
133 | var args = {
134 | publicKey: options.toObject()
135 | };
136 | args.publicKey.attestation = args.publicKey.attestation || "direct";
137 | delete args.publicKey.status;
138 | delete args.publicKey.errorMessage;
139 |
140 | fireDebug("create-options", args);
141 | fireUserPresence("start");
142 |
143 | return navigator.credentials.create(args)
144 | .then((res) => {
145 | // save client extensions
146 | if (typeof res.getClientExtensionResults === "function") {
147 | let exts = res.getClientExtensionResults();
148 | if (typeof exts === "object") res.getClientExtensionResults = exts;
149 | }
150 |
151 | fireUserPresence("done");
152 | fireDebug("create-result", res);
153 | return res;
154 | })
155 | .catch((err) => {
156 | fireUserPresence("done");
157 | fireDebug("create-error", err);
158 | return Promise.reject(err);
159 | });
160 | }
161 |
162 | /**
163 | * A wrapper around a call to `navigator.credentials.get()`,
164 | * which is WebAuthn's way of authenticating a user to a service.
165 | *
166 | * @param {GetOptions} options The desired options for the `navigator.credentials.get()`
167 | * call. May be the return value from {@link requestLoginOptions} or a modified version thereof.
168 | * Note that this object contains a `challenge` property which MUST come from the server and that
169 | * the server will use to make sure that the credential isn't part of a replay attack.
170 | * @return {Promise.} Returns a Promise that resolves to a
171 | * {@link PublicKeyCredentialAssertion} on success (i.e. - the actual return value from `navigator.credentials.get()`),
172 | * or rejects with an Error on failure.
173 | * @fires WebAuthnApp#userPresenceEvent
174 | */
175 | get(options) {
176 | if (!(options instanceof GetOptions)) {
177 | throw new Error("expected 'options' to be instance of GetOptions");
178 | }
179 | options.decodeBinaryProperties();
180 |
181 | var args = {
182 | publicKey: options.toObject()
183 | };
184 | delete args.publicKey.status;
185 | delete args.publicKey.errorMessage;
186 |
187 | fireDebug("get-options", args);
188 | fireUserPresence("start");
189 |
190 | return navigator.credentials.get(args)
191 | .then((res) => {
192 | // save client extensions
193 | if (typeof res.getClientExtensionResults === "function") {
194 | let exts = res.getClientExtensionResults();
195 | if (typeof exts === "object") res.getClientExtensionResults = exts;
196 | }
197 |
198 | fireUserPresence("done");
199 | fireDebug("get-result", res);
200 | return res;
201 | })
202 | .catch((err) => {
203 | fireUserPresence("done");
204 | fireDebug("get-error", err);
205 | return Promise.reject(err);
206 | });
207 | }
208 |
209 | /**
210 | * Requests the registration options to be used from the server, including the random
211 | * challenge to be used for this registration request.
212 | *
213 | * @return {CreateOptions} The options to be used for creating the new
214 | * credential to be registered with the server. The options returned will
215 | * have been validated.
216 | */
217 | requestRegisterOptions() {
218 | var sendData = CreateOptionsRequest.from({
219 | username: this.username,
220 | displayName: this.displayName || this.username
221 | });
222 |
223 | return this.send(
224 | this.registerChallengeMethod,
225 | this.registerChallengeEndpoint,
226 | sendData,
227 | CreateOptions
228 | );
229 | }
230 |
231 | /**
232 | * Sends the {@link WebAuthn#AuthenticatorAttestationResponse}
233 | * to the server.
234 | *
235 | * @param {WebAuthn#AuthenticatorAttestationResponse} pkCred The public key credential (containing an attesation) returned from `navigator.credentials.get()`
236 | * @return {Promise.} Resolves to the {@link ServerResponse} from the server on success, or rejects with Error on failure
237 | */
238 | sendRegisterResult(pkCred) {
239 | if (!(pkCred instanceof window.PublicKeyCredential)) {
240 | throw new Error("expected 'pkCred' to be instance of PublicKeyCredential");
241 | }
242 |
243 | var sendData = CredentialAttestation.from({
244 | username: this.username,
245 | rawId: pkCred.rawId,
246 | id: pkCred.rawId,
247 | response: {
248 | attestationObject: pkCred.response.attestationObject,
249 | clientDataJSON: pkCred.response.clientDataJSON
250 | }
251 | });
252 |
253 | return this.send(
254 | this.registerResponseMethod,
255 | this.registerResponseEndpoint,
256 | sendData,
257 | ServerResponse
258 | );
259 | }
260 |
261 | /**
262 | * Requests the login options to be used from the server, including the random
263 | * challenge to be used for this registration request.
264 | *
265 | * @return {GetOptions} The options to be used for creating the new
266 | * credential to be registered with the server. The options returned will
267 | * have been validated.
268 | */
269 | requestLoginOptions() {
270 | var sendData = GetOptionsRequest.from({
271 | username: this.username,
272 | displayName: this.displayname || this.username
273 | });
274 |
275 | return this.send(
276 | this.loginChallengeMethod,
277 | this.loginChallengeEndpoint,
278 | sendData,
279 | GetOptions
280 | );
281 | }
282 |
283 | /**
284 | * This class refers to the dictionaries and interfaces defined in the
285 | * {@link https://www.w3.org/TR/webauthn/ WebAuthn specification} that are
286 | * used by the {@link WebAuthnApp} class. They are included here for reference.
287 | *
288 | * @class WebAuthn
289 | */
290 |
291 | /**
292 | * A {@link https://www.w3.org/TR/webauthn/#iface-pkcredential PublicKeyCredential}
293 | * that has been created by an authenticator, where the `response` field contains a
294 | * {@link https://www.w3.org/TR/webauthn/#authenticatorattestationresponse AuthenticatorAttesationResponse}.
295 | *
296 | * @typedef {Object} WebAuthn#AuthenticatorAttesationResponse
297 | */
298 |
299 | /**
300 | * A {@link https://www.w3.org/TR/webauthn/#iface-pkcredential PublicKeyCredential}
301 | * that has been created by an authenticator, where the `response` field contains a
302 | * {@link https://www.w3.org/TR/webauthn/#authenticatorassertionresponse AuthenticatorAssertionResponse}.
303 | *
304 | * @typedef {Object} WebAuthn#AuthenticatorAssertionResponse
305 | */
306 |
307 | /**
308 | * Sends the {@link WebAuthn#AuthenticatorAssertionResponse}
309 | * to the server.
310 | *
311 | * @param {WebAuthn#AuthenticatorAssertionResponse} assn The assertion returned from `navigator.credentials.get()`
312 | * @return {Promise.} Resolves to the {@link ServerResponse} from the server on success, or rejects with Error on failure
313 | */
314 | sendLoginResult(assn) {
315 | if (!(assn instanceof window.PublicKeyCredential)) {
316 | throw new Error("expected 'assn' to be instance of PublicKeyCredential");
317 | }
318 |
319 | var msg = CredentialAssertion.from(assn);
320 |
321 | return this.send(
322 | this.loginResponseMethod,
323 | this.loginResponseEndpoint,
324 | msg,
325 | ServerResponse
326 | );
327 | }
328 |
329 | /**
330 | * The lowest-level message sending. Transmits a response over the wire.
331 | *
332 | * @param {String} method "POST", currently throws if non-POST, but this may be changed in the future.
333 | * @param {String} url The REST path to send the data to
334 | * @param {Msg} data The data to be sent, in the form of a {@link Msg} object. This method will convert binary fields to their transmittable form and will validate the data being sent.
335 | * @param {Function} responseConstructor The constructor of the data to be received, which must inherit from {@link ServerResponse}. The data returned from this function will be of this type, as created by {@link Msg.from} and will be validated by {@link Msg.validate}.
336 | * @return {Promise.} Returns a Promise that resolves to a {@link Msg} of the type specified by the `responseConstructor` parameter, or rejects with an Error on failure.
337 | * @fires WebAuthnApp#debugEvent
338 | */
339 | send(method, url, data, responseConstructor) {
340 | // check args
341 | if (method !== "POST") {
342 | return Promise.reject(new Error("why not POST your data?"));
343 | }
344 |
345 | if (typeof url !== "string") {
346 | return Promise.reject(new Error("expected 'url' to be 'string', got: " + typeof url));
347 | }
348 |
349 | if (!(data instanceof Msg)) {
350 | return Promise.reject(new Error("expected 'data' to be instance of 'Msg'"));
351 | }
352 |
353 | if (typeof responseConstructor !== "function") {
354 | return Promise.reject(new Error("expected 'responseConstructor' to be 'function', got: " + typeof responseConstructor));
355 | }
356 |
357 | // convert binary properties (if any) to strings
358 | data.encodeBinaryProperties();
359 |
360 | // validate the data we're sending
361 | try {
362 | data.validate();
363 | } catch (err) {
364 | // console.log("validation error", err);
365 | return Promise.reject(err);
366 | }
367 |
368 | // TODO: maybe some day upgrade to fetch(); have to change the mock in the tests too
369 | return new Promise(function(resolve, reject) {
370 | var xhr = new XMLHttpRequest();
371 | function rejectWithFailed(errorMessage) {
372 | fireDebug("send-error", new Error(errorMessage));
373 | return reject(new Error(errorMessage));
374 | }
375 |
376 | xhr.open(method, url, true);
377 | xhr.setRequestHeader("Content-type", "application/json; charset=utf-8");
378 | xhr.onload = function() {
379 | fireDebug("response-raw", {
380 | status: xhr.status,
381 | body: xhr.responseText
382 | });
383 |
384 | if (xhr.readyState !== 4) {
385 | return rejectWithFailed("server returned ready state: " + xhr.readyState);
386 | }
387 |
388 | var response;
389 | try {
390 | response = JSON.parse(xhr.responseText);
391 | } catch (err) {
392 | if (xhr.status === 200) {
393 | return rejectWithFailed("error parsing JSON response: '" + xhr.responseText + "'");
394 | }
395 | return rejectWithFailed("server returned status: " + xhr.status);
396 | }
397 |
398 | if (Array.isArray(response)) {
399 | response = response[0];
400 | }
401 |
402 | var msg = responseConstructor.from(response);
403 |
404 | if (msg.status === "failed") {
405 | return rejectWithFailed(msg.errorMessage);
406 | }
407 |
408 |
409 | try {
410 | msg.validate();
411 | } catch (err) {
412 | return rejectWithFailed(err.message);
413 | }
414 |
415 | fireDebug("response", {
416 | status: xhr.status,
417 | body: msg
418 | });
419 | return resolve(msg);
420 | };
421 | xhr.onerror = function() {
422 | return rejectWithFailed("POST to URL failed: " + url);
423 | };
424 | fireDebug("send", data);
425 |
426 | data = data.toString();
427 | fireDebug("send-raw", data);
428 | xhr.send(data);
429 | });
430 | }
431 | }
432 |
433 | function fireEvent(type, data) {
434 | // console.log("firing event", type);
435 | var e = new CustomEvent(type, { detail: data || null });
436 | document.dispatchEvent(e);
437 | }
438 |
439 | /**
440 | * Event fired to signal that WebAuthn is not supported in the current context.
441 | *
442 | * @event WebAuthnApp#notSupportedEvent
443 | *
444 | * @property {String} type "webauthn-not-supported"
445 | * @property {String} detail A human-readable reason for why WebAuthn is currently not supported.
446 | */
447 | function fireNotSupported(reason) {
448 | fireEvent("webauthn-not-supported", reason);
449 | // fireDebug("not-supported", reason);
450 | }
451 |
452 | /**
453 | * Debug event, for tracking the internal status of login() and register()
454 | *
455 | * @event WebAuthnApp#debugEvent
456 | * @type {CustomEvent}
457 | * @property {String} type "webauthn-debug"
458 | * @property {Object} detail The details of the event
459 | * @property {String} detail.subtype The sub-type of the "webauth-debug" event.
460 | * Options include: "create-options", "create-result", "create-error", "get-options",
461 | * "get-result", "get-error", "send-error", "send-raw", "send", "response-raw", "response"
462 | * @property {Any} detail.data The data of the event. Varies based on the `subtype` of the event.
463 | */
464 | function fireDebug(subtype, data) {
465 | fireEvent("webauthn-debug", {
466 | subtype: subtype,
467 | data: data
468 | });
469 | }
470 |
471 | /**
472 | * Event that signals state changes for "User Presence" or "User Verification" testing.
473 | * User Presence involves a user simply touching a device (or perhaps a button) to signal
474 | * that the user is present and approves of a registration or log in action. On traditional
475 | * Security Key devices, such as USB Security Keys, this may be signaled to the user by a
476 | * flashing LED light on the device. User Verification is similar to User Presence, but
477 | * involves a user performing biometric authentication (fingerprint, face, etc.) or entering
478 | * a PIN. This event can be caught and a message can be displayed to the user reminding them
479 | * to perform the approperiate action to continue the registration or log in process.
480 | *
481 | * @event WebAuthnApp#userPresenceEvent
482 | * @type {CustomEvent}
483 | * @property {String} type "webauthn-user-presence-start" when the User Presence or User Verification is beginning and waiting for the user.
484 | * @property {String} type "webauthn-user-presence-done" when the User Presence or User Verification has completed (successfully or unsuccessfully)
485 | * @property {null} detail (there are no details for this event)
486 | */
487 | function fireUserPresence(state) {
488 | switch (state) {
489 | case "start":
490 | return fireEvent("webauthn-user-presence-start");
491 | case "done":
492 | return fireEvent("webauthn-user-presence-done");
493 | default:
494 | throw new Error("unknown 'state' in fireUserPresence");
495 | }
496 | }
497 |
498 | /**
499 | * Event that signals the state changes for registration.
500 | *
501 | * @event WebAuthnApp#registerEvent
502 | * @type {CustomEvent}
503 | * @property {String} type "webauthn-register-start"
504 | * @property {String} type "webauthn-register-done"
505 | * @property {String} type "webauthn-register-error"
506 | * @property {String} type "webauthn-register-success"
507 | * @property {null|Error} detail There are no details for these events, except "webauthn-register-error"
508 | * which will have the Error in detail.
509 | */
510 | function fireRegister(state, data) {
511 | switch (state) {
512 | case "start":
513 | return fireEvent("webauthn-register-start");
514 | case "done":
515 | return fireEvent("webauthn-register-done");
516 | case "error":
517 | fireEvent("webauthn-register-error", data);
518 | return fireEvent("webauthn-register-done");
519 | case "success":
520 | fireEvent("webauthn-register-success", data);
521 | return fireEvent("webauthn-register-done");
522 | default:
523 | throw new Error("unknown 'state' in fireRegister");
524 | }
525 | }
526 |
527 | /**
528 | * Event that signals the state changes for log in.
529 | *
530 | * @event WebAuthnApp#loginEvent
531 | * @type {CustomEvent}
532 | * @property {String} type "webauthn-login-start"
533 | * @property {String} type "webauthn-login-done"
534 | * @property {String} type "webauthn-login-error"
535 | * @property {String} type "webauthn-login-success"
536 | * @property {null|Error} detail There are no details for these events, except "webauthn-login-error"
537 | * which will have the Error in detail.
538 | */
539 | function fireLogin(state, data) {
540 | switch (state) {
541 | case "start":
542 | return fireEvent("webauthn-login-start");
543 | case "done":
544 | return fireEvent("webauthn-login-done");
545 | case "error":
546 | fireEvent("webauthn-login-error", data);
547 | return fireEvent("webauthn-login-done");
548 | case "success":
549 | fireEvent("webauthn-login-success", data);
550 | return fireEvent("webauthn-login-done");
551 | default:
552 | throw new Error("unknown 'state' in fireLogin");
553 | }
554 | }
555 |
556 | if (!utils.isBrowser()) {
557 | WebAuthnApp = undefined; // eslint-disable-line no-class-assign
558 | }
559 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export * from "./classes/CreateOptions.js";
2 | export * from "./classes/CreateOptionsRequest.js";
3 | export * from "./classes/CredentialAssertion.js";
4 | export * from "./classes/CredentialAttestation.js";
5 | export * from "./classes/GetOptions.js";
6 | export * from "./classes/GetOptionsRequest.js";
7 | export * from "./classes/Msg.js";
8 | export * from "./classes/ServerResponse.js";
9 | export * from "./classes/WebAuthnApp.js";
10 |
11 | import * as defaultRoutes from "./lib/default-routes.js";
12 | import * as utils from "./lib/utils.js";
13 |
14 | // helpers
15 | let helpers = {};
16 | helpers.utils = utils;
17 | helpers.defaultRoutes = defaultRoutes;
18 | export { helpers as WebAuthnHelpers };
19 |
--------------------------------------------------------------------------------
/lib/browser/detect.js:
--------------------------------------------------------------------------------
1 | export function isBrowser() {
2 | try {
3 | if (!window) return false;
4 | } catch (err) {
5 | return false;
6 | }
7 | return true;
8 | }
9 |
--------------------------------------------------------------------------------
/lib/browser/utils.js:
--------------------------------------------------------------------------------
1 | export function coerceToBase64Url(thing, name) {
2 | // Array or ArrayBuffer to Uint8Array
3 | if (Array.isArray(thing)) {
4 | thing = Uint8Array.from(thing);
5 | }
6 |
7 | if (thing instanceof ArrayBuffer) {
8 | thing = new Uint8Array(thing);
9 | }
10 |
11 | // Uint8Array to base64
12 | if (thing instanceof Uint8Array) {
13 | var str = "";
14 | var len = thing.byteLength;
15 |
16 | for (var i = 0; i < len; i++) {
17 | str += String.fromCharCode(thing[i]);
18 | }
19 | thing = window.btoa(str);
20 | }
21 |
22 | if (typeof thing !== "string") {
23 | throw new Error("could not coerce '" + name + "' to string");
24 | }
25 |
26 | // base64 to base64url
27 | // NOTE: "=" at the end of challenge is optional, strip it off here
28 | thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
29 |
30 | return thing;
31 | }
32 |
33 | export function coerceToArrayBuffer(thing, name) {
34 | if (typeof thing === "string") {
35 | // base64url to base64
36 | thing = thing.replace(/-/g, "+").replace(/_/g, "/");
37 |
38 | // base64 to Uint8Array
39 | var str = window.atob(thing);
40 | var bytes = new Uint8Array(str.length);
41 | for (var i = 0; i < str.length; i++) {
42 | bytes[i] = str.charCodeAt(i);
43 | }
44 | thing = bytes;
45 | }
46 |
47 | // Array to Uint8Array
48 | if (Array.isArray(thing)) {
49 | thing = new Uint8Array(thing);
50 | }
51 |
52 | // Uint8Array to ArrayBuffer
53 | if (thing instanceof Uint8Array) {
54 | thing = thing.buffer;
55 | }
56 |
57 | // error if none of the above worked
58 | if (!(thing instanceof ArrayBuffer)) {
59 | throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
60 | }
61 |
62 | return thing;
63 | }
64 |
--------------------------------------------------------------------------------
/lib/default-routes.js:
--------------------------------------------------------------------------------
1 | export let attestationOptions = "/attestation/options";
2 | export let attestationResult = "/attestation/result";
3 | export let assertionOptions = "/assertion/options";
4 | export let assertionResult = "/assertion/result";
5 |
--------------------------------------------------------------------------------
/lib/input-validation.js:
--------------------------------------------------------------------------------
1 | export function checkType(obj, prop, type) {
2 | switch (typeof type) {
3 | case "string":
4 | if (typeof obj[prop] !== type) {
5 | throw new Error("expected '" + prop + "' to be '" + type + "', got: " + typeof obj[prop]);
6 | }
7 | break;
8 |
9 | case "function":
10 | if (!(obj[prop] instanceof type)) {
11 | throw new Error("expected '" + prop + "' to be '" + type.name + "', got: " + obj[prop]);
12 | }
13 | break;
14 |
15 | default:
16 | throw new Error("internal error: checkType received invalid type");
17 | }
18 | }
19 |
20 | export function checkOptionalType(obj, prop, type) {
21 | if (obj === undefined || obj[prop] === undefined) return;
22 |
23 | checkType(obj, prop, type);
24 | }
25 |
26 | export function checkFormat(obj, prop, format) {
27 | switch (format) {
28 | case "non-empty-string":
29 | checkType(obj, prop, "string");
30 | checkTrue(
31 | obj[prop].length > 0,
32 | "expected '" + prop + "' to be non-empty string"
33 | );
34 | break;
35 | case "base64url":
36 | checkType(obj, prop, "string");
37 | checkTrue(
38 | isBase64Url(obj[prop]),
39 | "expected '" + prop + "' to be base64url format, got: " + obj[prop]
40 | );
41 | break;
42 | case "positive-integer":
43 | checkType(obj, prop, "number");
44 | var n = obj[prop];
45 | checkTrue(
46 | n >>> 0 === parseFloat(n),
47 | "expected '" + prop + "' to be positive integer"
48 | );
49 | break;
50 | case "nullable-base64":
51 | var t = typeof obj[prop];
52 | if (obj[prop] === null) t = "null";
53 | checkTrue(
54 | ["null", "string", "undefined"].includes(t),
55 | "expected '" + prop + "' to be null or string"
56 | );
57 | if (!obj[prop]) return;
58 | checkTrue(
59 | isBase64Url(obj[prop]),
60 | "expected '" + prop + "' to be base64url format, got: " + obj[prop]
61 | );
62 | break;
63 | default:
64 | throw new Error("internal error: unknown format");
65 | }
66 | }
67 |
68 | export function checkOptionalFormat(obj, prop, format) {
69 | if (obj === undefined || obj[prop] === undefined) return;
70 |
71 | checkFormat(obj, prop, format);
72 | }
73 |
74 | export function isBase64Url(str) {
75 | return !!str.match(/^[A-Za-z0-9\-_]+={0,2}$/);
76 | }
77 |
78 | export function checkTrue(truthy, msg) {
79 | if (!truthy) {
80 | throw Error(msg);
81 | }
82 | }
83 |
84 | export function checkUserVerification(val) {
85 | checkTrue(
86 | ["required", "preferred", "discouraged"].includes(val),
87 | "userVerification must be 'required', 'preferred' or 'discouraged'"
88 | );
89 | }
90 |
91 | export function checkAuthenticatorSelection(obj) {
92 | checkOptionalType(obj, "authenticatorSelection", Object);
93 | if (obj.authenticatorSelection && obj.authenticatorSelection.authenticatorAttachment) {
94 | checkTrue(
95 | ["platform", "cross-platform"].includes(obj.authenticatorSelection.authenticatorAttachment),
96 | "authenticatorAttachment must be either 'platform' or 'cross-platform'"
97 | );
98 | }
99 | if (obj.authenticatorSelection && obj.authenticatorSelection.userVerification) {
100 | checkUserVerification(obj.authenticatorSelection.userVerification);
101 |
102 | }
103 | checkOptionalType(obj.authenticatorSelection, "requireResidentKey", "boolean");
104 | }
105 |
106 | export function checkCredentialDescriptorList(arr) {
107 | arr.forEach((cred) => {
108 | checkFormat(cred, "id", "base64url");
109 | checkTrue(cred.type === "public-key", "credential type must be 'public-key'");
110 | checkOptionalType(cred, "transports", Array);
111 | if (cred.transports) cred.transports.forEach((trans) => {
112 | checkTrue(
113 | ["usb", "nfc", "ble"].includes(trans),
114 | "expected transport to be 'usb', 'nfc', or 'ble', got: " + trans
115 | );
116 | });
117 | });
118 | }
119 |
120 | export function checkAttestation(obj) {
121 | if (obj.attestation) checkTrue(
122 | ["direct", "none", "indirect"].includes(obj.attestation),
123 | "expected attestation to be 'direct', 'none', or 'indirect'"
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/lib/node/detect.js:
--------------------------------------------------------------------------------
1 | export function isNode() {
2 | if (typeof module === "object" && module.exports) return true;
3 | return false;
4 | }
5 |
--------------------------------------------------------------------------------
/lib/node/utils.js:
--------------------------------------------------------------------------------
1 | export function coerceToBase64Url(thing, name) {
2 | name = name || "''";
3 |
4 | // Array to Uint8Array
5 | if (Array.isArray(thing)) {
6 | thing = Uint8Array.from(thing);
7 | }
8 |
9 | // Uint8Array, etc. to ArrayBuffer
10 | if (typeof thing === "object" &&
11 | thing.buffer instanceof ArrayBuffer &&
12 | !(thing instanceof Buffer)) {
13 | thing = thing.buffer;
14 | }
15 |
16 | // ArrayBuffer to Buffer
17 | if (thing instanceof ArrayBuffer && !(thing instanceof Buffer)) {
18 | thing = new Buffer(thing);
19 | }
20 |
21 | // Buffer to base64 string
22 | if (thing instanceof Buffer) {
23 | thing = thing.toString("base64");
24 | }
25 |
26 | if (typeof thing !== "string") {
27 | throw new Error(`could not coerce '${name}' to string`);
28 | }
29 |
30 | // base64 to base64url
31 | // NOTE: "=" at the end of challenge is optional, strip it off here so that it's compatible with client
32 | thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");
33 |
34 | return thing;
35 | }
36 |
37 | export function coerceToArrayBuffer(buf, name) {
38 | name = name || "''";
39 |
40 | if (typeof buf === "string") {
41 | // base64url to base64
42 | buf = buf.replace(/-/g, "+").replace(/_/g, "/");
43 | // base64 to Buffer
44 | buf = Buffer.from(buf, "base64");
45 | }
46 |
47 | // Buffer or Array to Uint8Array
48 | if (buf instanceof Buffer || Array.isArray(buf)) {
49 | buf = new Uint8Array(buf);
50 | }
51 |
52 | // Uint8Array to ArrayBuffer
53 | if (buf instanceof Uint8Array) {
54 | buf = buf.buffer;
55 | }
56 |
57 | // error if none of the above worked
58 | if (!(buf instanceof ArrayBuffer)) {
59 | throw new TypeError(`could not coerce '${name}' to ArrayBuffer`);
60 | }
61 |
62 | return buf;
63 | }
64 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | import {
2 | coerceToArrayBuffer as browserCoerceToArrayBuffer,
3 | coerceToBase64Url as browserCoerceToBase64Url
4 | } from "./browser/utils.js";
5 |
6 | import {
7 | coerceToArrayBuffer as nodeCoerceToArrayBuffer,
8 | coerceToBase64Url as nodeCoerceToBase64Url
9 | } from "./node/utils.js";
10 |
11 | import { isBrowser } from "./browser/detect.js";
12 | import { isNode } from "./node/detect.js";
13 |
14 | var coerceToArrayBuffer, coerceToBase64Url;
15 | if (isBrowser()) {
16 | coerceToArrayBuffer = browserCoerceToArrayBuffer;
17 | coerceToBase64Url = browserCoerceToBase64Url;
18 | }
19 |
20 | if (isNode()) {
21 | coerceToArrayBuffer = nodeCoerceToArrayBuffer;
22 | coerceToBase64Url = nodeCoerceToBase64Url;
23 | }
24 |
25 | function ab2str(buf) {
26 | return String.fromCharCode.apply(null, new Uint8Array(buf));
27 | }
28 |
29 | function str2ab(str) {
30 | var buf = new ArrayBuffer(str.length);
31 | var bufView = new Uint8Array(buf);
32 | for (var i = 0, strLen = str.length; i < strLen; i++) {
33 | bufView[i] = str.charCodeAt(i);
34 | }
35 | return buf;
36 | }
37 |
38 | function stringifyObj(obj, depth) {
39 | var str = "";
40 |
41 | // opening bracket
42 | str += "{\n";
43 | depth++;
44 |
45 | // print all properties
46 | for (let key of Object.keys(obj)) {
47 | // add key
48 | str += indent(depth) + key + ": ";
49 | // add value
50 | str += stringifyType(obj, key, depth) + ",\n";
51 | }
52 |
53 | // closing bracket
54 | depth--;
55 | str += indent(depth) + "}";
56 |
57 | return str;
58 | }
59 |
60 | function stringifyArr(arr, depth) {
61 | var str = "";
62 |
63 | // opening brace
64 | str += "[\n";
65 | depth++;
66 |
67 | // print all properties
68 | for (let i = 0; i < arr.length; i++) {
69 | // add value
70 | str += indent(depth) + stringifyType(arr, i, depth) + ",\n";
71 | }
72 |
73 | // closing brace
74 | depth--;
75 | str += indent(depth) + "]";
76 |
77 | return str;
78 | }
79 |
80 | function stringifyType(obj, key, depth) {
81 | // handle native types
82 | switch (typeof obj[key]) {
83 | case "object": break;
84 | case "undefined": return "undefined";
85 | // case "string": return "\"" + obj[key].replace(/\n/g, "\\n\"\n" + indent(depth + 1) + "\"") + "\"";
86 | case "string": return "\"" + obj[key].replace(/\n/g, "\n" + indent(depth + 1)) + "\"";
87 | case "number": return obj[key].toString();
88 | case "boolean": return obj[key].toString();
89 | case "symbol": return obj[key].toString();
90 | default:
91 | throw new TypeError("unknown type in stringifyType: " + typeof obj[key]);
92 | }
93 |
94 | // handle objects
95 | switch (true) {
96 | case obj[key] instanceof ArrayBuffer:
97 | return abToHumanStr(obj[key], (depth + 1));
98 | case obj[key] instanceof Array:
99 | return stringifyArr(obj[key], depth);
100 | case obj[key] instanceof Set:
101 | return stringifyArr([...obj[key]], depth);
102 | case obj[key] instanceof Map:
103 | return stringifyObj(mapToObj(obj[key]), depth);
104 | default:
105 | return stringifyObj(obj[key], depth);
106 | }
107 | }
108 |
109 | function indent(depth) {
110 | var ret = "";
111 |
112 | for (let i = 0; i < depth * 4; i++) {
113 | ret += " ";
114 | }
115 |
116 | return ret;
117 | }
118 |
119 | // printHex
120 | function abToHumanStr(buf, depth) {
121 | var ret = "";
122 |
123 | // if the buffer was a TypedArray (e.g. Uint8Array), grab its buffer and use that
124 | if (ArrayBuffer.isView(buf) && buf.buffer instanceof ArrayBuffer) {
125 | buf = buf.buffer;
126 | }
127 |
128 | // check the arguments
129 | if ((typeof depth != "number") ||
130 | (typeof buf != "object")) {
131 | throw new TypeError("Bad args to abToHumanStr");
132 | }
133 | if (!(buf instanceof ArrayBuffer)) {
134 | throw new TypeError("Attempted abToHumanStr with non-ArrayBuffer:", buf);
135 | }
136 | // print the buffer as a 16 byte long hex string
137 | var arr = new Uint8Array(buf);
138 | var len = buf.byteLength;
139 | var i, str = "";
140 | ret += `[ArrayBuffer] (${buf.byteLength} bytes)\n`;
141 | for (i = 0; i < len; i++) {
142 | var hexch = arr[i].toString(16);
143 | hexch = (hexch.length == 1) ? ("0" + hexch) : hexch;
144 | str += hexch.toUpperCase() + " ";
145 | if (i && !((i + 1) % 16)) {
146 | ret += indent(depth) + str.replace(/.$/, "\n");
147 | str = "";
148 | }
149 | }
150 | // print the remaining bytes
151 | if ((i) % 16) {
152 | ret += indent(depth) + str.replace(/.$/, "\n");
153 | }
154 |
155 | // remove final newline
156 | ret = ret.replace(/\n$/, "");
157 |
158 | return ret;
159 | }
160 |
161 | function mapToObj(mapObj) {
162 | var m = {};
163 | mapObj.forEach((v, k) => {
164 | m[k] = v;
165 | });
166 | return m;
167 | }
168 |
169 | function copyProp(src, dst, prop) {
170 | if (src[prop] !== undefined) dst[prop] = src[prop];
171 | }
172 |
173 | function copyPropList(src, dst, propList) {
174 | var i;
175 | for (i = 0; i < propList.length; i++) {
176 | copyProp(src, dst, propList[i]);
177 | }
178 | }
179 |
180 | export {
181 | coerceToArrayBuffer,
182 | coerceToBase64Url,
183 | isBrowser,
184 | isNode,
185 | ab2str,
186 | str2ab,
187 | stringifyObj,
188 | stringifyArr,
189 | stringifyType,
190 | indent,
191 | abToHumanStr,
192 | mapToObj,
193 | copyProp,
194 | copyPropList
195 | };
196 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webauthn-simple-app",
3 | "version": "2.1.0",
4 | "description": "webauthn-simple-app",
5 | "main": "dist/webauthn-simple-app.cjs.js",
6 | "module": "dist/webauthn-simple-app.esm.js",
7 | "directories": {
8 | "test": "test"
9 | },
10 | "scripts": {
11 | "build": "rollup -c rollup.config.js --environment NODE_ENV:development",
12 | "build:prod": "rollup -c rollup.config.js --environment NODE_ENV:production",
13 | "postinstall": "npm run build:prod",
14 | "test:node": "npm run build:prod && mocha --require test/node/test-setup.js test/common/*.js test/node/*.js",
15 | "test:browser": "grunt default",
16 | "test": "npm run test:node && npm run test:browser",
17 | "prepublishOnly": "npm run clean && npm run test",
18 | "clean": "rm -rf dist docs",
19 | "docs": "jsdoc -c ./.jsdoc-conf.json",
20 | "publish-docs": "gh-pages --repo https://$GH_TOKEN@github.com/apowers313/webauthn-simple-app.git --dist docs"
21 | },
22 | "keywords": [
23 | "webauthn",
24 | "fido",
25 | "fido2"
26 | ],
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/apowers313/webauthn-simple-app.git"
30 | },
31 | "author": "Adam Powers",
32 | "contributors": [
33 | {
34 | "name": "@madwizard-thomas"
35 | }
36 | ],
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/apowers313/webauthn-simple-app/issues",
40 | "email": "apowers@ato.ms"
41 | },
42 | "homepage": "https://github.com/apowers313/webauthn-simple-app#readme",
43 | "dependencies": {
44 | "rollup": "^0.61.2"
45 | },
46 | "devDependencies": {
47 | "chai": "^3.5.0",
48 | "docdash": "^0.4.0",
49 | "fido2-helpers": "^1.4.0",
50 | "gh-pages": "^0.12.0",
51 | "grunt": "^1.0.1",
52 | "grunt-contrib-connect": "^1.0.2",
53 | "grunt-saucelabs": "^9.0.0",
54 | "jsdoc": "^3.5.5",
55 | "mocha": "^3.2.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | const env = process.env.NODE_ENV; // eslint-disable-line no-process-env
2 |
3 | export default [
4 | {
5 | input: "index.js",
6 | output: [
7 | {
8 | file: "dist/webauthn-simple-app.umd.js",
9 | format: "umd",
10 | name: "WebAuthnSimpleApp",
11 | sourcemap: (env === "development")
12 | },
13 | {
14 | file: "dist/webauthn-simple-app.esm.js",
15 | format: "es",
16 | sourcemap: (env === "development")
17 | },
18 | {
19 | file: "dist/webauthn-simple-app.cjs.js",
20 | format: "cjs",
21 | name: "WebAuthnSimpleApp",
22 | sourcemap: (env === "development")
23 | }
24 | ]
25 | }
26 | ];
27 |
--------------------------------------------------------------------------------
/test/browser/css/mocha.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | body {
4 | margin:0;
5 | }
6 |
7 | #mocha {
8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
9 | margin: 60px 50px;
10 | }
11 |
12 | #mocha ul,
13 | #mocha li {
14 | margin: 0;
15 | padding: 0;
16 | }
17 |
18 | #mocha ul {
19 | list-style: none;
20 | }
21 |
22 | #mocha h1,
23 | #mocha h2 {
24 | margin: 0;
25 | }
26 |
27 | #mocha h1 {
28 | margin-top: 15px;
29 | font-size: 1em;
30 | font-weight: 200;
31 | }
32 |
33 | #mocha h1 a {
34 | text-decoration: none;
35 | color: inherit;
36 | }
37 |
38 | #mocha h1 a:hover {
39 | text-decoration: underline;
40 | }
41 |
42 | #mocha .suite .suite h1 {
43 | margin-top: 0;
44 | font-size: .8em;
45 | }
46 |
47 | #mocha .hidden {
48 | display: none;
49 | }
50 |
51 | #mocha h2 {
52 | font-size: 12px;
53 | font-weight: normal;
54 | cursor: pointer;
55 | }
56 |
57 | #mocha .suite {
58 | margin-left: 15px;
59 | }
60 |
61 | #mocha .test {
62 | margin-left: 15px;
63 | overflow: hidden;
64 | }
65 |
66 | #mocha .test.pending:hover h2::after {
67 | content: '(pending)';
68 | font-family: arial, sans-serif;
69 | }
70 |
71 | #mocha .test.pass.medium .duration {
72 | background: #c09853;
73 | }
74 |
75 | #mocha .test.pass.slow .duration {
76 | background: #b94a48;
77 | }
78 |
79 | #mocha .test.pass::before {
80 | content: '✓';
81 | font-size: 12px;
82 | display: block;
83 | float: left;
84 | margin-right: 5px;
85 | color: #00d6b2;
86 | }
87 |
88 | #mocha .test.pass .duration {
89 | font-size: 9px;
90 | margin-left: 5px;
91 | padding: 2px 5px;
92 | color: #fff;
93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
96 | -webkit-border-radius: 5px;
97 | -moz-border-radius: 5px;
98 | -ms-border-radius: 5px;
99 | -o-border-radius: 5px;
100 | border-radius: 5px;
101 | }
102 |
103 | #mocha .test.pass.fast .duration {
104 | display: none;
105 | }
106 |
107 | #mocha .test.pending {
108 | color: #0b97c4;
109 | }
110 |
111 | #mocha .test.pending::before {
112 | content: '◦';
113 | color: #0b97c4;
114 | }
115 |
116 | #mocha .test.fail {
117 | color: #c00;
118 | }
119 |
120 | #mocha .test.fail pre {
121 | color: black;
122 | }
123 |
124 | #mocha .test.fail::before {
125 | content: '✖';
126 | font-size: 12px;
127 | display: block;
128 | float: left;
129 | margin-right: 5px;
130 | color: #c00;
131 | }
132 |
133 | #mocha .test pre.error {
134 | color: #c00;
135 | max-height: 300px;
136 | overflow: auto;
137 | }
138 |
139 | /**
140 | * (1): approximate for browsers not supporting calc
141 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
142 | * ^^ seriously
143 | */
144 | #mocha .test pre {
145 | display: block;
146 | float: left;
147 | clear: left;
148 | font: 12px/1.5 monaco, monospace;
149 | margin: 5px;
150 | padding: 15px;
151 | border: 1px solid #eee;
152 | max-width: 85%; /*(1)*/
153 | max-width: calc(100% - 42px); /*(2)*/
154 | word-wrap: break-word;
155 | border-bottom-color: #ddd;
156 | -webkit-border-radius: 3px;
157 | -webkit-box-shadow: 0 1px 3px #eee;
158 | -moz-border-radius: 3px;
159 | -moz-box-shadow: 0 1px 3px #eee;
160 | border-radius: 3px;
161 | }
162 |
163 | #mocha .test h2 {
164 | position: relative;
165 | }
166 |
167 | #mocha .test a.replay {
168 | position: absolute;
169 | top: 3px;
170 | right: 0;
171 | text-decoration: none;
172 | vertical-align: middle;
173 | display: block;
174 | width: 15px;
175 | height: 15px;
176 | line-height: 15px;
177 | text-align: center;
178 | background: #eee;
179 | font-size: 15px;
180 | -moz-border-radius: 15px;
181 | border-radius: 15px;
182 | -webkit-transition: opacity 200ms;
183 | -moz-transition: opacity 200ms;
184 | transition: opacity 200ms;
185 | opacity: 0.3;
186 | color: #888;
187 | }
188 |
189 | #mocha .test:hover a.replay {
190 | opacity: 1;
191 | }
192 |
193 | #mocha-report.pass .test.fail {
194 | display: none;
195 | }
196 |
197 | #mocha-report.fail .test.pass {
198 | display: none;
199 | }
200 |
201 | #mocha-report.pending .test.pass,
202 | #mocha-report.pending .test.fail {
203 | display: none;
204 | }
205 | #mocha-report.pending .test.pass.pending {
206 | display: block;
207 | }
208 |
209 | #mocha-error {
210 | color: #c00;
211 | font-size: 1.5em;
212 | font-weight: 100;
213 | letter-spacing: 1px;
214 | }
215 |
216 | #mocha-stats {
217 | position: fixed;
218 | top: 15px;
219 | right: 10px;
220 | font-size: 12px;
221 | margin: 0;
222 | color: #888;
223 | z-index: 1;
224 | }
225 |
226 | #mocha-stats .progress {
227 | float: right;
228 | padding-top: 0;
229 | }
230 |
231 | #mocha-stats em {
232 | color: black;
233 | }
234 |
235 | #mocha-stats a {
236 | text-decoration: none;
237 | color: inherit;
238 | }
239 |
240 | #mocha-stats a:hover {
241 | border-bottom: 1px solid #eee;
242 | }
243 |
244 | #mocha-stats li {
245 | display: inline-block;
246 | margin: 0 5px;
247 | list-style: none;
248 | padding-top: 11px;
249 | }
250 |
251 | #mocha-stats canvas {
252 | width: 40px;
253 | height: 40px;
254 | }
255 |
256 | #mocha code .comment { color: #ddd; }
257 | #mocha code .init { color: #2f6fad; }
258 | #mocha code .string { color: #5890ad; }
259 | #mocha code .keyword { color: #8a6343; }
260 | #mocha code .number { color: #2f6fad; }
261 |
262 | @media screen and (max-device-width: 480px) {
263 | #mocha {
264 | margin: 60px 0px;
265 | }
266 |
267 | #mocha #stats {
268 | position: absolute;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/test/browser/test-setup.js:
--------------------------------------------------------------------------------
1 | /* globals chai, mocha */
2 |
3 | import * as tmp from "../../index.js";
4 | window.GlobalWebAuthnClasses = tmp;
5 | window.assert = window.chai.assert;
6 | window.mocha.setup("bdd");
7 |
8 | onload = function() {
9 | //mocha.checkLeaks();
10 | //mocha.globals(['foo']);
11 | var runner = mocha.run();
12 |
13 | var failedTests = [];
14 | runner.on("end", function() {
15 | window.mochaResults = runner.stats;
16 | window.mochaResults.reports = failedTests;
17 | });
18 |
19 | runner.on("fail", logFailure);
20 |
21 | function logFailure(test, err) {
22 |
23 | function flattenTitles(test) {
24 | var titles = [];
25 | while (test.parent.title) {
26 | titles.push(test.parent.title);
27 | test = test.parent;
28 | }
29 | return titles.reverse();
30 | }
31 |
32 | failedTests.push({
33 | name: test.title,
34 | result: false,
35 | message: err.message,
36 | stack: err.stack,
37 | titles: flattenTitles(test)
38 | });
39 | }
40 | };
41 |
42 |
--------------------------------------------------------------------------------
/test/browser/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WebAuthn Simple Application Tests
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 |
--------------------------------------------------------------------------------
/test/common/create-options-request-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("CreateOptionsRequest", function() {
4 | const {
5 | CreateOptionsRequest,
6 | Msg
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(CreateOptionsRequest);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new CreateOptionsRequest();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var inputObj = {
20 | username: "adam",
21 | displayName: "AdamPowers"
22 | };
23 | var msg = CreateOptionsRequest.from(inputObj);
24 |
25 | var outputObj = msg.toObject();
26 |
27 | assert.deepEqual(outputObj, inputObj);
28 | });
29 |
30 | describe("validate", function() {
31 | var testArgs;
32 | beforeEach(function() {
33 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.creationOptionsRequest);
34 | });
35 |
36 | it("passes with basic args", function() {
37 | var msg = CreateOptionsRequest.from(testArgs);
38 | msg.validate();
39 | });
40 |
41 | it("throws on missing username", function() {
42 | delete testArgs.username;
43 | var msg = CreateOptionsRequest.from(testArgs);
44 |
45 | assert.throws(() => {
46 | msg.validate();
47 | }, Error, "expected 'username' to be 'string', got: undefined");
48 | });
49 |
50 | it("throws on empty username", function() {
51 | testArgs.username = "";
52 | var msg = CreateOptionsRequest.from(testArgs);
53 |
54 | assert.throws(() => {
55 | msg.validate();
56 | }, Error, "expected 'username' to be non-empty string");
57 | });
58 |
59 | it("throws on missing displayName", function() {
60 | delete testArgs.displayName;
61 | var msg = CreateOptionsRequest.from(testArgs);
62 |
63 | assert.throws(() => {
64 | msg.validate();
65 | }, Error, "expected 'displayName' to be 'string', got: undefined");
66 | });
67 |
68 | it("throws on empty displayName", function() {
69 | testArgs.displayName = "";
70 | var msg = CreateOptionsRequest.from(testArgs);
71 |
72 | assert.throws(() => {
73 | msg.validate();
74 | }, Error, "expected 'displayName' to be non-empty string");
75 | });
76 |
77 | it("passes with extraData", function() {
78 | testArgs.extraData = "AAAA==";
79 | var msg = CreateOptionsRequest.from(testArgs);
80 |
81 | msg.validate();
82 | });
83 |
84 | it("passes with undefined extraData", function() {
85 | testArgs.extraData = undefined;
86 | var msg = CreateOptionsRequest.from(testArgs);
87 |
88 | msg.validate();
89 | });
90 |
91 | it("throws on non-string extraData", function() {
92 | testArgs.extraData = 42;
93 | var msg = CreateOptionsRequest.from(testArgs);
94 |
95 | assert.throws(() => {
96 | msg.validate();
97 | }, Error, "expected 'extraData' to be 'string', got: number");
98 | });
99 |
100 | it("throws on non-base64url extraData", function() {
101 | testArgs.extraData = "!!!";
102 | var msg = CreateOptionsRequest.from(testArgs);
103 |
104 | assert.throws(() => {
105 | msg.validate();
106 | }, Error, "expected 'extraData' to be base64url format, got: !!!");
107 | });
108 | });
109 |
110 | describe("decodeBinaryProperties", function() {
111 | it("doesn't throw", function() {
112 | var msg = CreateOptionsRequest.from(fido2Helpers.server.creationOptionsRequest);
113 | msg.decodeBinaryProperties();
114 | });
115 | });
116 |
117 | describe("encodeBinaryProperties", function() {
118 | it("doesn't throw", function() {
119 | var msg = CreateOptionsRequest.from(fido2Helpers.server.creationOptionsRequest);
120 | msg.encodeBinaryProperties();
121 | });
122 | });
123 |
124 | describe("toHumanString", function() {
125 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
126 | it("creates correct string", function() {
127 | var msg = CreateOptionsRequest.from(fido2Helpers.server.creationOptionsRequest);
128 | var str = msg.toHumanString();
129 | assert.isString(str);
130 | assert.strictEqual(
131 | str,
132 | // eslint-disable-next-line
133 | `[CreateOptionsRequest] {
134 | username: "bubba",
135 | displayName: "Bubba Smith",
136 | authenticatorSelection: {
137 | authenticatorAttachment: "cross-platform",
138 | requireResidentKey: false,
139 | userVerification: "preferred",
140 | },
141 | attestation: "none",
142 | }`
143 | );
144 | });
145 | });
146 | });
147 |
--------------------------------------------------------------------------------
/test/common/create-options-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("CreateOptions", function() {
4 | const {
5 | CreateOptions,
6 | ServerResponse
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(CreateOptions);
11 | });
12 |
13 | it("is ServerResponse class", function() {
14 | var msg = new CreateOptions();
15 | assert.instanceOf(msg, ServerResponse);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
20 |
21 | var outputObj = msg.toObject();
22 |
23 | assert.deepEqual(outputObj, fido2Helpers.server.completeCreationOptions);
24 | });
25 |
26 | describe("validate", function() {
27 | var testArgs;
28 | beforeEach(function() {
29 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.completeCreationOptions);
30 | });
31 |
32 | it("accepts basic CreateOptions", function() {
33 | var msg = CreateOptions.from(fido2Helpers.server.basicCreationOptions);
34 |
35 | msg.validate();
36 | });
37 |
38 | it("accepts complete CreateOptions", function() {
39 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
40 |
41 | msg.validate();
42 | });
43 |
44 | it("throws on bad ServerResponse", function() {
45 | delete testArgs.status;
46 | var msg = CreateOptions.from(testArgs);
47 |
48 | assert.throws(() => {
49 | msg.validate();
50 | }, Error, "expected 'status' to be 'string', got: undefined");
51 | });
52 |
53 | it("throws on missing rp", function() {
54 | delete testArgs.rp;
55 | var msg = CreateOptions.from(testArgs);
56 |
57 | assert.throws(() => {
58 | msg.validate();
59 | }, Error, "expected 'rp' to be 'Object', got: undefined");
60 | });
61 |
62 | it("throws on missing rp.name", function() {
63 | delete testArgs.rp.name;
64 | var msg = CreateOptions.from(testArgs);
65 |
66 | assert.throws(() => {
67 | msg.validate();
68 | }, Error, "expected 'name' to be 'string', got: undefined");
69 | });
70 |
71 | it("throws on empty rp.name", function() {
72 | testArgs.rp.name = "";
73 | var msg = CreateOptions.from(testArgs);
74 |
75 | assert.throws(() => {
76 | msg.validate();
77 | }, Error, "expected 'name' to be non-empty string");
78 | });
79 |
80 | it("throws on non-string rp.name", function() {
81 | testArgs.rp.name = 42;
82 | var msg = CreateOptions.from(testArgs);
83 |
84 | assert.throws(() => {
85 | msg.validate();
86 | }, Error, "expected 'name' to be 'string', got: number");
87 | });
88 |
89 | it("throws on empty rp.id", function() {
90 | testArgs.rp.id = "";
91 | var msg = CreateOptions.from(testArgs);
92 |
93 | assert.throws(() => {
94 | msg.validate();
95 | }, Error, "expected 'id' to be non-empty string");
96 | });
97 |
98 | it("throws on non-string rp.id", function() {
99 | testArgs.rp.id = 42;
100 | var msg = CreateOptions.from(testArgs);
101 |
102 | assert.throws(() => {
103 | msg.validate();
104 | }, Error, "expected 'id' to be 'string', got: number");
105 | });
106 |
107 | it("throws on empty rp.icon", function() {
108 | testArgs.rp.icon = "";
109 | var msg = CreateOptions.from(testArgs);
110 |
111 | assert.throws(() => {
112 | msg.validate();
113 | }, Error, "expected 'icon' to be non-empty string");
114 | });
115 |
116 | it("throws on non-string rp.icon", function() {
117 | testArgs.rp.icon = 42;
118 | var msg = CreateOptions.from(testArgs);
119 |
120 | assert.throws(() => {
121 | msg.validate();
122 | }, Error, "expected 'icon' to be 'string', got: number");
123 | });
124 |
125 | it("throws on missing user", function() {
126 | delete testArgs.user;
127 | var msg = CreateOptions.from(testArgs);
128 |
129 | assert.throws(() => {
130 | msg.validate();
131 | }, Error, "expected 'user' to be 'Object', got: undefined");
132 | });
133 |
134 | it("throws on missing user.name", function() {
135 | delete testArgs.user.name;
136 | var msg = CreateOptions.from(testArgs);
137 |
138 | assert.throws(() => {
139 | msg.validate();
140 | }, Error, "expected 'name' to be 'string', got: undefined");
141 | });
142 |
143 | it("throws on missing user.displayName", function() {
144 | delete testArgs.user.displayName;
145 | var msg = CreateOptions.from(testArgs);
146 |
147 | assert.throws(() => {
148 | msg.validate();
149 | }, Error, "expected 'displayName' to be 'string', got: undefined");
150 | });
151 |
152 | it("throws on missing user.id", function() {
153 | delete testArgs.user.id;
154 | var msg = CreateOptions.from(testArgs);
155 |
156 | assert.throws(() => {
157 | msg.validate();
158 | }, Error, "expected 'id' to be 'string', got: undefined");
159 | });
160 |
161 | it("throws on missing challenge", function() {
162 | delete testArgs.challenge;
163 | var msg = CreateOptions.from(testArgs);
164 |
165 | assert.throws(() => {
166 | msg.validate();
167 | }, Error, "expected 'challenge' to be 'string', got: undefined");
168 | });
169 |
170 | it("throws on missing pubKeyCredParams", function() {
171 | delete testArgs.pubKeyCredParams;
172 | var msg = CreateOptions.from(testArgs);
173 |
174 | assert.throws(() => {
175 | msg.validate();
176 | }, Error, "expected 'pubKeyCredParams' to be 'Array', got: undefined");
177 | });
178 |
179 | it("throws on missing pubKeyCredParams[0].type", function() {
180 | delete testArgs.pubKeyCredParams[0].type;
181 | var msg = CreateOptions.from(testArgs);
182 |
183 | assert.throws(() => {
184 | msg.validate();
185 | }, Error, "credential type must be 'public-key'");
186 | });
187 |
188 | it("throws on missing pubKeyCredParams[0].alg", function() {
189 | delete testArgs.pubKeyCredParams[0].alg;
190 | var msg = CreateOptions.from(testArgs);
191 |
192 | assert.throws(() => {
193 | msg.validate();
194 | }, Error, "expected 'alg' to be 'number', got: undefined");
195 | });
196 |
197 | it("throws on negative timeout", function() {
198 | testArgs.timeout = -1;
199 | var msg = CreateOptions.from(testArgs);
200 |
201 | assert.throws(() => {
202 | msg.validate();
203 | }, Error, "expected 'timeout' to be positive integer");
204 | });
205 |
206 | it("throws on timeout NaN", function() {
207 | testArgs.timeout = NaN;
208 | var msg = CreateOptions.from(testArgs);
209 |
210 | assert.throws(() => {
211 | msg.validate();
212 | }, Error, "expected 'timeout' to be positive integer");
213 | });
214 |
215 | it("throws on timeout float", function() {
216 | testArgs.timeout = 3.14159;
217 | var msg = CreateOptions.from(testArgs);
218 |
219 | assert.throws(() => {
220 | msg.validate();
221 | }, Error, "expected 'timeout' to be positive integer");
222 | });
223 |
224 | it("throws on missing excludeCredentials[0].type", function() {
225 | delete testArgs.excludeCredentials[0].type;
226 | var msg = CreateOptions.from(testArgs);
227 |
228 | assert.throws(() => {
229 | msg.validate();
230 | }, Error, "credential type must be 'public-key'");
231 | });
232 |
233 | it("throws on missing excludeCredentials[0].id", function() {
234 | delete testArgs.excludeCredentials[0].id;
235 | var msg = CreateOptions.from(testArgs);
236 |
237 | assert.throws(() => {
238 | msg.validate();
239 | }, Error, "expected 'id' to be 'string', got: undefined");
240 | });
241 |
242 | it("allows missing excludeCredentials[0].transports", function() {
243 | delete testArgs.excludeCredentials[0].transports;
244 | var msg = CreateOptions.from(testArgs);
245 |
246 | msg.validate();
247 | });
248 |
249 | it("throws on non-Array excludeCredentials[0].transports", function() {
250 | testArgs.excludeCredentials[0].transports = 42;
251 | var msg = CreateOptions.from(testArgs);
252 |
253 | assert.throws(() => {
254 | msg.validate();
255 | }, Error, "expected 'transports' to be 'Array', got: 42");
256 | });
257 |
258 | it("throws on invalid excludeCredentials[0].transports string", function() {
259 | testArgs.excludeCredentials[0].transports = ["blah"];
260 | var msg = CreateOptions.from(testArgs);
261 |
262 | assert.throws(() => {
263 | msg.validate();
264 | }, Error, "expected transport to be 'usb', 'nfc', or 'ble', got: blah");
265 | });
266 |
267 | it("throws on invalid excludeCredentials[0].transports type", function() {
268 | testArgs.excludeCredentials[0].transports = [42];
269 | var msg = CreateOptions.from(testArgs);
270 |
271 | assert.throws(() => {
272 | msg.validate();
273 | }, Error, "expected transport to be 'usb', 'nfc', or 'ble', got: 42");
274 | });
275 |
276 | it("allows empty excludeCredentials[0].transports", function() {
277 | testArgs.excludeCredentials[0].transports = [];
278 | var msg = CreateOptions.from(testArgs);
279 |
280 | msg.validate();
281 | });
282 |
283 | it("throws on wrong type authenticatorSelection", function() {
284 | testArgs.authenticatorSelection = "hi";
285 | var msg = CreateOptions.from(testArgs);
286 |
287 | assert.throws(() => {
288 | msg.validate();
289 | }, Error, "expected 'authenticatorSelection' to be 'Object', got: hi");
290 | });
291 |
292 | it("throws on wrong type authenticatorSelection.authenticatorAttachment", function() {
293 | testArgs.authenticatorSelection.authenticatorAttachment = 42;
294 | var msg = CreateOptions.from(testArgs);
295 |
296 | assert.throws(() => {
297 | msg.validate();
298 | }, Error, "authenticatorAttachment must be either 'platform' or 'cross-platform'");
299 | });
300 |
301 | it("throws on invalid authenticatorSelection.authenticatorAttachment", function() {
302 | testArgs.authenticatorSelection.authenticatorAttachment = "beer";
303 | var msg = CreateOptions.from(testArgs);
304 |
305 | assert.throws(() => {
306 | msg.validate();
307 | }, Error, "authenticatorAttachment must be either 'platform' or 'cross-platform'");
308 | });
309 |
310 | it("throws on wrong type authenticatorSelection.userVerification", function() {
311 | testArgs.authenticatorSelection.userVerification = 42;
312 | var msg = CreateOptions.from(testArgs);
313 |
314 | assert.throws(() => {
315 | msg.validate();
316 | }, Error, "userVerification must be 'required', 'preferred' or 'discouraged'");
317 | });
318 |
319 | it("throws on invalid authenticatorSelection.userVerification", function() {
320 | testArgs.authenticatorSelection.userVerification = "bob";
321 | var msg = CreateOptions.from(testArgs);
322 |
323 | assert.throws(() => {
324 | msg.validate();
325 | }, Error, "userVerification must be 'required', 'preferred' or 'discouraged'");
326 | });
327 |
328 | it("throws on wrong type authenticatorSelection.requireResidentKey", function() {
329 | testArgs.authenticatorSelection.requireResidentKey = "hi";
330 | var msg = CreateOptions.from(testArgs);
331 |
332 | assert.throws(() => {
333 | msg.validate();
334 | }, Error, "expected 'requireResidentKey' to be 'boolean', got: string");
335 | });
336 |
337 | it("throws on invalid attestation", function() {
338 | testArgs.attestation = "hi";
339 | var msg = CreateOptions.from(testArgs);
340 |
341 | assert.throws(() => {
342 | msg.validate();
343 | }, Error, "expected attestation to be 'direct', 'none', or 'indirect'");
344 | });
345 |
346 | it("throws on invalid extensions", function() {
347 | testArgs.extensions = "hi";
348 | var msg = CreateOptions.from(testArgs);
349 |
350 | assert.throws(() => {
351 | msg.validate();
352 | }, Error, "expected 'extensions' to be 'Object', got: hi");
353 | });
354 |
355 | it("passes with rawChallenge", function() {
356 | testArgs.rawChallenge = "AAAA";
357 | var msg = CreateOptions.from(testArgs);
358 |
359 | msg.validate();
360 | });
361 |
362 | it("passes with undefined rawChallenge", function() {
363 | testArgs.rawChallenge = undefined;
364 | var msg = CreateOptions.from(testArgs);
365 |
366 | msg.validate();
367 | });
368 |
369 | it("throws on non-string rawChallenge", function() {
370 | testArgs.rawChallenge = 42;
371 | var msg = CreateOptions.from(testArgs);
372 |
373 | assert.throws(() => {
374 | msg.validate();
375 | }, Error, "expected 'rawChallenge' to be 'string', got: number");
376 | });
377 |
378 | it("throws on non-base64url rawChallenge", function() {
379 | testArgs.rawChallenge = "!!!";
380 | var msg = CreateOptions.from(testArgs);
381 |
382 | assert.throws(() => {
383 | msg.validate();
384 | }, Error, "expected 'rawChallenge' to be base64url format, got: !!!");
385 | });
386 | });
387 |
388 | describe("decodeBinaryProperties", function() {
389 | it("decodes correct fields", function() {
390 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
391 | assert.isString(msg.user.id);
392 | assert.isString(msg.challenge);
393 | msg.decodeBinaryProperties();
394 | assert.instanceOf(msg.user.id, ArrayBuffer);
395 | assert.instanceOf(msg.challenge, ArrayBuffer);
396 | assert.strictEqual(msg.excludeCredentials.length, 1);
397 | msg.excludeCredentials.forEach((cred) => {
398 | assert.instanceOf(cred.id, ArrayBuffer);
399 | });
400 | });
401 |
402 | it("decodes rawChallenge", function() {
403 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
404 | msg.rawChallenge = "AAAA";
405 | msg.decodeBinaryProperties();
406 | assert.instanceOf(msg.rawChallenge, ArrayBuffer);
407 | assert.strictEqual(msg.rawChallenge.byteLength, 3);
408 | });
409 | });
410 |
411 | describe("encodeBinaryProperties", function() {
412 | it("encodes correct fields", function() {
413 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
414 | msg.decodeBinaryProperties();
415 | assert.instanceOf(msg.user.id, ArrayBuffer);
416 | assert.instanceOf(msg.challenge, ArrayBuffer);
417 | assert.strictEqual(msg.excludeCredentials.length, 1);
418 | msg.excludeCredentials.forEach((cred) => {
419 | assert.instanceOf(cred.id, ArrayBuffer);
420 | });
421 | msg.encodeBinaryProperties();
422 | assert.isString(msg.user.id);
423 | assert.isString(msg.challenge);
424 | assert.strictEqual(msg.excludeCredentials.length, 1);
425 | msg.excludeCredentials.forEach((cred) => {
426 | assert.isString(cred.id);
427 | });
428 | });
429 |
430 | it("encodes rawChallenge", function() {
431 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
432 | msg.decodeBinaryProperties();
433 | msg.rawChallenge = new Uint8Array([0x00, 0x00, 0x00]).buffer;
434 | msg.encodeBinaryProperties();
435 | assert.strictEqual(msg.rawChallenge, "AAAA");
436 | });
437 | });
438 |
439 | describe("toHumanString", function() {
440 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
441 | it("creates correct string", function() {
442 | var msg = CreateOptions.from(fido2Helpers.server.completeCreationOptions);
443 | var str = msg.toHumanString();
444 | assert.isString(str);
445 | assert.strictEqual(
446 | str,
447 | // eslint-disable-next-line
448 | `[CreateOptions] {
449 | status: "ok",
450 | rp: {
451 | name: "My RP",
452 | id: "TXkgUlA=",
453 | icon: "aWNvbnBuZ2RhdGFibGFoYmxhaGJsYWg=",
454 | },
455 | user: {
456 | id: [ArrayBuffer] (4 bytes)
457 | 61 64 61 6D,
458 | displayName: "Adam Powers",
459 | name: "apowers",
460 | icon: "aWNvbnBuZ2RhdGFibGFoYmxhaGJsYWg=",
461 | },
462 | challenge: [ArrayBuffer] (64 bytes)
463 | B0 FE 0C 8B 0A 1D 8E B7 82 F3 EF 34 20 C8 DC C9
464 | 63 65 A3 F6 35 48 95 E6 16 04 0D 06 29 67 8D D7
465 | F7 D1 64 6C 8C 50 E1 0D 89 9F 63 8F B8 BA 1A B6
466 | 1C 58 D8 44 46 D7 76 BE 95 8E EB F3 D9 7B D3 8C,
467 | pubKeyCredParams: [
468 | {
469 | alg: -7,
470 | type: "public-key",
471 | },
472 | ],
473 | timeout: 30000,
474 | excludeCredentials: [
475 | {
476 | type: "public-key",
477 | id: [ArrayBuffer] (162 bytes)
478 | 00 08 47 ED C9 CF 44 19 1C BA 48 E7 73 61 B6 18
479 | CD 47 E5 D9 15 B3 D3 F5 AB 65 44 AE 10 F9 EE 99
480 | 33 29 58 C1 6E 2C 5D B2 E7 E3 5E 15 0E 7E 20 F6
481 | EC 3D 15 03 E7 CF 29 45 58 34 61 36 5D 87 23 86
482 | 28 6D 60 E0 D0 BF EC 44 6A BA 65 B1 AE C8 C7 A8
483 | 4A D7 71 40 EA EC 91 C4 C8 07 0B 73 E1 4D BC 7E
484 | AD BA BF 44 C5 1B 68 9F 87 A0 65 6D F9 CF 36 D2
485 | 27 DD A1 A8 24 15 1D 36 55 A9 FC 56 BF 6A EB B0
486 | 67 EB 31 CD 0D 3F C3 36 B4 1B B6 92 14 AA A5 FF
487 | 46 0D A9 E6 8E 85 ED B5 4E DE E3 89 1B D8 54 36
488 | 05 1B,
489 | transports: [
490 | "usb",
491 | "nfc",
492 | "ble",
493 | ],
494 | },
495 | ],
496 | authenticatorSelection: {
497 | authenticatorAttachment: "platform",
498 | requireResidentKey: true,
499 | userVerification: "required",
500 | },
501 | attestation: "direct",
502 | extensions: {
503 | },
504 | }`
505 | );
506 | });
507 | });
508 | });
509 |
--------------------------------------------------------------------------------
/test/common/credential-assertion-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("CredentialAssertion", function() {
4 | const {
5 | CredentialAssertion,
6 | Msg
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(CredentialAssertion);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new CredentialAssertion();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var msg = CredentialAssertion.from(fido2Helpers.server.assertionResponseMsgB64Url);
20 |
21 | var outputObj = msg.toObject();
22 |
23 | assert.deepEqual(outputObj, fido2Helpers.server.assertionResponseMsgB64Url);
24 | });
25 |
26 | describe("validation", function() {
27 | var testArgs;
28 | beforeEach(function() {
29 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.assertionResponseMsgB64Url);
30 | });
31 |
32 | it("allows basic data", function() {
33 | var msg = CredentialAssertion.from(testArgs);
34 | msg.validate();
35 | });
36 |
37 | it("throws on missing rawId", function() {
38 | delete testArgs.rawId;
39 | var msg = CredentialAssertion.from(testArgs);
40 |
41 | assert.throws(() => {
42 | msg.validate();
43 | }, Error, "expected 'rawId' to be 'string', got: undefined");
44 | });
45 |
46 | it("throws on empty rawId", function() {
47 | testArgs.rawId = "";
48 | var msg = CredentialAssertion.from(testArgs);
49 |
50 | assert.throws(() => {
51 | msg.validate();
52 | }, Error, "expected 'rawId' to be base64url format, got:");
53 | });
54 |
55 | it("throws on wrong type rawId", function() {
56 | testArgs.rawId = 42;
57 | var msg = CredentialAssertion.from(testArgs);
58 |
59 | assert.throws(() => {
60 | msg.validate();
61 | }, Error, "expected 'rawId' to be 'string', got: number");
62 | });
63 |
64 | it("allows missing id", function() {
65 | delete testArgs.id;
66 | var msg = CredentialAssertion.from(testArgs);
67 |
68 | msg.validate();
69 | });
70 |
71 | it("throws on empty id", function() {
72 | testArgs.id = "";
73 | var msg = CredentialAssertion.from(testArgs);
74 |
75 | assert.throws(() => {
76 | msg.validate();
77 | }, Error, "expected 'id' to be base64url format, got:");
78 | });
79 |
80 | it("throws on wrong type id", function() {
81 | testArgs.id = 42;
82 | var msg = CredentialAssertion.from(testArgs);
83 |
84 | assert.throws(() => {
85 | msg.validate();
86 | }, Error, "expected 'id' to be 'string', got: number");
87 | });
88 |
89 |
90 | it("throws on missing response", function() {
91 | delete testArgs.response;
92 | var msg = CredentialAssertion.from(testArgs);
93 |
94 | assert.throws(() => {
95 | msg.validate();
96 | }, Error, "expected 'response' to be 'Object', got: undefined");
97 | });
98 |
99 | it("throws on wrong type response", function() {
100 | testArgs.response = "beer";
101 | var msg = CredentialAssertion.from(testArgs);
102 |
103 | assert.throws(() => {
104 | msg.validate();
105 | }, Error, "expected 'response' to be 'Object', got: beer");
106 | });
107 |
108 | it("throws on missing authenticatorData", function() {
109 | delete testArgs.response.authenticatorData;
110 | var msg = CredentialAssertion.from(testArgs);
111 |
112 | assert.throws(() => {
113 | msg.validate();
114 | }, Error, "expected 'authenticatorData' to be 'string', got: undefined");
115 | });
116 |
117 | it("throws on emtpy authenticatorData", function() {
118 | testArgs.response.authenticatorData = "";
119 | var msg = CredentialAssertion.from(testArgs);
120 |
121 | assert.throws(() => {
122 | msg.validate();
123 | }, Error, "expected 'authenticatorData' to be base64url format, got: ");
124 | });
125 |
126 | it("throws on wrong type authenticatorData", function() {
127 | testArgs.response.authenticatorData = /foo/;
128 | var msg = CredentialAssertion.from(testArgs);
129 |
130 | assert.throws(() => {
131 | msg.validate();
132 | }, Error, "expected 'authenticatorData' to be 'string', got: object");
133 | });
134 |
135 | it("throws on missing clientDataJSON", function() {
136 | delete testArgs.response.clientDataJSON;
137 | var msg = CredentialAssertion.from(testArgs);
138 |
139 | assert.throws(() => {
140 | msg.validate();
141 | }, Error, "expected 'clientDataJSON' to be 'string', got: undefined");
142 | });
143 |
144 | it("throws on empty clientDataJSON", function() {
145 | testArgs.response.clientDataJSON = "";
146 | var msg = CredentialAssertion.from(testArgs);
147 |
148 | assert.throws(() => {
149 | msg.validate();
150 | }, Error, "expected 'clientDataJSON' to be base64url format, got: ");
151 | });
152 |
153 | it("throws on wrong type clientDataJSON", function() {
154 | testArgs.response.clientDataJSON = [];
155 | var msg = CredentialAssertion.from(testArgs);
156 |
157 | assert.throws(() => {
158 | msg.validate();
159 | }, Error, "expected 'clientDataJSON' to be 'string', got: object");
160 | });
161 |
162 | it("throws on missing signature", function() {
163 | delete testArgs.response.signature;
164 | var msg = CredentialAssertion.from(testArgs);
165 |
166 | assert.throws(() => {
167 | msg.validate();
168 | }, Error, "expected 'signature' to be 'string', got: undefined");
169 | });
170 |
171 | it("throws on empty signature", function() {
172 | testArgs.response.signature = "";
173 | var msg = CredentialAssertion.from(testArgs);
174 |
175 | assert.throws(() => {
176 | msg.validate();
177 | }, Error, "expected 'signature' to be base64url format, got: ");
178 | });
179 |
180 | it("throws on wrong type signature", function() {
181 | testArgs.response.signature = {};
182 | var msg = CredentialAssertion.from(testArgs);
183 |
184 | assert.throws(() => {
185 | msg.validate();
186 | }, Error, "expected 'signature' to be 'string', got: object");
187 | });
188 |
189 | it("passes on missing userHandle", function() {
190 | delete testArgs.response.userHandle;
191 | var msg = CredentialAssertion.from(testArgs);
192 |
193 | msg.validate();
194 | });
195 |
196 | it("passes on null userHandle", function() {
197 | testArgs.response.userHandle = null;
198 | var msg = CredentialAssertion.from(testArgs);
199 |
200 | msg.validate();
201 | });
202 |
203 | it("passes on empty userHandle", function() {
204 | testArgs.response.userHandle = "";
205 | var msg = CredentialAssertion.from(testArgs);
206 | msg.validate();
207 | });
208 |
209 | it("throws on wrong type userHandle", function() {
210 | testArgs.response.userHandle = 42;
211 | var msg = CredentialAssertion.from(testArgs);
212 |
213 | assert.throws(() => {
214 | msg.validate();
215 | }, Error, "expected 'userHandle' to be null or string");
216 | });
217 |
218 | it("throws on null getClientExtensionResults", function() {
219 | testArgs.getClientExtensionResults = null;
220 | var msg = CredentialAssertion.from(testArgs);
221 |
222 | assert.throws(() => {
223 | msg.validate();
224 | }, Error, "expected 'getClientExtensionResults' to be 'Object', got: null");
225 | });
226 |
227 | it("throws on string getClientExtensionResults", function() {
228 | testArgs.getClientExtensionResults = "foo";
229 | var msg = CredentialAssertion.from(testArgs);
230 |
231 | assert.throws(() => {
232 | msg.validate();
233 | }, Error, "expected 'getClientExtensionResults' to be 'Object', got: foo");
234 | });
235 |
236 | it("allows empty Object getClientExtensionResults", function() {
237 | testArgs.getClientExtensionResults = {};
238 | var msg = CredentialAssertion.from(testArgs);
239 |
240 | msg.validate();
241 | });
242 |
243 | it("allows complex Object getClientExtensionResults", function() {
244 | var exts = {
245 | foo: "bar",
246 | alice: {
247 | goes: {
248 | down: {
249 | the: {
250 | hole: "after the rabbit"
251 | }
252 | }
253 | }
254 | },
255 | arr: ["a", { b: "c" }, 1, 2, 3]
256 | };
257 |
258 | testArgs.getClientExtensionResults = exts;
259 | var msg = CredentialAssertion.from(testArgs);
260 | assert.deepEqual(msg.getClientExtensionResults, exts);
261 |
262 | msg.validate();
263 | });
264 | });
265 |
266 | describe("decodeBinaryProperties", function() {
267 | it("decodes correct fields", function() {
268 | var msg = CredentialAssertion.from(fido2Helpers.server.assertionResponseMsgB64Url);
269 | assert.isString(msg.rawId);
270 | assert.isString(msg.response.clientDataJSON);
271 | assert.isString(msg.response.signature);
272 | assert.isString(msg.response.authenticatorData);
273 | // assert.isNull(msg.response.userHandle);
274 | msg.decodeBinaryProperties();
275 | assert.instanceOf(msg.rawId, ArrayBuffer);
276 | assert.instanceOf(msg.response.clientDataJSON, ArrayBuffer);
277 | assert.instanceOf(msg.response.signature, ArrayBuffer);
278 | assert.instanceOf(msg.response.authenticatorData, ArrayBuffer);
279 | assert.instanceOf(msg.response.userHandle, ArrayBuffer);
280 | });
281 | });
282 |
283 | describe("encodeBinaryProperties", function() {
284 | it("encodes correct fields", function() {
285 | var msg = CredentialAssertion.from(fido2Helpers.server.assertionResponseMsgB64Url);
286 | msg.decodeBinaryProperties();
287 | assert.instanceOf(msg.rawId, ArrayBuffer);
288 | assert.instanceOf(msg.response.clientDataJSON, ArrayBuffer);
289 | assert.instanceOf(msg.response.signature, ArrayBuffer);
290 | assert.instanceOf(msg.response.authenticatorData, ArrayBuffer);
291 | assert.instanceOf(msg.response.userHandle, ArrayBuffer);
292 | msg.encodeBinaryProperties();
293 | assert.isString(msg.rawId);
294 | assert.isString(msg.response.clientDataJSON);
295 | assert.isString(msg.response.signature);
296 | assert.isString(msg.response.authenticatorData);
297 | assert.isNull(msg.response.userHandle);
298 | });
299 | });
300 |
301 | describe("toHumanString", function() {
302 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
303 | it("creates correct string", function() {
304 | var msg = CredentialAssertion.from(fido2Helpers.server.assertionResponseMsgB64Url);
305 | var str = msg.toHumanString();
306 | assert.isString(str);
307 | assert.strictEqual(
308 | str,
309 | // eslint-disable-next-line
310 | `[CredentialAssertion] {
311 | rawId: [ArrayBuffer] (162 bytes)
312 | 00 08 47 ED C9 CF 44 19 1C BA 48 E7 73 61 B6 18
313 | CD 47 E5 D9 15 B3 D3 F5 AB 65 44 AE 10 F9 EE 99
314 | 33 29 58 C1 6E 2C 5D B2 E7 E3 5E 15 0E 7E 20 F6
315 | EC 3D 15 03 E7 CF 29 45 58 34 61 36 5D 87 23 86
316 | 28 6D 60 E0 D0 BF EC 44 6A BA 65 B1 AE C8 C7 A8
317 | 4A D7 71 40 EA EC 91 C4 C8 07 0B 73 E1 4D BC 7E
318 | AD BA BF 44 C5 1B 68 9F 87 A0 65 6D F9 CF 36 D2
319 | 27 DD A1 A8 24 15 1D 36 55 A9 FC 56 BF 6A EB B0
320 | 67 EB 31 CD 0D 3F C3 36 B4 1B B6 92 14 AA A5 FF
321 | 46 0D A9 E6 8E 85 ED B5 4E DE E3 89 1B D8 54 36
322 | 05 1B,
323 | id: [ArrayBuffer] (162 bytes)
324 | 00 08 47 ED C9 CF 44 19 1C BA 48 E7 73 61 B6 18
325 | CD 47 E5 D9 15 B3 D3 F5 AB 65 44 AE 10 F9 EE 99
326 | 33 29 58 C1 6E 2C 5D B2 E7 E3 5E 15 0E 7E 20 F6
327 | EC 3D 15 03 E7 CF 29 45 58 34 61 36 5D 87 23 86
328 | 28 6D 60 E0 D0 BF EC 44 6A BA 65 B1 AE C8 C7 A8
329 | 4A D7 71 40 EA EC 91 C4 C8 07 0B 73 E1 4D BC 7E
330 | AD BA BF 44 C5 1B 68 9F 87 A0 65 6D F9 CF 36 D2
331 | 27 DD A1 A8 24 15 1D 36 55 A9 FC 56 BF 6A EB B0
332 | 67 EB 31 CD 0D 3F C3 36 B4 1B B6 92 14 AA A5 FF
333 | 46 0D A9 E6 8E 85 ED B5 4E DE E3 89 1B D8 54 36
334 | 05 1B,
335 | response: {
336 | clientDataJSON: [ArrayBuffer] (206 bytes)
337 | 7B 22 63 68 61 6C 6C 65 6E 67 65 22 3A 22 65 61
338 | 54 79 55 4E 6E 79 50 44 44 64 4B 38 53 4E 45 67
339 | 54 45 55 76 7A 31 51 38 64 79 6C 6B 6A 6A 54 69
340 | 6D 59 64 35 58 37 51 41 6F 2D 46 38 5F 5A 31 6C
341 | 73 4A 69 33 42 69 6C 55 70 46 5A 48 6B 49 43 4E
342 | 44 57 59 38 72 39 69 76 6E 54 67 57 37 2D 58 5A
343 | 43 33 71 51 22 2C 22 63 6C 69 65 6E 74 45 78 74
344 | 65 6E 73 69 6F 6E 73 22 3A 7B 7D 2C 22 68 61 73
345 | 68 41 6C 67 6F 72 69 74 68 6D 22 3A 22 53 48 41
346 | 2D 32 35 36 22 2C 22 6F 72 69 67 69 6E 22 3A 22
347 | 68 74 74 70 73 3A 2F 2F 6C 6F 63 61 6C 68 6F 73
348 | 74 3A 38 34 34 33 22 2C 22 74 79 70 65 22 3A 22
349 | 77 65 62 61 75 74 68 6E 2E 67 65 74 22 7D,
350 | authenticatorData: [ArrayBuffer] (37 bytes)
351 | 49 96 0D E5 88 0E 8C 68 74 34 17 0F 64 76 60 5B
352 | 8F E4 AE B9 A2 86 32 C7 99 5C F3 BA 83 1D 97 63
353 | 01 00 00 01 6B,
354 | signature: [ArrayBuffer] (72 bytes)
355 | 30 46 02 21 00 FA 74 5D C1 D1 9A 1A 2C 0D 2B EF
356 | CA 32 45 DA 0C 35 1D 1B 37 DD D9 8B 87 05 FF BE
357 | 61 14 01 FA A5 02 21 00 B6 34 50 8B 2B 87 4D EE
358 | FD FE 32 28 EC 33 C0 3E 82 8F 7F C6 58 B2 62 8A
359 | 84 D3 F7 9F 34 B3 56 BB,
360 | userHandle: [ArrayBuffer] (0 bytes),
361 | },
362 | }`
363 | );
364 | });
365 | });
366 | });
367 |
--------------------------------------------------------------------------------
/test/common/credential-attestation-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("CredentialAttestation", function() {
4 | const {
5 | CredentialAttestation,
6 | Msg
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(CredentialAttestation);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new CredentialAttestation();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var msg = CredentialAttestation.from(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
20 |
21 | var outputObj = msg.toObject();
22 |
23 | assert.deepEqual(outputObj, fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
24 | });
25 |
26 | describe("validation", function() {
27 | var testArgs;
28 | beforeEach(function() {
29 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
30 | });
31 |
32 | it("passes with default args", function() {
33 | var msg = CredentialAttestation.from(testArgs);
34 | msg.validate();
35 | });
36 |
37 | it("throws on missing rawId", function() {
38 | delete testArgs.rawId;
39 | var msg = CredentialAttestation.from(testArgs);
40 |
41 | assert.throws(() => {
42 | msg.validate();
43 | }, Error, "expected 'rawId' to be 'string', got: undefined");
44 | });
45 |
46 | it("throws on empty id", function() {
47 | testArgs.id = "";
48 | var msg = CredentialAttestation.from(testArgs);
49 |
50 | assert.throws(() => {
51 | msg.validate();
52 | }, Error, "expected 'id' to be base64url format, got: ");
53 | });
54 |
55 | it("throws on non-base64url id", function() {
56 | testArgs.id = "beer!";
57 | var msg = CredentialAttestation.from(testArgs);
58 |
59 | assert.throws(() => {
60 | msg.validate();
61 | }, Error, "expected 'id' to be base64url format, got: ");
62 | });
63 |
64 | it("throws on base64 id", function() {
65 | testArgs.id = "Bo+VjHOkJZy8DjnCJnIc0Oxt9QAz5upMdSJxNbd+GyAo6MNIvPBb9YsUlE0ZJaaWXtWH5FQyPS6bT/e698IirQ==";
66 | var msg = CredentialAttestation.from(testArgs);
67 |
68 | assert.throws(() => {
69 | msg.validate();
70 | }, Error, "expected 'id' to be base64url format, got: ");
71 | });
72 |
73 | it("throws on wrong type id", function() {
74 | testArgs.id = 42;
75 | var msg = CredentialAttestation.from(testArgs);
76 |
77 | assert.throws(() => {
78 | msg.validate();
79 | }, Error, "expected 'id' to be 'string', got: number");
80 | });
81 |
82 | it("allows on missing id", function() {
83 | delete testArgs.id;
84 | var msg = CredentialAttestation.from(testArgs);
85 |
86 | msg.validate();
87 | });
88 |
89 | it("throws on empty rawId", function() {
90 | testArgs.rawId = "";
91 | var msg = CredentialAttestation.from(testArgs);
92 |
93 | assert.throws(() => {
94 | msg.validate();
95 | }, Error, "expected 'rawId' to be base64url format, got: ");
96 | });
97 |
98 | it("throws on non-base64url rawId", function() {
99 | testArgs.rawId = "beer!";
100 | var msg = CredentialAttestation.from(testArgs);
101 |
102 | assert.throws(() => {
103 | msg.validate();
104 | }, Error, "expected 'rawId' to be base64url format, got: ");
105 | });
106 |
107 | it("throws on base64 rawId", function() {
108 | testArgs.rawId = "Bo+VjHOkJZy8DjnCJnIc0Oxt9QAz5upMdSJxNbd+GyAo6MNIvPBb9YsUlE0ZJaaWXtWH5FQyPS6bT/e698IirQ==";
109 | var msg = CredentialAttestation.from(testArgs);
110 |
111 | assert.throws(() => {
112 | msg.validate();
113 | }, Error, "expected 'rawId' to be base64url format, got: ");
114 | });
115 |
116 | it("throws on wrong type rawId", function() {
117 | testArgs.rawId = 42;
118 | var msg = CredentialAttestation.from(testArgs);
119 |
120 | assert.throws(() => {
121 | msg.validate();
122 | }, Error, "expected 'rawId' to be 'string', got: number");
123 | });
124 |
125 | it("throws on missing response", function() {
126 | delete testArgs.response;
127 | var msg = CredentialAttestation.from(testArgs);
128 |
129 | assert.throws(() => {
130 | msg.validate();
131 | }, Error, "expected 'response' to be 'Object', got: undefined");
132 | });
133 |
134 | it("throws on wrong type response", function() {
135 | testArgs.response = "beer";
136 | var msg = CredentialAttestation.from(testArgs);
137 |
138 | assert.throws(() => {
139 | msg.validate();
140 | }, Error, "expected 'response' to be 'Object', got: beer");
141 | });
142 |
143 | it("throws on missing response.attestationObject", function() {
144 | delete testArgs.response.attestationObject;
145 | var msg = CredentialAttestation.from(testArgs);
146 |
147 | assert.throws(() => {
148 | msg.validate();
149 | }, Error, "expected 'attestationObject' to be 'string', got: undefined");
150 | });
151 |
152 | it("throws on wrong type response.attestationObject", function() {
153 | testArgs.response.attestationObject = 42;
154 | var msg = CredentialAttestation.from(testArgs);
155 |
156 | assert.throws(() => {
157 | msg.validate();
158 | }, Error, "expected 'attestationObject' to be 'string', got: number");
159 | });
160 |
161 | it("throws on empty response.attestationObject", function() {
162 | testArgs.response.attestationObject = "";
163 | var msg = CredentialAttestation.from(testArgs);
164 |
165 | assert.throws(() => {
166 | msg.validate();
167 | }, Error, "expected 'attestationObject' to be base64url format, got: ");
168 | });
169 |
170 | it("throws on non-base64url response.attestationObject", function() {
171 | testArgs.response.attestationObject = "beer!";
172 | var msg = CredentialAttestation.from(testArgs);
173 |
174 | assert.throws(() => {
175 | msg.validate();
176 | }, Error, "expected 'attestationObject' to be base64url format, got: ");
177 | });
178 |
179 | it("throws on base64 response.attestationObject", function() {
180 | testArgs.response.attestationObject = "Bo+VjHOkJZy8DjnCJnIc0Oxt9QAz5upMdSJxNbd+GyAo6MNIvPBb9YsUlE0ZJaaWXtWH5FQyPS6bT/e698IirQ==";
181 | var msg = CredentialAttestation.from(testArgs);
182 |
183 | assert.throws(() => {
184 | msg.validate();
185 | }, Error, "expected 'attestationObject' to be base64url format, got: ");
186 | });
187 |
188 | it("throws on missing response.clientDataJSON", function() {
189 | delete testArgs.response.clientDataJSON;
190 | var msg = CredentialAttestation.from(testArgs);
191 |
192 | assert.throws(() => {
193 | msg.validate();
194 | }, Error, "expected 'clientDataJSON' to be 'string', got: undefined");
195 | });
196 |
197 | it("throws on wrong type response.clientDataJSON", function() {
198 | testArgs.response.clientDataJSON = 42;
199 | var msg = CredentialAttestation.from(testArgs);
200 |
201 | assert.throws(() => {
202 | msg.validate();
203 | }, Error, "expected 'clientDataJSON' to be 'string', got: number");
204 | });
205 |
206 | it("throws on empty response.clientDataJSON", function() {
207 | testArgs.response.clientDataJSON = "";
208 | var msg = CredentialAttestation.from(testArgs);
209 |
210 | assert.throws(() => {
211 | msg.validate();
212 | }, Error, "expected 'clientDataJSON' to be base64url format, got: ");
213 | });
214 |
215 | it("throws on non-base64url response.clientDataJSON", function() {
216 | testArgs.response.clientDataJSON = "beer!";
217 | var msg = CredentialAttestation.from(testArgs);
218 |
219 | assert.throws(() => {
220 | msg.validate();
221 | }, Error, "expected 'clientDataJSON' to be base64url format, got: ");
222 | });
223 |
224 | it("throws on base64 response.clientDataJSON", function() {
225 | testArgs.response.clientDataJSON = "Bo+VjHOkJZy8DjnCJnIc0Oxt9QAz5upMdSJxNbd+GyAo6MNIvPBb9YsUlE0ZJaaWXtWH5FQyPS6bT/e698IirQ==";
226 | var msg = CredentialAttestation.from(testArgs);
227 |
228 | assert.throws(() => {
229 | msg.validate();
230 | }, Error, "expected 'clientDataJSON' to be base64url format, got: ");
231 | });
232 |
233 | it("throws on null getClientExtensionResults", function() {
234 | testArgs.getClientExtensionResults = null;
235 | var msg = CredentialAttestation.from(testArgs);
236 |
237 | assert.throws(() => {
238 | msg.validate();
239 | }, Error, "expected 'getClientExtensionResults' to be 'Object', got: null");
240 | });
241 |
242 | it("throws on string getClientExtensionResults", function() {
243 | testArgs.getClientExtensionResults = "foo";
244 | var msg = CredentialAttestation.from(testArgs);
245 |
246 | assert.throws(() => {
247 | msg.validate();
248 | }, Error, "expected 'getClientExtensionResults' to be 'Object', got: foo");
249 | });
250 |
251 | it("allows empty Object getClientExtensionResults", function() {
252 | testArgs.getClientExtensionResults = {};
253 | var msg = CredentialAttestation.from(testArgs);
254 |
255 | msg.validate();
256 | });
257 |
258 | it("allows complex Object getClientExtensionResults", function() {
259 | var exts = {
260 | foo: "bar",
261 | alice: {
262 | goes: {
263 | down: {
264 | the: {
265 | hole: "after the rabbit"
266 | }
267 | }
268 | }
269 | },
270 | arr: ["a", { b: "c" }, 1, 2, 3]
271 | };
272 |
273 | testArgs.getClientExtensionResults = exts;
274 | var msg = CredentialAttestation.from(testArgs);
275 | assert.deepEqual(msg.getClientExtensionResults, exts);
276 |
277 | msg.validate();
278 | });
279 |
280 | describe("decodeBinaryProperties", function() {
281 | it("decodes correct fields", function() {
282 | var msg = CredentialAttestation.from(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
283 | assert.isString(msg.rawId);
284 | assert.isString(msg.id);
285 | assert.isString(msg.response.attestationObject);
286 | assert.isString(msg.response.clientDataJSON);
287 | msg.decodeBinaryProperties();
288 | assert.instanceOf(msg.rawId, ArrayBuffer);
289 | assert.instanceOf(msg.id, ArrayBuffer);
290 | assert.instanceOf(msg.response.attestationObject, ArrayBuffer);
291 | assert.instanceOf(msg.response.clientDataJSON, ArrayBuffer);
292 | });
293 | });
294 |
295 | describe("encodeBinaryProperties", function() {
296 | it("encodes correct fields", function() {
297 | var msg = CredentialAttestation.from(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
298 | msg.decodeBinaryProperties();
299 | assert.instanceOf(msg.rawId, ArrayBuffer);
300 | assert.instanceOf(msg.id, ArrayBuffer);
301 | assert.instanceOf(msg.response.attestationObject, ArrayBuffer);
302 | assert.instanceOf(msg.response.clientDataJSON, ArrayBuffer);
303 | msg.encodeBinaryProperties();
304 | assert.isString(msg.rawId);
305 | assert.isString(msg.id);
306 | assert.isString(msg.response.attestationObject);
307 | assert.isString(msg.response.clientDataJSON);
308 | });
309 | });
310 |
311 | describe("toHumanString", function() {
312 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
313 | it("creates correct string", function() {
314 | var msg = CredentialAttestation.from(testArgs);
315 | var str = msg.toHumanString();
316 | assert.isString(str);
317 | assert.strictEqual(
318 | str,
319 | // eslint-disable-next-line
320 | `[CredentialAttestation] {
321 | rawId: [ArrayBuffer] (162 bytes)
322 | 00 08 A2 DD 5E AC 1A 86 A8 CD 6E D3 6C D6 98 94
323 | 96 89 E5 BA FC 4E B0 5F 45 79 E8 7D 93 BA 97 6B
324 | 2E 73 76 B9 B6 DF D7 16 E1 64 14 0F F9 79 A6 D4
325 | F3 44 B5 3D 6D 26 E0 86 7B F4 14 B6 91 03 BB 65
326 | CB B2 DA F7 F4 11 28 35 F0 64 CB 1B 59 A8 E5 84
327 | A4 21 DA 8B D8 9E 38 7A 0B 7E EA B7 23 EC D7 9D
328 | 48 4C 31 6B FB AE C5 46 01 B4 73 67 49 0A 83 9A
329 | DA 14 01 F3 3D 2D 25 8B 97 AE 41 8C A5 59 34 65
330 | 29 F5 AA 37 DE 63 12 75 57 D0 43 46 C7 CD EE BD
331 | 25 54 2F 2C 17 FC 39 38 99 52 A2 6C 3A E2 A6 A6
332 | A5 1C,
333 | id: [ArrayBuffer] (162 bytes)
334 | 00 08 A2 DD 5E AC 1A 86 A8 CD 6E D3 6C D6 98 94
335 | 96 89 E5 BA FC 4E B0 5F 45 79 E8 7D 93 BA 97 6B
336 | 2E 73 76 B9 B6 DF D7 16 E1 64 14 0F F9 79 A6 D4
337 | F3 44 B5 3D 6D 26 E0 86 7B F4 14 B6 91 03 BB 65
338 | CB B2 DA F7 F4 11 28 35 F0 64 CB 1B 59 A8 E5 84
339 | A4 21 DA 8B D8 9E 38 7A 0B 7E EA B7 23 EC D7 9D
340 | 48 4C 31 6B FB AE C5 46 01 B4 73 67 49 0A 83 9A
341 | DA 14 01 F3 3D 2D 25 8B 97 AE 41 8C A5 59 34 65
342 | 29 F5 AA 37 DE 63 12 75 57 D0 43 46 C7 CD EE BD
343 | 25 54 2F 2C 17 FC 39 38 99 52 A2 6C 3A E2 A6 A6
344 | A5 1C,
345 | response: {
346 | clientDataJSON: [ArrayBuffer] (209 bytes)
347 | 7B 22 63 68 61 6C 6C 65 6E 67 65 22 3A 22 33 33
348 | 45 48 61 76 2D 6A 5A 31 76 39 71 77 48 37 38 33
349 | 61 55 2D 6A 30 41 52 78 36 72 35 6F 2D 59 48 68
350 | 2D 77 64 37 43 36 6A 50 62 64 37 57 68 36 79 74
351 | 62 49 5A 6F 73 49 49 41 43 65 68 77 66 39 2D 73
352 | 36 68 58 68 79 53 48 4F 2D 48 48 55 6A 45 77 5A
353 | 53 32 39 77 22 2C 22 63 6C 69 65 6E 74 45 78 74
354 | 65 6E 73 69 6F 6E 73 22 3A 7B 7D 2C 22 68 61 73
355 | 68 41 6C 67 6F 72 69 74 68 6D 22 3A 22 53 48 41
356 | 2D 32 35 36 22 2C 22 6F 72 69 67 69 6E 22 3A 22
357 | 68 74 74 70 73 3A 2F 2F 6C 6F 63 61 6C 68 6F 73
358 | 74 3A 38 34 34 33 22 2C 22 74 79 70 65 22 3A 22
359 | 77 65 62 61 75 74 68 6E 2E 63 72 65 61 74 65 22
360 | 7D,
361 | attestationObject: [ArrayBuffer] (325 bytes)
362 | A3 63 66 6D 74 64 6E 6F 6E 65 67 61 74 74 53 74
363 | 6D 74 A0 68 61 75 74 68 44 61 74 61 59 01 26 49
364 | 96 0D E5 88 0E 8C 68 74 34 17 0F 64 76 60 5B 8F
365 | E4 AE B9 A2 86 32 C7 99 5C F3 BA 83 1D 97 63 41
366 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
367 | 00 00 00 00 00 A2 00 08 A2 DD 5E AC 1A 86 A8 CD
368 | 6E D3 6C D6 98 94 96 89 E5 BA FC 4E B0 5F 45 79
369 | E8 7D 93 BA 97 6B 2E 73 76 B9 B6 DF D7 16 E1 64
370 | 14 0F F9 79 A6 D4 F3 44 B5 3D 6D 26 E0 86 7B F4
371 | 14 B6 91 03 BB 65 CB B2 DA F7 F4 11 28 35 F0 64
372 | CB 1B 59 A8 E5 84 A4 21 DA 8B D8 9E 38 7A 0B 7E
373 | EA B7 23 EC D7 9D 48 4C 31 6B FB AE C5 46 01 B4
374 | 73 67 49 0A 83 9A DA 14 01 F3 3D 2D 25 8B 97 AE
375 | 41 8C A5 59 34 65 29 F5 AA 37 DE 63 12 75 57 D0
376 | 43 46 C7 CD EE BD 25 54 2F 2C 17 FC 39 38 99 52
377 | A2 6C 3A E2 A6 A6 A5 1C A5 01 02 03 26 20 01 21
378 | 58 20 BB 11 CD DD 6E 9E 86 9D 15 59 72 9A 30 D8
379 | 9E D4 9F 36 31 52 42 15 96 12 71 AB BB E2 8D 7B
380 | 73 1F 22 58 20 DB D6 39 13 2E 2E E5 61 96 5B 83
381 | 05 30 A6 A0 24 F1 09 88 88 F3 13 55 05 15 92 11
382 | 84 C8 6A CA C3,
383 | },
384 | }`
385 | );
386 | });
387 | });
388 | });
389 | });
390 |
--------------------------------------------------------------------------------
/test/common/get-options-request-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("GetOptionsRequest", function() {
4 | const {
5 | GetOptionsRequest,
6 | Msg
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(GetOptionsRequest);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new GetOptionsRequest();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var inputObj = {
20 | username: "adam",
21 | displayName: "AdamPowers"
22 | };
23 | var msg = GetOptionsRequest.from(inputObj);
24 |
25 | var outputObj = msg.toObject();
26 |
27 | assert.deepEqual(outputObj, inputObj);
28 | });
29 |
30 | describe("validate", function() {
31 | var testArgs;
32 | beforeEach(function() {
33 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.getOptionsRequest);
34 | });
35 |
36 | it("passes with basic args", function() {
37 | var msg = GetOptionsRequest.from(testArgs);
38 | msg.validate();
39 | });
40 |
41 | it("throws on missing username", function() {
42 | delete testArgs.username;
43 | var msg = GetOptionsRequest.from(testArgs);
44 |
45 | assert.throws(() => {
46 | msg.validate();
47 | }, Error, "expected 'username' to be 'string', got: undefined");
48 | });
49 |
50 | it("throws on empty username", function() {
51 | testArgs.username = "";
52 | var msg = GetOptionsRequest.from(testArgs);
53 |
54 | assert.throws(() => {
55 | msg.validate();
56 | }, Error, "expected 'username' to be non-empty string");
57 | });
58 |
59 | it("throws on missing displayName", function() {
60 | delete testArgs.displayName;
61 | var msg = GetOptionsRequest.from(testArgs);
62 |
63 | assert.throws(() => {
64 | msg.validate();
65 | }, Error, "expected 'displayName' to be 'string', got: undefined");
66 | });
67 |
68 | it("throws on empty displayName", function() {
69 | testArgs.displayName = "";
70 | var msg = GetOptionsRequest.from(testArgs);
71 |
72 | assert.throws(() => {
73 | msg.validate();
74 | }, Error, "expected 'displayName' to be non-empty string");
75 | });
76 |
77 | it("passes with extraData", function() {
78 | testArgs.extraData = "AAAA==";
79 | var msg = GetOptionsRequest.from(testArgs);
80 |
81 | msg.validate();
82 | });
83 |
84 | it("passes with undefined extraData", function() {
85 | testArgs.extraData = undefined;
86 | var msg = GetOptionsRequest.from(testArgs);
87 |
88 | msg.validate();
89 | });
90 |
91 | it("throws on non-string extraData", function() {
92 | testArgs.extraData = 42;
93 | var msg = GetOptionsRequest.from(testArgs);
94 |
95 | assert.throws(() => {
96 | msg.validate();
97 | }, Error, "expected 'extraData' to be 'string', got: number");
98 | });
99 |
100 | it("throws on non-base64url extraData", function() {
101 | testArgs.extraData = "!!!";
102 | var msg = GetOptionsRequest.from(testArgs);
103 |
104 | assert.throws(() => {
105 | msg.validate();
106 | }, Error, "expected 'extraData' to be base64url format, got: !!!");
107 | });
108 | });
109 |
110 | describe("decodeBinaryProperties", function() {
111 | it("doesn't throw", function() {
112 | var msg = GetOptionsRequest.from(fido2Helpers.server.getOptionsRequest);
113 | msg.decodeBinaryProperties();
114 | });
115 | });
116 |
117 | describe("encodeBinaryProperties", function() {
118 | it("doesn't throw", function() {
119 | var msg = GetOptionsRequest.from(fido2Helpers.server.getOptionsRequest);
120 | msg.encodeBinaryProperties();
121 | });
122 | });
123 |
124 | describe("toHumanString", function() {
125 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
126 | it("creates correct string", function() {
127 | var msg = GetOptionsRequest.from(fido2Helpers.server.getOptionsRequest);
128 | var str = msg.toHumanString();
129 | assert.isString(str);
130 | assert.strictEqual(
131 | str,
132 | // eslint-disable-next-line
133 | `[GetOptionsRequest] {
134 | username: "bubba",
135 | displayName: "Bubba Smith",
136 | }`
137 | );
138 | });
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/test/common/get-options-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("GetOptions", function() {
4 | const {
5 | GetOptions,
6 | ServerResponse
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(GetOptions);
11 | });
12 |
13 | it("is ServerResponse class", function() {
14 | var msg = new GetOptions();
15 | assert.instanceOf(msg, ServerResponse);
16 | });
17 |
18 | it("converts correctly", function() {
19 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
20 |
21 | var outputObj = msg.toObject();
22 |
23 | assert.deepEqual(outputObj, fido2Helpers.server.completeGetOptions);
24 | });
25 |
26 | describe("validate", function() {
27 | var testArgs;
28 | beforeEach(function() {
29 | testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.completeGetOptions);
30 | });
31 |
32 | it("allows basic data", function() {
33 | var msg = GetOptions.from(fido2Helpers.server.basicGetOptions);
34 | msg.validate();
35 | });
36 |
37 | it("allows complete data", function() {
38 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
39 | msg.validate();
40 | });
41 |
42 | it("throws on missing status", function() {
43 | delete testArgs.status;
44 | var msg = GetOptions.from(testArgs);
45 |
46 | assert.throws(() => {
47 | msg.validate();
48 | }, Error, "expected 'status' to be 'string', got: undefined");
49 | });
50 |
51 | it("throws on missing challenge", function() {
52 | delete testArgs.challenge;
53 | var msg = GetOptions.from(testArgs);
54 |
55 | assert.throws(() => {
56 | msg.validate();
57 | }, Error, "expected 'challenge' to be 'string', got: undefined");
58 | });
59 |
60 | it("throws on empty challenge", function() {
61 | testArgs.challenge = "";
62 | var msg = GetOptions.from(testArgs);
63 |
64 | assert.throws(() => {
65 | msg.validate();
66 | }, Error, "expected 'challenge' to be base64url format, got:");
67 | });
68 |
69 | it("throws on wrong type challenge", function() {
70 | testArgs.challenge = {};
71 | var msg = GetOptions.from(testArgs);
72 |
73 | assert.throws(() => {
74 | msg.validate();
75 | }, Error, "expected 'challenge' to be 'string', got: object");
76 | });
77 |
78 | it("throws on wrong type timeout", function() {
79 | testArgs.timeout = "beer";
80 | var msg = GetOptions.from(testArgs);
81 |
82 | assert.throws(() => {
83 | msg.validate();
84 | }, Error, "expected 'timeout' to be 'number', got: string");
85 | });
86 |
87 | it("throws on negative timeout", function() {
88 | testArgs.timeout = -1;
89 | var msg = GetOptions.from(testArgs);
90 |
91 | assert.throws(() => {
92 | msg.validate();
93 | }, Error, "expected 'timeout' to be positive integer");
94 | });
95 |
96 | it("throws on NaN timeout", function() {
97 | testArgs.timeout = NaN;
98 | var msg = GetOptions.from(testArgs);
99 |
100 | assert.throws(() => {
101 | msg.validate();
102 | }, Error, "expected 'timeout' to be positive integer");
103 | });
104 |
105 | it("throws on float timeout", function() {
106 | testArgs.timeout = 3.14159;
107 | var msg = GetOptions.from(testArgs);
108 |
109 | assert.throws(() => {
110 | msg.validate();
111 | }, Error, "expected 'timeout' to be positive integer");
112 | });
113 |
114 | it("throws on wrong type rpId", function() {
115 | testArgs.rpId = [];
116 | var msg = GetOptions.from(testArgs);
117 |
118 | assert.throws(() => {
119 | msg.validate();
120 | }, Error, "expected 'rpId' to be 'string', got: object");
121 | });
122 |
123 | it("throws on empty rpId", function() {
124 | testArgs.rpId = "";
125 | var msg = GetOptions.from(testArgs);
126 |
127 | assert.throws(() => {
128 | msg.validate();
129 | }, Error, "expected 'rpId' to be non-empty string");
130 | });
131 |
132 | it("throws on wrong type allowCredentials", function() {
133 | testArgs.allowCredentials = 42;
134 | var msg = GetOptions.from(testArgs);
135 |
136 | assert.throws(() => {
137 | msg.validate();
138 | }, Error, "expected 'allowCredentials' to be 'Array', got: 42");
139 | });
140 |
141 | it("throws on missing allowCredentials[0].type", function() {
142 | delete testArgs.allowCredentials[0].type;
143 | var msg = GetOptions.from(testArgs);
144 |
145 | assert.throws(() => {
146 | msg.validate();
147 | }, Error, "credential type must be 'public-key'");
148 | });
149 |
150 | it("throws on wrong type allowCredentials[0].type", function() {
151 | testArgs.allowCredentials[0].type = -7;
152 | var msg = GetOptions.from(testArgs);
153 |
154 | assert.throws(() => {
155 | msg.validate();
156 | }, Error, "credential type must be 'public-key'");
157 | });
158 |
159 | it("throws on missing allowCredentials[0].id", function() {
160 | delete testArgs.allowCredentials[0].id;
161 | var msg = GetOptions.from(testArgs);
162 |
163 | assert.throws(() => {
164 | msg.validate();
165 | }, Error, "expected 'id' to be 'string', got: undefined");
166 | });
167 |
168 | it("throws on wrong type allowCredentials[0].id", function() {
169 | testArgs.allowCredentials[0].id = {};
170 | var msg = GetOptions.from(testArgs);
171 |
172 | assert.throws(() => {
173 | msg.validate();
174 | }, Error, "expected 'id' to be 'string', got: object");
175 | });
176 |
177 | it("throws on wrong type allowCredentials[0].transports", function() {
178 | testArgs.allowCredentials[0].transports = "usb";
179 | var msg = GetOptions.from(testArgs);
180 |
181 | assert.throws(() => {
182 | msg.validate();
183 | }, Error, "expected 'transports' to be 'Array', got: usb");
184 | });
185 |
186 | it("throws on invalid transport", function() {
187 | testArgs.allowCredentials[0].transports = ["foo"];
188 | var msg = GetOptions.from(testArgs);
189 |
190 | assert.throws(() => {
191 | msg.validate();
192 | }, Error, "expected transport to be 'usb', 'nfc', or 'ble', got: foo");
193 | });
194 |
195 | it("throws on wrong type userVerification", function() {
196 | testArgs.userVerification = 42;
197 | var msg = GetOptions.from(testArgs);
198 |
199 | assert.throws(() => {
200 | msg.validate();
201 | }, Error, "userVerification must be 'required', 'preferred' or 'discouraged'");
202 | });
203 |
204 | it("throws on invalid userVerification", function() {
205 | testArgs.userVerification = "foo";
206 | var msg = GetOptions.from(testArgs);
207 |
208 | assert.throws(() => {
209 | msg.validate();
210 | }, Error, "userVerification must be 'required', 'preferred' or 'discouraged'");
211 | });
212 |
213 | it("throws on wrong type extensions", function() {
214 | testArgs.extensions = "foo";
215 | var msg = GetOptions.from(testArgs);
216 |
217 | assert.throws(() => {
218 | msg.validate();
219 | }, Error, "expected 'extensions' to be 'Object', got: foo");
220 | });
221 |
222 | it("passes with rawChallenge", function() {
223 | testArgs.rawChallenge = "AAAA";
224 | var msg = GetOptions.from(testArgs);
225 |
226 | msg.validate();
227 | });
228 |
229 | it("passes with undefined rawChallenge", function() {
230 | testArgs.rawChallenge = undefined;
231 | var msg = GetOptions.from(testArgs);
232 |
233 | msg.validate();
234 | });
235 |
236 | it("throws on non-string rawChallenge", function() {
237 | testArgs.rawChallenge = 42;
238 | var msg = GetOptions.from(testArgs);
239 |
240 | assert.throws(() => {
241 | msg.validate();
242 | }, Error, "expected 'rawChallenge' to be 'string', got: number");
243 | });
244 |
245 | it("throws on non-base64url rawChallenge", function() {
246 | testArgs.rawChallenge = "!!!";
247 | var msg = GetOptions.from(testArgs);
248 |
249 | assert.throws(() => {
250 | msg.validate();
251 | }, Error, "expected 'rawChallenge' to be base64url format, got: !!!");
252 | });
253 | });
254 |
255 | describe("decodeBinaryProperties", function() {
256 | it("decodes correct fields", function() {
257 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
258 | assert.isString(msg.challenge);
259 | msg.allowCredentials.forEach((cred) => {
260 | assert.isString(cred.id);
261 | });
262 | msg.decodeBinaryProperties();
263 | assert.instanceOf(msg.challenge, ArrayBuffer);
264 | msg.allowCredentials.forEach((cred) => {
265 | assert.instanceOf(cred.id, ArrayBuffer);
266 | });
267 | });
268 |
269 | it("decodes rawChallenge", function() {
270 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
271 | msg.rawChallenge = "AAAA";
272 | msg.decodeBinaryProperties();
273 | assert.instanceOf(msg.rawChallenge, ArrayBuffer);
274 | assert.strictEqual(msg.rawChallenge.byteLength, 3);
275 | });
276 | });
277 |
278 | describe("encodeBinaryProperties", function() {
279 | it("encodes correct fields", function() {
280 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
281 | msg.decodeBinaryProperties();
282 | assert.instanceOf(msg.challenge, ArrayBuffer);
283 | msg.allowCredentials.forEach((cred) => {
284 | assert.instanceOf(cred.id, ArrayBuffer);
285 | });
286 | msg.encodeBinaryProperties();
287 | assert.isString(msg.challenge);
288 | msg.allowCredentials.forEach((cred) => {
289 | assert.isString(cred.id);
290 | });
291 | });
292 |
293 | it("encodes rawChallenge", function() {
294 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
295 | msg.decodeBinaryProperties();
296 | msg.rawChallenge = new Uint8Array([0x00, 0x00, 0x00]).buffer;
297 | msg.encodeBinaryProperties();
298 | assert.strictEqual(msg.rawChallenge, "AAAA");
299 | });
300 | });
301 |
302 | describe("toHumanString", function() {
303 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
304 | it("creates correct string", function() {
305 | var msg = GetOptions.from(fido2Helpers.server.completeGetOptions);
306 | var str = msg.toHumanString();
307 | assert.isString(str);
308 | assert.strictEqual(
309 | str,
310 | // eslint-disable-next-line
311 | `[GetOptions] {
312 | status: "ok",
313 | challenge: [ArrayBuffer] (64 bytes)
314 | B0 FE 0C 8B 0A 1D 8E B7 82 F3 EF 34 20 C8 DC C9
315 | 63 65 A3 F6 35 48 95 E6 16 04 0D 06 29 67 8D D7
316 | F7 D1 64 6C 8C 50 E1 0D 89 9F 63 8F B8 BA 1A B6
317 | 1C 58 D8 44 46 D7 76 BE 95 8E EB F3 D9 7B D3 8C,
318 | timeout: 60000,
319 | rpId: "My RP",
320 | allowCredentials: [
321 | {
322 | type: "public-key",
323 | id: [ArrayBuffer] (162 bytes)
324 | 00 08 47 ED C9 CF 44 19 1C BA 48 E7 73 61 B6 18
325 | CD 47 E5 D9 15 B3 D3 F5 AB 65 44 AE 10 F9 EE 99
326 | 33 29 58 C1 6E 2C 5D B2 E7 E3 5E 15 0E 7E 20 F6
327 | EC 3D 15 03 E7 CF 29 45 58 34 61 36 5D 87 23 86
328 | 28 6D 60 E0 D0 BF EC 44 6A BA 65 B1 AE C8 C7 A8
329 | 4A D7 71 40 EA EC 91 C4 C8 07 0B 73 E1 4D BC 7E
330 | AD BA BF 44 C5 1B 68 9F 87 A0 65 6D F9 CF 36 D2
331 | 27 DD A1 A8 24 15 1D 36 55 A9 FC 56 BF 6A EB B0
332 | 67 EB 31 CD 0D 3F C3 36 B4 1B B6 92 14 AA A5 FF
333 | 46 0D A9 E6 8E 85 ED B5 4E DE E3 89 1B D8 54 36
334 | 05 1B,
335 | transports: [
336 | "usb",
337 | "nfc",
338 | "ble",
339 | ],
340 | },
341 | ],
342 | userVerification: "discouraged",
343 | extensions: {
344 | },
345 | }`
346 | );
347 | });
348 | });
349 | });
350 |
351 |
--------------------------------------------------------------------------------
/test/common/helpers-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("helpers", function() {
4 | const {
5 | WebAuthnHelpers
6 | } = GlobalWebAuthnClasses;
7 |
8 | const {
9 | coerceToBase64Url,
10 | coerceToArrayBuffer
11 | } = WebAuthnHelpers.utils;
12 |
13 | describe("defaultRoutes", function() {
14 | var defaultRoutes = WebAuthnHelpers.defaultRoutes;
15 | it("is object", function() {
16 | // assert.isObject(defaultRoutes);
17 | assert.isDefined(defaultRoutes);
18 | });
19 |
20 | it("has attestationOptions", function() {
21 | assert.isString(defaultRoutes.attestationOptions);
22 | assert.strictEqual(defaultRoutes.attestationOptions, "/attestation/options");
23 | });
24 | it("has attestationResult", function() {
25 | assert.isString(defaultRoutes.attestationResult);
26 | assert.strictEqual(defaultRoutes.attestationResult, "/attestation/result");
27 | });
28 |
29 | it("has assertionOptions", function() {
30 | assert.isString(defaultRoutes.assertionOptions);
31 | assert.strictEqual(defaultRoutes.assertionOptions, "/assertion/options");
32 | });
33 |
34 | it("has assertionResult", function() {
35 | assert.isString(defaultRoutes.assertionResult);
36 | assert.strictEqual(defaultRoutes.assertionResult, "/assertion/result");
37 | });
38 | });
39 |
40 | describe("utils", function() {
41 | describe("coerceToBase64Url", function() {
42 | it("exists", function() {
43 | assert.isFunction(coerceToBase64Url);
44 | });
45 |
46 | it("coerce ArrayBuffer to base64url", function() {
47 | var ab = Uint8Array.from([
48 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
49 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
50 | ]).buffer;
51 | var res = coerceToBase64Url(ab);
52 | assert.isString(res);
53 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
54 | });
55 |
56 | it("coerce Uint8Array to base64url", function() {
57 | var buf = Uint8Array.from([
58 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
59 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
60 | ]);
61 | var res = coerceToBase64Url(buf);
62 | assert.isString(res);
63 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
64 | });
65 |
66 | it("coerce Array to base64url", function() {
67 | var arr = [
68 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
69 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
70 | ];
71 | var res = coerceToBase64Url(arr);
72 | assert.isString(res);
73 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
74 | });
75 |
76 | it("coerce base64 to base64url", function() {
77 | var b64 = "AAECAwQFBgcJCgsMDQ4/+A==";
78 | var res = coerceToBase64Url(b64);
79 | assert.isString(res);
80 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
81 | });
82 |
83 | it("coerce base64url to base64url", function() {
84 | var b64url = "AAECAwQFBgcJCgsMDQ4_-A";
85 | var res = coerceToBase64Url(b64url);
86 | assert.isString(res);
87 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
88 | });
89 |
90 | it("throws on incompatible: number", function() {
91 | assert.throws(() => {
92 | coerceToBase64Url(42, "test.number");
93 | }, Error, "could not coerce 'test.number' to string");
94 | });
95 |
96 | it("throws on incompatible: undefined", function() {
97 | assert.throws(() => {
98 | coerceToBase64Url(undefined, "test.number");
99 | }, Error, "could not coerce 'test.number' to string");
100 | });
101 | });
102 |
103 | describe("coerceToArrayBuffer", function() {
104 | it("exists", function() {
105 | assert.isFunction(coerceToArrayBuffer);
106 | });
107 |
108 | it("coerce base64url to ArrayBuffer", function() {
109 | var b64url = "AAECAwQFBgcJCgsMDQ4_-A";
110 | var res = coerceToArrayBuffer(b64url);
111 | assert.instanceOf(res, ArrayBuffer);
112 | var expectedAb = Uint8Array.from([
113 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
114 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
115 | ]).buffer;
116 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
117 | });
118 |
119 | it("coerce base64 to ArrayBuffer", function() {
120 | var b64 = "AAECAwQFBgcJCgsMDQ4/+A==";
121 | var res = coerceToArrayBuffer(b64);
122 | assert.instanceOf(res, ArrayBuffer);
123 | var expectedAb = Uint8Array.from([
124 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
125 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
126 | ]).buffer;
127 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
128 | });
129 |
130 | it("coerce Array to ArrayBuffer", function() {
131 | var arr = [
132 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
133 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
134 | ];
135 | var res = coerceToArrayBuffer(arr);
136 | assert.instanceOf(res, ArrayBuffer);
137 | var expectedAb = Uint8Array.from([
138 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
139 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
140 | ]).buffer;
141 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
142 | });
143 |
144 | it("coerce Uint8Array to ArrayBuffer", function() {
145 | var buf = Uint8Array.from([
146 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
147 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
148 | ]);
149 | var res = coerceToArrayBuffer(buf);
150 | assert.instanceOf(res, ArrayBuffer);
151 | var expectedAb = Uint8Array.from([
152 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
153 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
154 | ]).buffer;
155 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
156 | });
157 |
158 | it("coerce ArrayBuffer to ArrayBuffer", function() {
159 | var ab = Uint8Array.from([
160 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
161 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
162 | ]).buffer;
163 | var res = coerceToArrayBuffer(ab);
164 | assert.instanceOf(res, ArrayBuffer);
165 | var expectedAb = Uint8Array.from([
166 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
167 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
168 | ]).buffer;
169 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
170 | });
171 |
172 | it("throws on incompatible: number", function() {
173 | assert.throws(() => {
174 | coerceToArrayBuffer(42, "test.number");
175 | }, Error, "could not coerce 'test.number' to ArrayBuffer");
176 | });
177 |
178 | it("throws on incompatible: undefined", function() {
179 | assert.throws(() => {
180 | coerceToArrayBuffer(undefined, "test.number");
181 | }, Error, "could not coerce 'test.number' to ArrayBuffer");
182 | });
183 |
184 | it("throws on incompatible: object", function() {
185 | assert.throws(() => {
186 | coerceToArrayBuffer({}, "test.number");
187 | }, Error, "could not coerce 'test.number' to ArrayBuffer");
188 | });
189 | });
190 | });
191 | });
192 |
--------------------------------------------------------------------------------
/test/common/index-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("index", function() {
4 | it("imported", function() {
5 | assert.isDefined(GlobalWebAuthnClasses);
6 | });
7 |
8 | it("has CreateOptions", function() {
9 | assert.isFunction(GlobalWebAuthnClasses.CreateOptions);
10 | });
11 |
12 | it("has CreateOptionsRequest", function() {
13 | assert.isFunction(GlobalWebAuthnClasses.CreateOptionsRequest);
14 | });
15 |
16 | it("has CredentialAssertion", function() {
17 | assert.isFunction(GlobalWebAuthnClasses.CredentialAssertion);
18 | });
19 |
20 | it("has CredentialAttestation", function() {
21 | assert.isFunction(GlobalWebAuthnClasses.CredentialAttestation);
22 | });
23 |
24 | it("has GetOptions", function() {
25 | assert.isFunction(GlobalWebAuthnClasses.GetOptions);
26 | });
27 |
28 | it("has GetOptionsRequest", function() {
29 | assert.isFunction(GlobalWebAuthnClasses.GetOptionsRequest);
30 | });
31 |
32 | it("has Msg", function() {
33 | assert.isFunction(GlobalWebAuthnClasses.Msg);
34 | });
35 |
36 | it("has ServerResponse", function() {
37 | assert.isFunction(GlobalWebAuthnClasses.ServerResponse);
38 | });
39 |
40 | it("has WebAuthnHelpers", function() {
41 | assert.isObject(GlobalWebAuthnClasses.WebAuthnHelpers);
42 | });
43 |
44 | describe("WebAuthnHelpers", function() {
45 | it("has utils", function() {
46 | // XXX isObject fails
47 | assert.isDefined(GlobalWebAuthnClasses.WebAuthnHelpers.utils);
48 | });
49 |
50 | describe("utils", function() {
51 | it("has coerceToBase64Url", function() {
52 | assert.isFunction(GlobalWebAuthnClasses.WebAuthnHelpers.utils.coerceToBase64Url);
53 | });
54 |
55 | it("has coerceToArrayBuffer", function() {
56 | assert.isFunction(GlobalWebAuthnClasses.WebAuthnHelpers.utils.coerceToArrayBuffer);
57 | });
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/test/common/msg-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("Msg", function() {
4 | const { Msg } = GlobalWebAuthnClasses;
5 |
6 | class TestClass extends Msg {
7 | constructor() {
8 | super();
9 |
10 | this.propList = ["username", "displayName"];
11 | }
12 | }
13 |
14 | describe("from", function() {
15 |
16 | it("accepts object", function() {
17 | var msg = TestClass.from({
18 | username: "adam",
19 | displayName: "Adam Powers"
20 | });
21 |
22 | assert.instanceOf(msg, Msg);
23 | assert.strictEqual(msg.username, "adam");
24 | assert.strictEqual(msg.displayName, "Adam Powers");
25 | });
26 |
27 | it("accepts string", function() {
28 | var json = JSON.stringify({
29 | username: "adam",
30 | displayName: "Adam Powers"
31 | });
32 |
33 | var msg = TestClass.from(json);
34 | assert.instanceOf(msg, Msg);
35 | assert.strictEqual(msg.username, "adam");
36 | assert.strictEqual(msg.displayName, "Adam Powers");
37 | });
38 |
39 | it("throws on no arguments", function() {
40 | assert.throws(() => {
41 | TestClass.from();
42 | }, TypeError, "could not coerce 'json' argument to an object");
43 | });
44 |
45 | it("throws on bad string", function() {
46 | assert.throws(() => {
47 | TestClass.from("this is a bad string");
48 | }, TypeError, "error parsing JSON string");
49 | });
50 |
51 | it("accepts empty object", function() {
52 | var msg = TestClass.from({});
53 | msg.propList = ["username", "displayName"];
54 |
55 | assert.instanceOf(msg, Msg);
56 | assert.isUndefined(msg.username);
57 | assert.isUndefined(msg.displayName);
58 | });
59 | });
60 |
61 | describe("toObject", function() {
62 | it("converts to object", function() {
63 | var msg = TestClass.from({
64 | username: "adam",
65 | displayName: "Adam Powers"
66 | });
67 |
68 | var obj = msg.toObject();
69 | assert.notInstanceOf(obj, Msg);
70 | assert.strictEqual(obj.username, "adam");
71 | assert.strictEqual(obj.displayName, "Adam Powers");
72 | });
73 | });
74 |
75 | describe("toString", function() {
76 | it("converts object to string", function() {
77 | var msg = TestClass.from({
78 | username: "adam",
79 | displayName: "Adam Powers"
80 | });
81 |
82 | var str = msg.toString();
83 | assert.isString(str);
84 | assert.strictEqual(str, "{\"username\":\"adam\",\"displayName\":\"Adam Powers\"}");
85 | });
86 | });
87 |
88 | describe("toHumanString", function() {
89 | it("converts object to string", function() {
90 | var msg = TestClass.from({
91 | username: "adam",
92 | displayName: "Adam Powers"
93 | });
94 |
95 | var str = msg.toHumanString();
96 | assert.isString(str);
97 | assert.strictEqual(str, "[TestClass] {\n username: \"adam\",\n displayName: \"Adam Powers\",\n}");
98 | });
99 | });
100 |
101 | describe("static toHumanString", function() {
102 | it("converts object to string", function() {
103 | var str = TestClass.toHumanString({
104 | username: "adam",
105 | displayName: "Adam Powers"
106 | });
107 |
108 | assert.isString(str);
109 | assert.strictEqual(str, "[TestClass] {\n username: \"adam\",\n displayName: \"Adam Powers\",\n}");
110 | });
111 | });
112 |
113 | describe("toHumanHtml", function() {
114 | it("converts object to string", function() {
115 | var msg = TestClass.from({
116 | username: "adam",
117 | displayName: "Adam Powers"
118 | });
119 |
120 | var str = msg.toHumanHtml();
121 | assert.isString(str);
122 | assert.strictEqual(str, "[TestClass] {
username: \"adam\",
displayName: \"Adam Powers\",
}");
123 | });
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/test/common/server-response-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe("ServerResponse", function() {
4 | const {
5 | Msg,
6 | ServerResponse
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(ServerResponse);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new ServerResponse();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | it("has right properties", function() {
19 | var msg = new ServerResponse();
20 |
21 | assert.deepEqual(msg.propList, ["status", "errorMessage", "debugInfo"]);
22 | });
23 |
24 | it("converts correctly", function() {
25 | var inputObj = {
26 | status: "ok",
27 | errorMessage: ""
28 | };
29 | var msg = ServerResponse.from(inputObj);
30 |
31 | var outputObj = msg.toObject();
32 |
33 | assert.deepEqual(outputObj, inputObj);
34 | });
35 |
36 | describe("validate", function() {
37 | it("accepts status ok", function() {
38 | var msg = ServerResponse.from({
39 | status: "ok",
40 | errorMessage: ""
41 | });
42 |
43 | msg.validate();
44 | });
45 |
46 | it("accepts status ok with no errorMessage", function() {
47 | var msg = ServerResponse.from({
48 | status: "ok",
49 | });
50 |
51 | msg.validate();
52 | });
53 |
54 | it("accepts status failed", function() {
55 | var msg = ServerResponse.from({
56 | status: "failed",
57 | errorMessage: "out of beer"
58 | });
59 |
60 | msg.validate();
61 | });
62 |
63 | it("throws on bad status", function() {
64 | var msg = ServerResponse.from({
65 | status: "foobar",
66 | errorMessage: ""
67 | });
68 |
69 | assert.throws(() => {
70 | msg.validate();
71 | }, Error, "expected 'status' to be 'string', got: foobar");
72 | });
73 |
74 | it("throws on ok with errorMessage", function() {
75 | var msg = ServerResponse.from({
76 | status: "ok",
77 | errorMessage: "there is no error"
78 | });
79 |
80 | assert.throws(() => {
81 | msg.validate();
82 | }, Error, "errorMessage must be empty string when status is 'ok'");
83 | });
84 |
85 | it("throws on failed with empty errorMessage", function() {
86 | var msg = ServerResponse.from({
87 | status: "failed",
88 | errorMessage: ""
89 | });
90 |
91 | assert.throws(() => {
92 | msg.validate();
93 | }, Error, "errorMessage must be non-zero length when status is 'failed'");
94 | });
95 |
96 | it("throws on failed without errorMessage", function() {
97 | var msg = ServerResponse.from({
98 | status: "failed",
99 | });
100 |
101 | assert.throws(() => {
102 | msg.validate();
103 | }, Error, "expected 'errorMessage' to be 'string', got: undefined");
104 | });
105 | });
106 |
107 | describe("decodeBinaryProperties", function() {
108 | it("doesn't throw", function() {
109 | var msg = ServerResponse.from({
110 | status: "failed",
111 | });
112 | msg.decodeBinaryProperties();
113 | });
114 | });
115 |
116 | describe("encodeBinaryProperties", function() {
117 | it("doesn't throw", function() {
118 | var msg = ServerResponse.from({
119 | status: "failed",
120 | });
121 | msg.encodeBinaryProperties();
122 | });
123 | });
124 |
125 | describe("attestation debugInfo", function() {
126 | var debugInfo;
127 | beforeEach(function() {
128 | debugInfo =
129 | {
130 | clientData: {
131 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
132 | origin: "https://localhost:8443",
133 | type: "webauthn.create",
134 | tokenBinding: undefined,
135 | rawClientDataJson: new ArrayBuffer(),
136 | rawId: new ArrayBuffer()
137 | },
138 | authnrData: {
139 | fmt: "none",
140 | rawAuthnrData: new ArrayBuffer(),
141 | rpIdHash: new ArrayBuffer(),
142 | flags: new Set(["UP", "AT"]),
143 | counter: 0,
144 | aaguid: new ArrayBuffer(),
145 | credIdLen: 162,
146 | credId: new ArrayBuffer(),
147 | credentialPublicKeyCose: new ArrayBuffer(),
148 | credentialPublicKeyJwk: {
149 | kty: "EC",
150 | alg: "ECDSA_w_SHA256",
151 | crv: "P-256",
152 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
153 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM="
154 | },
155 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC\nFZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==\n-----END PUBLIC KEY-----\n"
156 | },
157 | audit: {
158 | validExpectations: true,
159 | validRequest: true,
160 | complete: true,
161 | warning: new Map(),
162 | info: new Map([
163 | ["yubico-device-id", "YubiKey 4/YubiKey 4 Nano"],
164 | ["fido-u2f-transports", new Set(["usb"])],
165 | ["attestation-type", "basic"],
166 | ]),
167 | }
168 | };
169 | });
170 |
171 | it("is included", function() {
172 | var msg = ServerResponse.from({
173 | status: "ok",
174 | debugInfo: debugInfo
175 | });
176 |
177 | assert.isObject(msg.debugInfo);
178 | assert.isObject(msg.debugInfo.clientData);
179 | assert.isObject(msg.debugInfo.authnrData);
180 | });
181 |
182 | it("validates", function() {
183 | var msg = ServerResponse.from({
184 | status: "ok",
185 | debugInfo: debugInfo
186 | });
187 |
188 | msg.validate();
189 | });
190 |
191 | it("encodes correctly", function() {
192 | var msg = ServerResponse.from({
193 | status: "ok",
194 | debugInfo: debugInfo
195 | });
196 |
197 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
198 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
199 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
200 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
201 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
202 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
203 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
204 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
205 | msg.encodeBinaryProperties();
206 | assert.isString(msg.debugInfo.clientData.rawClientDataJson);
207 | assert.isString(msg.debugInfo.clientData.rawId);
208 | assert.isString(msg.debugInfo.authnrData.rawAuthnrData);
209 | assert.isString(msg.debugInfo.authnrData.rpIdHash);
210 | assert.isString(msg.debugInfo.authnrData.aaguid);
211 | assert.isString(msg.debugInfo.authnrData.credId);
212 | assert.isString(msg.debugInfo.authnrData.credentialPublicKeyCose);
213 | assert.isArray(msg.debugInfo.authnrData.flags);
214 | });
215 |
216 | it("decodes correctly", function() {
217 | var msg = ServerResponse.from({
218 | status: "ok",
219 | debugInfo: debugInfo
220 | });
221 |
222 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
223 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
224 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
225 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
226 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
227 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
228 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
229 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
230 | msg.encodeBinaryProperties();
231 | assert.isString(msg.debugInfo.clientData.rawClientDataJson);
232 | assert.isString(msg.debugInfo.clientData.rawId);
233 | assert.isString(msg.debugInfo.authnrData.rawAuthnrData);
234 | assert.isString(msg.debugInfo.authnrData.rpIdHash);
235 | assert.isString(msg.debugInfo.authnrData.aaguid);
236 | assert.isString(msg.debugInfo.authnrData.credId);
237 | assert.isString(msg.debugInfo.authnrData.credentialPublicKeyCose);
238 | assert.isArray(msg.debugInfo.authnrData.flags);
239 | msg.decodeBinaryProperties();
240 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
241 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
242 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
243 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
244 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
245 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
246 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
247 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
248 | });
249 | });
250 |
251 | describe.skip("assertion debugInfo", function() {
252 | var debugInfo;
253 | beforeEach(function() {
254 | debugInfo =
255 | {
256 | clientData: {
257 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
258 | origin: "https://localhost:8443",
259 | type: "webauthn.create",
260 | tokenBinding: undefined,
261 | rawClientDataJson: new ArrayBuffer(),
262 | rawId: new ArrayBuffer()
263 | },
264 | authnrData: {
265 | fmt: "none",
266 | rawAuthnrData: new ArrayBuffer(),
267 | rpIdHash: new ArrayBuffer(),
268 | flags: new Set(["UP", "AT"]),
269 | counter: 0,
270 | aaguid: new ArrayBuffer(),
271 | credIdLen: 162,
272 | credId: new ArrayBuffer(),
273 | credentialPublicKeyCose: new ArrayBuffer(),
274 | credentialPublicKeyJwk: {
275 | kty: "EC",
276 | alg: "ECDSA_w_SHA256",
277 | crv: "P-256",
278 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
279 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM="
280 | },
281 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC\nFZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==\n-----END PUBLIC KEY-----\n"
282 | }
283 | };
284 | });
285 |
286 | it("is included", function() {
287 | var msg = ServerResponse.from({
288 | status: "ok",
289 | debugInfo: debugInfo
290 | });
291 |
292 | assert.isObject(msg.debugInfo);
293 | assert.isObject(msg.debugInfo.clientData);
294 | assert.isObject(msg.debugInfo.authnrData);
295 | });
296 |
297 | it("validates", function() {
298 | var msg = ServerResponse.from({
299 | status: "ok",
300 | debugInfo: debugInfo
301 | });
302 |
303 | msg.validate();
304 | });
305 |
306 | it("encodes correctly", function() {
307 | var msg = ServerResponse.from({
308 | status: "ok",
309 | debugInfo: debugInfo
310 | });
311 |
312 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
313 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
314 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
315 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
316 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
317 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
318 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
319 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
320 | msg.encodeBinaryProperties();
321 | assert.isString(msg.debugInfo.clientData.rawClientDataJson);
322 | assert.isString(msg.debugInfo.clientData.rawId);
323 | assert.isString(msg.debugInfo.authnrData.rawAuthnrData);
324 | assert.isString(msg.debugInfo.authnrData.rpIdHash);
325 | assert.isString(msg.debugInfo.authnrData.aaguid);
326 | assert.isString(msg.debugInfo.authnrData.credId);
327 | assert.isString(msg.debugInfo.authnrData.credentialPublicKeyCose);
328 | assert.isArray(msg.debugInfo.authnrData.flags);
329 | });
330 |
331 | it("decodes correctly", function() {
332 | var msg = ServerResponse.from({
333 | status: "ok",
334 | debugInfo: debugInfo
335 | });
336 |
337 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
338 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
339 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
340 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
341 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
342 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
343 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
344 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
345 | msg.encodeBinaryProperties();
346 | assert.isString(msg.debugInfo.clientData.rawClientDataJson);
347 | assert.isString(msg.debugInfo.clientData.rawId);
348 | assert.isString(msg.debugInfo.authnrData.rawAuthnrData);
349 | assert.isString(msg.debugInfo.authnrData.rpIdHash);
350 | assert.isString(msg.debugInfo.authnrData.aaguid);
351 | assert.isString(msg.debugInfo.authnrData.credId);
352 | assert.isString(msg.debugInfo.authnrData.credentialPublicKeyCose);
353 | assert.isArray(msg.debugInfo.authnrData.flags);
354 | msg.decodeBinaryProperties();
355 | assert.instanceOf(msg.debugInfo.clientData.rawClientDataJson, ArrayBuffer);
356 | assert.instanceOf(msg.debugInfo.clientData.rawId, ArrayBuffer);
357 | assert.instanceOf(msg.debugInfo.authnrData.rawAuthnrData, ArrayBuffer);
358 | assert.instanceOf(msg.debugInfo.authnrData.rpIdHash, ArrayBuffer);
359 | assert.instanceOf(msg.debugInfo.authnrData.aaguid, ArrayBuffer);
360 | assert.instanceOf(msg.debugInfo.authnrData.credId, ArrayBuffer);
361 | assert.instanceOf(msg.debugInfo.authnrData.credentialPublicKeyCose, ArrayBuffer);
362 | assert.instanceOf(msg.debugInfo.authnrData.flags, Set);
363 | });
364 | });
365 |
366 |
367 | describe("toHumanString", function() {
368 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
369 | it("creates correct string for attestation", function() {
370 | var msg = ServerResponse.from({
371 | status: "ok",
372 | debugInfo: {
373 | clientData: {
374 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
375 | origin: "https://localhost:8443",
376 | type: "webauthn.create",
377 | tokenBinding: undefined,
378 | rawClientDataJson: new ArrayBuffer(),
379 | rawId: new ArrayBuffer()
380 | },
381 | authnrData: {
382 | fmt: "none",
383 | rawAuthnrData: new ArrayBuffer(),
384 | rpIdHash: new ArrayBuffer(),
385 | flags: new Set(["UP", "AT"]),
386 | counter: 0,
387 | aaguid: new ArrayBuffer(),
388 | credIdLen: 162,
389 | credId: new ArrayBuffer(),
390 | credentialPublicKeyCose: new ArrayBuffer(),
391 | credentialPublicKeyJwk: {
392 | kty: "EC",
393 | alg: "ECDSA_w_SHA256",
394 | crv: "P-256",
395 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
396 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM="
397 | },
398 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC\nFZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==\n-----END PUBLIC KEY-----\n"
399 | },
400 | audit: {
401 | validExpectations: true,
402 | validRequest: true,
403 | complete: true,
404 | warning: new Map(),
405 | info: new Map([
406 | ["yubico-device-id", "YubiKey 4/YubiKey 4 Nano"],
407 | ["fido-u2f-transports", new Set(["usb"])],
408 | ["attestation-type", "basic"],
409 | ]),
410 | }
411 | }
412 | });
413 | var str = msg.toHumanString();
414 | assert.isString(str);
415 | assert.strictEqual(
416 | str,
417 | // eslint-disable-next-line
418 | `[ServerResponse] {
419 | status: "ok",
420 | debugInfo: {
421 | clientData: {
422 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
423 | origin: "https://localhost:8443",
424 | type: "webauthn.create",
425 | tokenBinding: undefined,
426 | rawClientDataJson: [ArrayBuffer] (0 bytes),
427 | rawId: [ArrayBuffer] (0 bytes),
428 | },
429 | authnrData: {
430 | fmt: "none",
431 | rawAuthnrData: [ArrayBuffer] (0 bytes),
432 | rpIdHash: [ArrayBuffer] (0 bytes),
433 | flags: [
434 | "UP",
435 | "AT",
436 | ],
437 | counter: 0,
438 | aaguid: [ArrayBuffer] (0 bytes),
439 | credIdLen: 162,
440 | credId: [ArrayBuffer] (0 bytes),
441 | credentialPublicKeyCose: [ArrayBuffer] (0 bytes),
442 | credentialPublicKeyJwk: {
443 | kty: "EC",
444 | alg: "ECDSA_w_SHA256",
445 | crv: "P-256",
446 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
447 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM=",
448 | },
449 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----
450 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC
451 | FZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==
452 | -----END PUBLIC KEY-----
453 | ",
454 | },
455 | audit: {
456 | validExpectations: true,
457 | validRequest: true,
458 | complete: true,
459 | warning: {
460 | },
461 | info: {
462 | },
463 | },
464 | },
465 | }`
466 | );
467 | });
468 |
469 | // var testArgs = fido2Helpers.functions.cloneObject(fido2Helpers.server.challengeResponseAttestationNoneMsgB64Url);
470 | it("creates correct string for assertion", function() {
471 | var msg = ServerResponse.from({
472 | status: "ok",
473 | debugInfo: {
474 | clientData: {
475 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
476 | origin: "https://localhost:8443",
477 | type: "webauthn.create",
478 | tokenBinding: undefined,
479 | rawClientDataJson: new ArrayBuffer(),
480 | rawId: new ArrayBuffer()
481 | },
482 | authnrData: {
483 | fmt: "none",
484 | rawAuthnrData: new ArrayBuffer(),
485 | rpIdHash: new ArrayBuffer(),
486 | flags: new Set(["UP", "AT"]),
487 | counter: 0,
488 | aaguid: new ArrayBuffer(),
489 | credIdLen: 162,
490 | credId: new ArrayBuffer(),
491 | credentialPublicKeyCose: new ArrayBuffer(),
492 | credentialPublicKeyJwk: {
493 | kty: "EC",
494 | alg: "ECDSA_w_SHA256",
495 | crv: "P-256",
496 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
497 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM="
498 | },
499 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC\nFZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==\n-----END PUBLIC KEY-----\n"
500 | },
501 | audit: {
502 | validExpectations: true,
503 | validRequest: true,
504 | complete: true,
505 | warning: new Map(),
506 | info: new Map([
507 | ["yubico-device-id", "YubiKey 4/YubiKey 4 Nano"],
508 | ["fido-u2f-transports", new Set(["usb"])],
509 | ["attestation-type", "basic"],
510 | ]),
511 | }
512 | }
513 | });
514 | var str = msg.toHumanString();
515 | assert.isString(str);
516 | assert.strictEqual(
517 | str,
518 | // eslint-disable-next-line
519 | `[ServerResponse] {
520 | status: "ok",
521 | debugInfo: {
522 | clientData: {
523 | challenge: "33EHav-jZ1v9qwH783aU-j0ARx6r5o-YHh-wd7C6jPbd7Wh6ytbIZosIIACehwf9-s6hXhySHO-HHUjEwZS29w",
524 | origin: "https://localhost:8443",
525 | type: "webauthn.create",
526 | tokenBinding: undefined,
527 | rawClientDataJson: [ArrayBuffer] (0 bytes),
528 | rawId: [ArrayBuffer] (0 bytes),
529 | },
530 | authnrData: {
531 | fmt: "none",
532 | rawAuthnrData: [ArrayBuffer] (0 bytes),
533 | rpIdHash: [ArrayBuffer] (0 bytes),
534 | flags: [
535 | "UP",
536 | "AT",
537 | ],
538 | counter: 0,
539 | aaguid: [ArrayBuffer] (0 bytes),
540 | credIdLen: 162,
541 | credId: [ArrayBuffer] (0 bytes),
542 | credentialPublicKeyCose: [ArrayBuffer] (0 bytes),
543 | credentialPublicKeyJwk: {
544 | kty: "EC",
545 | alg: "ECDSA_w_SHA256",
546 | crv: "P-256",
547 | x: "uxHN3W6ehp0VWXKaMNie1J82MVJCFZYScau74o17cx8=",
548 | y: "29Y5Ey4u5WGWW4MFMKagJPEJiIjzE1UFFZIRhMhqysM=",
549 | },
550 | credentialPublicKeyPem: "-----BEGIN PUBLIC KEY-----
551 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuxHN3W6ehp0VWXKaMNie1J82MVJC
552 | FZYScau74o17cx/b1jkTLi7lYZZbgwUwpqAk8QmIiPMTVQUVkhGEyGrKww==
553 | -----END PUBLIC KEY-----
554 | ",
555 | },
556 | audit: {
557 | validExpectations: true,
558 | validRequest: true,
559 | complete: true,
560 | warning: {
561 | },
562 | info: {
563 | },
564 | },
565 | },
566 | }`
567 | );
568 | });
569 | });
570 | });
571 |
--------------------------------------------------------------------------------
/test/common/webauthn-options-test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | describe.skip("WebAuthnOptions", function() {
4 | const {
5 | Msg,
6 | WebAuthnOptions
7 | } = GlobalWebAuthnClasses;
8 |
9 | it("is loaded", function() {
10 | assert.isFunction(WebAuthnOptions);
11 | });
12 |
13 | it("is Msg class", function() {
14 | var msg = new WebAuthnOptions();
15 | assert.instanceOf(msg, Msg);
16 | });
17 |
18 | describe("merge", function() {
19 | it("dst over src", function() {
20 | var src = WebAuthnOptions.from({
21 | timeout: 1
22 | });
23 |
24 | var dst = WebAuthnOptions.from({
25 | timeout: 2
26 | });
27 |
28 | src.merge(dst, true);
29 |
30 | assert.strictEqual(src.timeout, 2);
31 | });
32 |
33 | it("src over dst", function() {
34 | var src = WebAuthnOptions.from({
35 | timeout: 1
36 | });
37 |
38 | var dst = WebAuthnOptions.from({
39 | timeout: 2
40 | });
41 |
42 | src.merge(dst, false);
43 |
44 | assert.strictEqual(src.timeout, 1);
45 | });
46 |
47 | it("sets missing values", function() {
48 | var src = WebAuthnOptions.from({});
49 | var dst = WebAuthnOptions.from({
50 | timeout: 2
51 | });
52 |
53 | src.merge(dst, false);
54 |
55 | assert.strictEqual(src.timeout, 2);
56 | });
57 |
58 | it("allows empty", function() {
59 | var src = WebAuthnOptions.from({});
60 | var dst = WebAuthnOptions.from({});
61 |
62 | src.merge(dst, false);
63 |
64 | assert.isUndefined(src.timeout);
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/node/test-setup.js:
--------------------------------------------------------------------------------
1 | global.GlobalWebAuthnClasses = require("../../dist/webauthn-simple-app.cjs");
2 | global.assert = require("chai").assert;
3 | global.fido2Helpers = require("fido2-helpers");
4 |
--------------------------------------------------------------------------------
/test/node/test.js:
--------------------------------------------------------------------------------
1 | /* globals chai, assert, fido2Helpers, GlobalWebAuthnClasses */
2 |
3 | const {
4 | Msg,
5 | ServerResponse,
6 | CreateOptionsRequest,
7 | CreateOptions,
8 | CredentialAttestation,
9 | GetOptionsRequest,
10 | GetOptions,
11 | CredentialAssertion,
12 | WebAuthnHelpers,
13 | WebAuthnApp
14 | } = GlobalWebAuthnClasses;
15 |
16 | const {
17 | isNode,
18 | isBrowser,
19 | coerceToBase64Url,
20 | coerceToArrayBuffer
21 | } = WebAuthnHelpers.utils;
22 |
23 | describe("node", function() {
24 | it("is running on node", function() {
25 | assert.throws(() => {
26 | assert.isUndefined(window);
27 | }, ReferenceError, "window is not defined");
28 | });
29 |
30 | it("can load", function() {
31 | assert.isFunction(Msg);
32 | assert.isFunction(ServerResponse);
33 | assert.isFunction(CreateOptionsRequest);
34 | assert.isFunction(CreateOptions);
35 | assert.isFunction(CredentialAttestation);
36 | assert.isFunction(GetOptionsRequest);
37 | assert.isFunction(GetOptions);
38 | assert.isFunction(CredentialAssertion);
39 | assert.isUndefined(WebAuthnApp);
40 | });
41 |
42 | describe("utils", function() {
43 | describe("coerceToArrayBuffer", function() {
44 | it("can coerce Buffer to ArrayBuffer", function() {
45 | var ab = Buffer.from([
46 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
47 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
48 | ]);
49 | var res = coerceToArrayBuffer(ab);
50 | assert.instanceOf(res, ArrayBuffer);
51 | var expectedAb = Uint8Array.from([
52 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
53 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
54 | ]).buffer;
55 | assert.isTrue(fido2Helpers.functions.abEqual(res, expectedAb), "got expected ArrayBuffer value");
56 | });
57 |
58 | it("coerceToArrayBuffer doesn't return Buffer", function() {
59 | var b64url = "AAECAwQFBgcJCgsMDQ4_-A";
60 | var res = coerceToArrayBuffer(b64url);
61 | assert.instanceOf(res, ArrayBuffer);
62 | assert.notInstanceOf(res, Buffer);
63 | });
64 | });
65 |
66 | describe("coerceToBase64Url", function() {
67 | it("can coerce Buffer to base64", function() {
68 | var ab = Uint8Array.from([
69 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
70 | 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x3F, 0xF8
71 | ]).buffer;
72 | var res = coerceToBase64Url(ab);
73 | assert.isString(res);
74 | assert.strictEqual(res, "AAECAwQFBgcJCgsMDQ4_-A");
75 | });
76 | });
77 |
78 | describe("isNode", function() {
79 | it("returns true", function() {
80 | assert.isTrue(isNode());
81 | });
82 | });
83 |
84 | describe("isBrowser", function() {
85 | it("returns false", function() {
86 | assert.isFalse(isBrowser());
87 | });
88 |
89 | });
90 | });
91 | });
92 |
--------------------------------------------------------------------------------