├── .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 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/apowers313.svg)](https://saucelabs.com/u/apowers313) 2 | 3 | node.js: [![Build Status](https://travis-ci.org/apowers313/webauthn-simple-app.svg?branch=master)](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 |
132 | 133 | 134 |
135 | 136 | 137 |
138 | 139 | 140 |
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 | --------------------------------------------------------------------------------