nid-webauthn-emulator 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +216 -0
- package/README.md +52 -49
- package/dist/spec/authenticator/authenticator-emulator.spec.js +21 -22
- package/dist/spec/authenticator/ctap-model.spec.js +14 -15
- package/dist/spec/webauthn/webauthn-emulator.spec.js +1 -1
- package/dist/src/authenticator/authenticator-emulator.js +1 -1
- package/dist/src/authenticator/ctap-model.js +48 -48
- package/dist/src/index.d.ts +5 -5
- package/dist/src/index.js +5 -5
- package/dist/src/webauthn/webauthn-emulator.js +4 -4
- package/dist/src/webauthn/webauthn-model.js +7 -8
- package/package.json +10 -10
package/README.ja.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# NID WebAuthn Emulator
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Nikkei/nid-webauthn-emulator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
[English](README.md) | [日本語](README.ja.md)
|
|
7
|
+
|
|
8
|
+
`NID WebAuthn Emulator` は、[FIDO2/CTAP Authenticator のエミュレータ](src/authenticator/authenticator-emulator.ts) およびそれを利用した [WebAuthn API のエミュレータ](src/webauthn/webauthn-emulator.ts) ライブラリです。それぞれ WebAuthn API および CTAP の仕様に基づいて実装されています。このモジュールは Node.js 上で動作し、ローカルでの Passkeys の統合テストを目的として設計されています。
|
|
9
|
+
|
|
10
|
+
それぞれのエミュレータの詳細な仕様は下記を参照してください。
|
|
11
|
+
|
|
12
|
+
- [FIDO2/CTAP Authenticator Emulator 開発者向け詳細仕様](docs/authenticator-emulator.ja.md)
|
|
13
|
+
- [WebAuthn API Emulator 開発者向け詳細仕様](docs/webauthn-emulator.ja.md)
|
|
14
|
+
|
|
15
|
+
## 使用方法
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install nid-webauthn-emulator
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
基本的な使い方は `WebAuthnEmulator` クラスを作成し、`create` および `get` メソッドを利用することで、WebAuthn API における `navigator.credentials.create` および `navigator.credentials.get` をエミュレーションすることができます。
|
|
22
|
+
|
|
23
|
+
```TypeScript
|
|
24
|
+
import WebAuthnEmulator from "nid-webauthn-emulator";
|
|
25
|
+
|
|
26
|
+
const emulator = new WebAuthnEmulator();
|
|
27
|
+
const origin = "https://example.com";
|
|
28
|
+
|
|
29
|
+
emulator.create(origin, creationOptions);
|
|
30
|
+
emulator.get(origin, requestOptions);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
また、`createJSON` および `getJSON` メソッドを利用することで、WebAuthn API で WebAuthn API 仕様に基づいた JSON データでのエミュレーションを行うこともできます。
|
|
34
|
+
|
|
35
|
+
```TypeScript
|
|
36
|
+
emulator.createJSON(origin, creationOptionsJSON);
|
|
37
|
+
emulator.getJSON(origin, requestOptionsJSON);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
これらの JSON の仕様は、標準仕様の下記に定義されたデータです。
|
|
41
|
+
|
|
42
|
+
- <https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson>
|
|
43
|
+
- <https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson>
|
|
44
|
+
|
|
45
|
+
`WebAuthnEmulator` クラスは標準で下記の FIDO2/CTAP Authenticator をエミュレートします。
|
|
46
|
+
|
|
47
|
+
- 自動で User Verification を行います (`uv` フラグを立てます)
|
|
48
|
+
- ES256, RS256, EdDSA のアルゴリズムをサポートします
|
|
49
|
+
- USB 接続の CTAP2 デバイスをエミュレートします
|
|
50
|
+
- AAGUID は `NID-AUTH-3141592` です
|
|
51
|
+
- 認証時には Sign Counter を `1` インクリメントさせます
|
|
52
|
+
|
|
53
|
+
これらの設定は下記のように `AuthenticatorEmulator` クラスを作成し、`WebAuthnEmulator` クラスに渡すことで、Authenticator の挙動を変更することができます。
|
|
54
|
+
|
|
55
|
+
```TypeScript
|
|
56
|
+
import WebAuthnEmulator, { AuthenticatorEmulator } from "nid-webauthn-emulator";
|
|
57
|
+
|
|
58
|
+
const authenticator = new AuthenticatorEmulator({
|
|
59
|
+
algorithmIdentifiers: ["ES256"],
|
|
60
|
+
verifications: {
|
|
61
|
+
userVerified: false,
|
|
62
|
+
userPresent: false,
|
|
63
|
+
},
|
|
64
|
+
signCounterIncrement: 0,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const webAuthnEmulator = new WebAuthnEmulator(authenticator);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`AuthenticatorEmulator` は FIDO2/CTAP の仕様のうち下記のコマンドを実装しています。
|
|
71
|
+
|
|
72
|
+
- `authenticatorMakeCredential` (CTAP2) : 認証情報の作成
|
|
73
|
+
- `authenticatorGetAssertion` (CTAP2) : 認証情報の取得
|
|
74
|
+
- `authenticatorGetInfo` (CTAP2) : 認証情報の取得
|
|
75
|
+
|
|
76
|
+
これらは通常は直接利用することはありませんが、`WebAuthnEmulator` クラスの内部で CTAP のプロトコルに従って下記の通り呼び出されています。
|
|
77
|
+
|
|
78
|
+
```TypeScript
|
|
79
|
+
const authenticatorRequest = packMakeCredentialRequest({
|
|
80
|
+
clientDataHash: createHash("sha256").update(clientDataJSON).digest(),
|
|
81
|
+
rp: options.publicKey.rp,
|
|
82
|
+
user: options.publicKey.user,
|
|
83
|
+
pubKeyCredParams: options.publicKey.pubKeyCredParams,
|
|
84
|
+
excludeList: options.publicKey.excludeCredentials,
|
|
85
|
+
options: {
|
|
86
|
+
rk: options.publicKey.authenticatorSelection?.requireResidentKey,
|
|
87
|
+
uv: options.publicKey.authenticatorSelection?.userVerification !== "discouraged",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const authenticatorResponse = unpackMakeCredentialResponse(this.authenticator.command(authenticatorRequest));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## [WebAuthn.io](https://webauthn.io/) での実行例
|
|
94
|
+
|
|
95
|
+
WebAuthn の著名なデモサイトである [webauthn.io](https://webauthn.io/) での使用例を示します。[統合テスト](spec/integration/integration.spec.ts) に実際にうごくテストコード例があります。
|
|
96
|
+
|
|
97
|
+
```TypeScript
|
|
98
|
+
// Origin および WebAuthn API エミュレータを初期化します
|
|
99
|
+
// ここでは、https://webauthn.io を Origin として利用します
|
|
100
|
+
const origin = "https://webauthn.io";
|
|
101
|
+
const emulator = new WebAuthnEmulator();
|
|
102
|
+
const webauthnIO = await WebAuthnIO.create();
|
|
103
|
+
const user = webauthnIO.getUser();
|
|
104
|
+
|
|
105
|
+
// Authenticator の情報を表示します
|
|
106
|
+
console.log("Authenticator Information", emulator.getAuthenticatorInfo());
|
|
107
|
+
|
|
108
|
+
// WebAuthn API Emulator により パスキーを作成します
|
|
109
|
+
const creationOptions = await webauthnIO.getRegistrationOptions(user);
|
|
110
|
+
const creationCredential = emulator.createJSON(origin, creationOptions);
|
|
111
|
+
await webauthnIO.getRegistrationVerification(user, creationCredential);
|
|
112
|
+
|
|
113
|
+
// 認証を webauthn.io で検証します
|
|
114
|
+
const requestOptions = await webauthnIO.getAuthenticationOptions();
|
|
115
|
+
const requestCredential = emulator.getJSON(origin, requestOptions);
|
|
116
|
+
await webauthnIO.getAuthenticationVerification(requestCredential);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Playwright による自動テスト
|
|
120
|
+
|
|
121
|
+
このライブラリは Passkeys の E2E テストでの利用を目的としており、特に Playwright での利用を想定しています。Playwright でのテスト用にユーティリティクラス `BrowserInjection` を利用して簡単に、WebAuthn API エミュレータを利用することができます。使い方は下記の通りです。
|
|
122
|
+
|
|
123
|
+
```TypeScript
|
|
124
|
+
import WebAuthnEmulator, {
|
|
125
|
+
BrowserInjection,
|
|
126
|
+
type PublicKeyCredentialCreationOptionsJSON,
|
|
127
|
+
type PublicKeyCredentialRequestOptionsJSON,
|
|
128
|
+
} from "nid-webauthn-emulator";
|
|
129
|
+
|
|
130
|
+
async function startWebAuthnEmulator(page: Page, origin: string, debug = false, relatedOrigins: string[] = []) {
|
|
131
|
+
const emulator = new WebAuthnEmulator();
|
|
132
|
+
|
|
133
|
+
await page.exposeFunction(
|
|
134
|
+
BrowserInjection.WebAuthnEmulatorCreate,
|
|
135
|
+
async (optionsJSON: PublicKeyCredentialCreationOptionsJSON) => {
|
|
136
|
+
const response = emulator.createJSON(origin, optionsJSON, relatedOrigins);
|
|
137
|
+
return response;
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await page.exposeFunction(
|
|
142
|
+
BrowserInjection.WebAuthnEmulatorGet,
|
|
143
|
+
async (optionsJSON: PublicKeyCredentialRequestOptionsJSON) => {
|
|
144
|
+
const response = emulator.getJSON(origin, optionsJSON, relatedOrigins);
|
|
145
|
+
return response;
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
await page.exposeFunction(
|
|
150
|
+
BrowserInjection.WebAuthnEmulatorSignalUnknownCredential,
|
|
151
|
+
async (options: UnknownCredentialOptionsJSON) => {
|
|
152
|
+
emulator.signalUnknownCredential(options);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await page.exposeFunction(
|
|
157
|
+
BrowserInjection.WebAuthnEmulatorSignalAllAcceptedCredentials,
|
|
158
|
+
async (options: AllAcceptedCredentialsOptionsJSON) => {
|
|
159
|
+
emulator.signalAllAcceptedCredentials(options);
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await page.exposeFunction(
|
|
164
|
+
BrowserInjection.WebAuthnEmulatorSignalCurrentUserDetails,
|
|
165
|
+
async (options: CurrentUserDetailsOptionsJSON) => {
|
|
166
|
+
emulator.signalCurrentUserDetails(options);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test.describe("Passkeys Tests", { tag: ["@daily"] }, () => {
|
|
172
|
+
test("Passkeys login test", async ({ page }) => {
|
|
173
|
+
// Page内で最初に1回だけ定義する exposed functions
|
|
174
|
+
// 必要に応じて関連オリジンを指定できます
|
|
175
|
+
const relatedOrigins = ["https://sub.example.com", "https://alt.example.com"];
|
|
176
|
+
await startWebAuthnEmulator(page, env, true, relatedOrigins);
|
|
177
|
+
await page.goto("https://example.com/passkeys/login");
|
|
178
|
+
|
|
179
|
+
// Passkeys の WebAuthn API をフック開始
|
|
180
|
+
// ページ遷移後に実行する必要がある
|
|
181
|
+
await page.evaluate(BrowserInjection.HookWebAuthnApis);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`startWebAuthnEmulator` では Playwright の `exposeFunction` を利用して、`WebAuthnEmulator` の `createJSON` および `getJSON` メソッドをブラウザのコンテキストに注入します。これにより、Playwright でのテストコンテキストにおいて `WebAuthnEmulator` クラスの `get` および `create` の各 API がそれぞれ `window` オブジェクトの下に定義されるようになります。
|
|
187
|
+
|
|
188
|
+
- `window.webAuthnEmulatorGet`: `WebAuthnEmulator.getJSON` の Exposed Function
|
|
189
|
+
- `window.webAuthnEmulatorCreate`: `WebAuthnEmulator.createJSON` の Exposed Function
|
|
190
|
+
- `window.webAuthnEmulatorSignalUnknownCredential`: `WebAuthnEmulator.signalUnknownCredential` の Exposed Function
|
|
191
|
+
- `window.webAuthnEmulatorSignalAllAcceptedCredentials`: `WebAuthnEmulator.signalAllAcceptedCredentials` の Exposed Function
|
|
192
|
+
- `window.webAuthnEmulatorSignalCurrentUserDetails`: `WebAuthnEmulator.signalCurrentUserDetails` の Exposed Function
|
|
193
|
+
|
|
194
|
+
さらに`startWebAuthnEmulator`関数は`relatedOrigins`パラメータをサポートしています。これにより、異なるオリジンからのリクエストでも同じRP IDを使用できるようになります。例えば、マルチドメイン環境(`example.com`と`sub.example.com`など)でPasskeysを使用する場合に便利です。`relatedOrigins` の値はRP IDで指定されたドメインでホストされた `/.well-known/webauthn` の内容と同じです。
|
|
195
|
+
|
|
196
|
+
これらは Page グローバルに定義されるため、Page インスタンスにつき 1 回だけ定義する必要があります。
|
|
197
|
+
|
|
198
|
+
次に `navigator.credentials.get` 等の WebAuthn API をフックするために、`BrowserInjection.HookWebAuthnApis` をテストコンテキストにおいて評価します。
|
|
199
|
+
|
|
200
|
+
```TypeScript
|
|
201
|
+
await page.evaluate(BrowserInjection.HookWebAuthnApis);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`BrowserInjection.HookWebAuthnApis` は JavaScript 関数のシリアライズされた文字列であり、評価すると下記のような処理を行います。
|
|
205
|
+
|
|
206
|
+
- `navigator.credentials.get` の定義を上書きし、`window.webAuthnEmulatorGet` を呼び出す
|
|
207
|
+
- `navigator.credentials.create` の定義を上書きし、`window.webAuthnEmulatorCreate` を呼び出す
|
|
208
|
+
- `PublicKeyCredential.signalUnknownCredential` の定義を追加し、`window.webAuthnEmulatorSignalUnknownCredential` を呼び出す
|
|
209
|
+
|
|
210
|
+
これにより先ほどの `exposeFunction` で定義した `WebAuthnEmulator` のメソッドが、`navigator.credentials.get` および `navigator.credentials.create` 呼び出し時に実行されるようになります。これらの処理中にはテストコンテキストと Playwright のコンテキスト間の通信のためにデータのシリアライズおよびデシリアライズが行われるため、そのための処理も含まれています。
|
|
211
|
+
|
|
212
|
+
## ライセンス
|
|
213
|
+
|
|
214
|
+
MIT License
|
|
215
|
+
|
|
216
|
+
Copyright (C) 2024 Nikkei Inc.
|
package/README.md
CHANGED
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Nikkei/nid-webauthn-emulator/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://deepwiki.com/Nikkei/nid-webauthn-emulator)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
[English](README.md) | [日本語](README.ja.md)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
`NID WebAuthn Emulator` is a library that provides both a [FIDO2/CTAP Authenticator emulator](src/authenticator/authenticator-emulator.ts) and a [WebAuthn API emulator](src/webauthn/webauthn-emulator.ts) built on top of it. Each component is implemented according to the WebAuthn API and CTAP specifications respectively. This module runs on Node.js and is designed for local integration testing of Passkeys.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
- [WebAuthn API Emulator 開発者向け詳細仕様](docs/webauthn-emulator.md)
|
|
11
|
+
For detailed specifications of each emulator, please refer to the following:
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- [FIDO2/CTAP Authenticator Emulator Detailed Specification for Developers](docs/authenticator-emulator.en.md)
|
|
14
|
+
- [WebAuthn API Emulator Detailed Specification for Developers](docs/webauthn-emulator.en.md)
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
14
17
|
|
|
15
18
|
```bash
|
|
16
19
|
npm install nid-webauthn-emulator
|
|
17
20
|
```
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
Basic usage involves creating a `WebAuthnEmulator` class and using the `create` and `get` methods to emulate `navigator.credentials.create` and `navigator.credentials.get` from the WebAuthn API.
|
|
20
23
|
|
|
21
24
|
```TypeScript
|
|
22
25
|
import WebAuthnEmulator from "nid-webauthn-emulator";
|
|
@@ -28,27 +31,27 @@ emulator.create(origin, creationOptions);
|
|
|
28
31
|
emulator.get(origin, requestOptions);
|
|
29
32
|
```
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
You can also use the `createJSON` and `getJSON` methods to perform emulation with JSON data based on the WebAuthn API specification.
|
|
32
35
|
|
|
33
36
|
```TypeScript
|
|
34
37
|
emulator.createJSON(origin, creationOptionsJSON);
|
|
35
38
|
emulator.getJSON(origin, requestOptionsJSON);
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
These JSON specifications are defined as standard specification data in the following:
|
|
39
42
|
|
|
40
43
|
- <https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson>
|
|
41
44
|
- <https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson>
|
|
42
45
|
|
|
43
|
-
`WebAuthnEmulator`
|
|
46
|
+
The `WebAuthnEmulator` class emulates the following FIDO2/CTAP Authenticator by default:
|
|
44
47
|
|
|
45
|
-
-
|
|
46
|
-
- ES256, RS256, EdDSA
|
|
47
|
-
- USB
|
|
48
|
-
- AAGUID
|
|
49
|
-
-
|
|
48
|
+
- Automatically performs User Verification (sets the `uv` flag)
|
|
49
|
+
- Supports ES256, RS256, and EdDSA algorithms
|
|
50
|
+
- Emulates a USB-connected CTAP2 device
|
|
51
|
+
- AAGUID is `NID-AUTH-3141592`
|
|
52
|
+
- Increments the Sign Counter by `1` during authentication
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
These settings can be changed by creating an `AuthenticatorEmulator` class and passing it to the `WebAuthnEmulator` class to modify the Authenticator behavior as follows:
|
|
52
55
|
|
|
53
56
|
```TypeScript
|
|
54
57
|
import WebAuthnEmulator, { AuthenticatorEmulator } from "nid-webauthn-emulator";
|
|
@@ -65,13 +68,13 @@ const authenticator = new AuthenticatorEmulator({
|
|
|
65
68
|
const webAuthnEmulator = new WebAuthnEmulator(authenticator);
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
`AuthenticatorEmulator`
|
|
71
|
+
`AuthenticatorEmulator` implements the following commands from the FIDO2/CTAP specification:
|
|
69
72
|
|
|
70
|
-
- `authenticatorMakeCredential` (CTAP2)
|
|
71
|
-
- `authenticatorGetAssertion` (CTAP2)
|
|
72
|
-
- `authenticatorGetInfo` (CTAP2)
|
|
73
|
+
- `authenticatorMakeCredential` (CTAP2): Creating credentials
|
|
74
|
+
- `authenticatorGetAssertion` (CTAP2): Retrieving credentials
|
|
75
|
+
- `authenticatorGetInfo` (CTAP2): Getting authenticator information
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
These are typically not used directly, but are called internally by the `WebAuthnEmulator` class according to the CTAP protocol as follows:
|
|
75
78
|
|
|
76
79
|
```TypeScript
|
|
77
80
|
const authenticatorRequest = packMakeCredentialRequest({
|
|
@@ -88,35 +91,35 @@ const authenticatorRequest = packMakeCredentialRequest({
|
|
|
88
91
|
const authenticatorResponse = unpackMakeCredentialResponse(this.authenticator.command(authenticatorRequest));
|
|
89
92
|
```
|
|
90
93
|
|
|
91
|
-
## [WebAuthn.io](https://webauthn.io/)
|
|
94
|
+
## Example with [WebAuthn.io](https://webauthn.io/)
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
Here's an example of usage with [webauthn.io](https://webauthn.io/), a well-known WebAuthn demo site. You can find working test code examples in the [integration tests](spec/integration/integration.spec.ts).
|
|
94
97
|
|
|
95
98
|
```TypeScript
|
|
96
|
-
// Origin
|
|
97
|
-
//
|
|
99
|
+
// Initialize the Origin and WebAuthn API emulator
|
|
100
|
+
// Here we use https://webauthn.io as the Origin
|
|
98
101
|
const origin = "https://webauthn.io";
|
|
99
102
|
const emulator = new WebAuthnEmulator();
|
|
100
103
|
const webauthnIO = await WebAuthnIO.create();
|
|
101
104
|
const user = webauthnIO.getUser();
|
|
102
105
|
|
|
103
|
-
// Authenticator
|
|
106
|
+
// Display Authenticator information
|
|
104
107
|
console.log("Authenticator Information", emulator.getAuthenticatorInfo());
|
|
105
108
|
|
|
106
|
-
// WebAuthn API Emulator
|
|
109
|
+
// Create passkey using WebAuthn API Emulator
|
|
107
110
|
const creationOptions = await webauthnIO.getRegistrationOptions(user);
|
|
108
111
|
const creationCredential = emulator.createJSON(origin, creationOptions);
|
|
109
112
|
await webauthnIO.getRegistrationVerification(user, creationCredential);
|
|
110
113
|
|
|
111
|
-
//
|
|
114
|
+
// Verify authentication with webauthn.io
|
|
112
115
|
const requestOptions = await webauthnIO.getAuthenticationOptions();
|
|
113
116
|
const requestCredential = emulator.getJSON(origin, requestOptions);
|
|
114
117
|
await webauthnIO.getAuthenticationVerification(requestCredential);
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
## Playwright
|
|
120
|
+
## Automated Testing with Playwright
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
This library is intended for use in Passkeys E2E testing, particularly with Playwright. For Playwright testing, you can easily use the WebAuthn API emulator with the utility class `BrowserInjection`. Here's how to use it:
|
|
120
123
|
|
|
121
124
|
```TypeScript
|
|
122
125
|
import WebAuthnEmulator, {
|
|
@@ -168,47 +171,47 @@ async function startWebAuthnEmulator(page: Page, origin: string, debug = false,
|
|
|
168
171
|
|
|
169
172
|
test.describe("Passkeys Tests", { tag: ["@daily"] }, () => {
|
|
170
173
|
test("Passkeys login test", async ({ page }) => {
|
|
171
|
-
//
|
|
172
|
-
//
|
|
174
|
+
// Exposed functions defined once per page initially
|
|
175
|
+
// Related origins can be specified as needed
|
|
173
176
|
const relatedOrigins = ["https://sub.example.com", "https://alt.example.com"];
|
|
174
177
|
await startWebAuthnEmulator(page, env, true, relatedOrigins);
|
|
175
178
|
await page.goto("https://example.com/passkeys/login");
|
|
176
179
|
|
|
177
|
-
// Passkeys
|
|
178
|
-
//
|
|
180
|
+
// Start hooking Passkeys WebAuthn API
|
|
181
|
+
// Must be executed after page navigation
|
|
179
182
|
await page.evaluate(BrowserInjection.HookWebAuthnApis);
|
|
180
183
|
});
|
|
181
184
|
});
|
|
182
185
|
```
|
|
183
186
|
|
|
184
|
-
`startWebAuthnEmulator`
|
|
187
|
+
`startWebAuthnEmulator` uses Playwright's `exposeFunction` to inject the `WebAuthnEmulator`'s `createJSON` and `getJSON` methods into the browser context. This makes the `WebAuthnEmulator` class's `get` and `create` APIs available under the `window` object in the Playwright test context.
|
|
185
188
|
|
|
186
|
-
- `window.webAuthnEmulatorGet`: `WebAuthnEmulator.getJSON`
|
|
187
|
-
- `window.webAuthnEmulatorCreate`: `WebAuthnEmulator.createJSON`
|
|
188
|
-
- `window.webAuthnEmulatorSignalUnknownCredential`: `WebAuthnEmulator.signalUnknownCredential`
|
|
189
|
-
- `window.webAuthnEmulatorSignalAllAcceptedCredentials`: `WebAuthnEmulator.signalAllAcceptedCredentials`
|
|
190
|
-
- `window.webAuthnEmulatorSignalCurrentUserDetails`: `WebAuthnEmulator.signalCurrentUserDetails`
|
|
189
|
+
- `window.webAuthnEmulatorGet`: Exposed Function for `WebAuthnEmulator.getJSON`
|
|
190
|
+
- `window.webAuthnEmulatorCreate`: Exposed Function for `WebAuthnEmulator.createJSON`
|
|
191
|
+
- `window.webAuthnEmulatorSignalUnknownCredential`: Exposed Function for `WebAuthnEmulator.signalUnknownCredential`
|
|
192
|
+
- `window.webAuthnEmulatorSignalAllAcceptedCredentials`: Exposed Function for `WebAuthnEmulator.signalAllAcceptedCredentials`
|
|
193
|
+
- `window.webAuthnEmulatorSignalCurrentUserDetails`: Exposed Function for `WebAuthnEmulator.signalCurrentUserDetails`
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
Additionally, the `startWebAuthnEmulator` function supports a `relatedOrigins` parameter. This allows requests from different origins to use the same RP ID. This is useful when using Passkeys in multi-domain environments (such as `example.com` and `sub.example.com`). The value of `relatedOrigins` is the same as the content of `/.well-known/webauthn` hosted on the domain specified by the RP ID.
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
These are defined globally per Page, so they need to be defined only once per Page instance.
|
|
195
198
|
|
|
196
|
-
|
|
199
|
+
Next, to hook WebAuthn APIs like `navigator.credentials.get`, evaluate `BrowserInjection.HookWebAuthnApis` in the test context.
|
|
197
200
|
|
|
198
201
|
```TypeScript
|
|
199
202
|
await page.evaluate(BrowserInjection.HookWebAuthnApis);
|
|
200
203
|
```
|
|
201
204
|
|
|
202
|
-
`BrowserInjection.HookWebAuthnApis`
|
|
205
|
+
`BrowserInjection.HookWebAuthnApis` is a serialized string of a JavaScript function that, when evaluated, performs the following operations:
|
|
203
206
|
|
|
204
|
-
- `navigator.credentials.get`
|
|
205
|
-
- `navigator.credentials.create`
|
|
206
|
-
- `PublicKeyCredential.signalUnknownCredential`
|
|
207
|
+
- Overrides the definition of `navigator.credentials.get` to call `window.webAuthnEmulatorGet`
|
|
208
|
+
- Overrides the definition of `navigator.credentials.create` to call `window.webAuthnEmulatorCreate`
|
|
209
|
+
- Adds the definition of `PublicKeyCredential.signalUnknownCredential` to call `window.webAuthnEmulatorSignalUnknownCredential`
|
|
207
210
|
|
|
208
|
-
|
|
211
|
+
This ensures that the `WebAuthnEmulator` methods defined earlier with `exposeFunction` are executed when `navigator.credentials.get` and `navigator.credentials.create` are called. These processes include serialization and deserialization of data for communication between the test context and Playwright context.
|
|
209
212
|
|
|
210
|
-
##
|
|
213
|
+
## License
|
|
211
214
|
|
|
212
215
|
MIT License
|
|
213
216
|
|
|
214
|
-
Copyright (C) 2024 Nikkei Inc.
|
|
217
|
+
Copyright (C) 2024 Nikkei Inc.
|
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const globals_1 = require("@jest/globals");
|
|
7
7
|
const src_1 = require("../../src");
|
|
8
8
|
const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
9
|
-
const ctap_model_2 = require("../../src/authenticator/ctap-model");
|
|
10
9
|
const encode_utils_1 = __importDefault(require("../../src/libs/encode-utils"));
|
|
11
10
|
const credentials_memory_repository_1 = require("../../src/repository/credentials-memory-repository");
|
|
12
11
|
(0, globals_1.describe)("Authenticator Emulator Exceptional Test", () => {
|
|
@@ -18,7 +17,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
18
17
|
const authenticator = new src_1.AuthenticatorEmulator();
|
|
19
18
|
(0, globals_1.expect)(() => {
|
|
20
19
|
authenticator.command(testRequest);
|
|
21
|
-
}).
|
|
20
|
+
}).toThrow("CTAP error: CTAP1_ERR_INVALID_COMMAND (1)");
|
|
22
21
|
});
|
|
23
22
|
});
|
|
24
23
|
(0, globals_1.describe)("Authenticator Credential Management Tests", () => {
|
|
@@ -69,7 +68,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
69
68
|
(0, globals_1.test)("should return the total number of credentials for a specific RP", () => {
|
|
70
69
|
// Create request for enumerateCredentialsBegin
|
|
71
70
|
const request = {
|
|
72
|
-
subCommand:
|
|
71
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
73
72
|
subCommandParams: {
|
|
74
73
|
rpId: rpId1,
|
|
75
74
|
},
|
|
@@ -82,17 +81,17 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
82
81
|
(0, globals_1.test)("should throw an error if rpId is not provided", () => {
|
|
83
82
|
// Create request without rpId
|
|
84
83
|
const request = {
|
|
85
|
-
subCommand:
|
|
84
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
86
85
|
};
|
|
87
86
|
// Execute the command and expect an error
|
|
88
87
|
(0, globals_1.expect)(() => {
|
|
89
88
|
authenticator.authenticatorCredentialManagement(request);
|
|
90
|
-
}).toThrow(new
|
|
89
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
91
90
|
});
|
|
92
91
|
(0, globals_1.test)("should return 0 credentials for an RP with no credentials", () => {
|
|
93
92
|
// Create request for a non-existent RP
|
|
94
93
|
const request = {
|
|
95
|
-
subCommand:
|
|
94
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
96
95
|
subCommandParams: {
|
|
97
96
|
rpId: "nonexistent.com",
|
|
98
97
|
},
|
|
@@ -107,7 +106,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
107
106
|
(0, globals_1.test)("should return credentials one by one", () => {
|
|
108
107
|
// First, call enumerateCredentialsBegin
|
|
109
108
|
const beginRequest = {
|
|
110
|
-
subCommand:
|
|
109
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
111
110
|
subCommandParams: {
|
|
112
111
|
rpId: rpId1,
|
|
113
112
|
},
|
|
@@ -115,7 +114,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
115
114
|
authenticator.authenticatorCredentialManagement(beginRequest);
|
|
116
115
|
// Now call enumerateCredentialsGetNextCredential twice to get both credentials
|
|
117
116
|
const getNextRequest = {
|
|
118
|
-
subCommand:
|
|
117
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
|
|
119
118
|
};
|
|
120
119
|
// Get first credential
|
|
121
120
|
const response1 = authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
@@ -134,15 +133,15 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
134
133
|
// Verify we can't get more credentials
|
|
135
134
|
(0, globals_1.expect)(() => {
|
|
136
135
|
authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
137
|
-
}).toThrow(new
|
|
136
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
138
137
|
});
|
|
139
138
|
(0, globals_1.test)("should throw an error if called without calling enumerateCredentialsBegin first", () => {
|
|
140
139
|
const getNextRequest = {
|
|
141
|
-
subCommand:
|
|
140
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
|
|
142
141
|
};
|
|
143
142
|
(0, globals_1.expect)(() => {
|
|
144
143
|
authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
145
|
-
}).toThrow(new
|
|
144
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
146
145
|
});
|
|
147
146
|
});
|
|
148
147
|
(0, globals_1.describe)("updateUserInformation", () => {
|
|
@@ -155,7 +154,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
155
154
|
};
|
|
156
155
|
// Create request for updateUserInformation
|
|
157
156
|
const request = {
|
|
158
|
-
subCommand:
|
|
157
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
|
|
159
158
|
subCommandParams: {
|
|
160
159
|
rpId: rpId1,
|
|
161
160
|
user: updatedUser,
|
|
@@ -165,14 +164,14 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
165
164
|
authenticator.authenticatorCredentialManagement(request);
|
|
166
165
|
// Verify the user information was updated by enumerating credentials
|
|
167
166
|
const beginRequest = {
|
|
168
|
-
subCommand:
|
|
167
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
169
168
|
subCommandParams: {
|
|
170
169
|
rpId: rpId1,
|
|
171
170
|
},
|
|
172
171
|
};
|
|
173
172
|
authenticator.authenticatorCredentialManagement(beginRequest);
|
|
174
173
|
const getNextRequest = {
|
|
175
|
-
subCommand:
|
|
174
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsGetNextCredential,
|
|
176
175
|
};
|
|
177
176
|
// Get credentials and check if user info was updated
|
|
178
177
|
const nextResponse = authenticator.authenticatorCredentialManagement(getNextRequest);
|
|
@@ -185,25 +184,25 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
185
184
|
});
|
|
186
185
|
(0, globals_1.test)("should throw an error if rpId is not provided", () => {
|
|
187
186
|
const request = {
|
|
188
|
-
subCommand:
|
|
187
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
|
|
189
188
|
subCommandParams: {
|
|
190
189
|
user: user1,
|
|
191
190
|
},
|
|
192
191
|
};
|
|
193
192
|
(0, globals_1.expect)(() => {
|
|
194
193
|
authenticator.authenticatorCredentialManagement(request);
|
|
195
|
-
}).toThrow(new
|
|
194
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
196
195
|
});
|
|
197
196
|
(0, globals_1.test)("should throw an error if user is not provided", () => {
|
|
198
197
|
const request = {
|
|
199
|
-
subCommand:
|
|
198
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
|
|
200
199
|
subCommandParams: {
|
|
201
200
|
rpId: rpId1,
|
|
202
201
|
},
|
|
203
202
|
};
|
|
204
203
|
(0, globals_1.expect)(() => {
|
|
205
204
|
authenticator.authenticatorCredentialManagement(request);
|
|
206
|
-
}).toThrow(new
|
|
205
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP1_ERR_INVALID_PARAMETER));
|
|
207
206
|
});
|
|
208
207
|
(0, globals_1.test)("should throw an error if no credentials exist for the user", () => {
|
|
209
208
|
const nonExistentUser = {
|
|
@@ -212,7 +211,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
212
211
|
displayName: "Non Existent User",
|
|
213
212
|
};
|
|
214
213
|
const request = {
|
|
215
|
-
subCommand:
|
|
214
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.updateUserInformation,
|
|
216
215
|
subCommandParams: {
|
|
217
216
|
rpId: rpId1,
|
|
218
217
|
user: nonExistentUser,
|
|
@@ -220,7 +219,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
220
219
|
};
|
|
221
220
|
(0, globals_1.expect)(() => {
|
|
222
221
|
authenticator.authenticatorCredentialManagement(request);
|
|
223
|
-
}).toThrow(new
|
|
222
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NO_CREDENTIALS));
|
|
224
223
|
});
|
|
225
224
|
});
|
|
226
225
|
(0, globals_1.describe)("authenticatorCredentialManagement with stateless authenticator", () => {
|
|
@@ -231,7 +230,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
231
230
|
});
|
|
232
231
|
// Create request for enumerateCredentialsBegin
|
|
233
232
|
const request = {
|
|
234
|
-
subCommand:
|
|
233
|
+
subCommand: ctap_model_1.CREDENTIAL_MANAGEMENT_SUBCOMMAND.enumerateCredentialsBegin,
|
|
235
234
|
subCommandParams: {
|
|
236
235
|
rpId: rpId1,
|
|
237
236
|
},
|
|
@@ -239,7 +238,7 @@ const credentials_memory_repository_1 = require("../../src/repository/credential
|
|
|
239
238
|
// Execute the command and expect an error
|
|
240
239
|
(0, globals_1.expect)(() => {
|
|
241
240
|
statelessAuthenticator.authenticatorCredentialManagement(request);
|
|
242
|
-
}).toThrow(new
|
|
241
|
+
}).toThrow(new ctap_model_1.AuthenticationEmulatorError(ctap_model_1.CTAP_STATUS_CODE.CTAP2_ERR_NOT_ALLOWED));
|
|
243
242
|
});
|
|
244
243
|
});
|
|
245
244
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const globals_1 = require("@jest/globals");
|
|
4
|
-
const globals_2 = require("@jest/globals");
|
|
5
4
|
const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
6
5
|
(0, globals_1.describe)("CTAP Model Test", () => {
|
|
7
6
|
(0, globals_1.test)("MakeCredentialRequest CTAP Object pack and unpack", async () => {
|
|
@@ -19,8 +18,8 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
|
19
18
|
const packed = (0, ctap_model_1.packMakeCredentialRequest)(testRequest);
|
|
20
19
|
const unpacked = (0, ctap_model_1.unpackRequest)(packed);
|
|
21
20
|
const rePacked = (0, ctap_model_1.packMakeCredentialRequest)(unpacked.request);
|
|
22
|
-
(0,
|
|
23
|
-
(0,
|
|
21
|
+
(0, globals_1.expect)(packed).toEqual(rePacked);
|
|
22
|
+
(0, globals_1.expect)(unpacked.request).toEqual(testRequest);
|
|
24
23
|
});
|
|
25
24
|
(0, globals_1.test)("GetAssertionRequest CTAP Object pack and unpack", async () => {
|
|
26
25
|
const testRequest = {
|
|
@@ -35,8 +34,8 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
|
35
34
|
const packed = (0, ctap_model_1.packGetAssertionRequest)(testRequest);
|
|
36
35
|
const unpacked = (0, ctap_model_1.unpackRequest)(packed);
|
|
37
36
|
const rePacked = (0, ctap_model_1.packGetAssertionRequest)(unpacked.request);
|
|
38
|
-
(0,
|
|
39
|
-
(0,
|
|
37
|
+
(0, globals_1.expect)(packed).toEqual(rePacked);
|
|
38
|
+
(0, globals_1.expect)(unpacked.request).toEqual(testRequest);
|
|
40
39
|
});
|
|
41
40
|
(0, globals_1.test)("MakeCredentialResponse CTAP Object pack and unpack", async () => {
|
|
42
41
|
const testResponse = {
|
|
@@ -47,8 +46,8 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
|
47
46
|
const packed = (0, ctap_model_1.packMakeCredentialResponse)(testResponse);
|
|
48
47
|
const unpacked = (0, ctap_model_1.unpackMakeCredentialResponse)(packed);
|
|
49
48
|
const rePacked = (0, ctap_model_1.packMakeCredentialResponse)(unpacked);
|
|
50
|
-
(0,
|
|
51
|
-
(0,
|
|
49
|
+
(0, globals_1.expect)(packed).toEqual(rePacked);
|
|
50
|
+
(0, globals_1.expect)(unpacked).toEqual(testResponse);
|
|
52
51
|
});
|
|
53
52
|
(0, globals_1.test)("GetAssertionResponse CTAP Object pack and unpack", async () => {
|
|
54
53
|
const testResponse = {
|
|
@@ -60,8 +59,8 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
|
60
59
|
const packed = (0, ctap_model_1.packGetAssertionResponse)(testResponse);
|
|
61
60
|
const unpacked = (0, ctap_model_1.unpackGetAssertionResponse)(packed);
|
|
62
61
|
const rePacked = (0, ctap_model_1.packGetAssertionResponse)(unpacked);
|
|
63
|
-
(0,
|
|
64
|
-
(0,
|
|
62
|
+
(0, globals_1.expect)(packed).toEqual(rePacked);
|
|
63
|
+
(0, globals_1.expect)(unpacked).toEqual(testResponse);
|
|
65
64
|
});
|
|
66
65
|
(0, globals_1.test)("GetInfoResponse CTAP Object pack and unpack", async () => {
|
|
67
66
|
const testResponse = {
|
|
@@ -75,21 +74,21 @@ const ctap_model_1 = require("../../src/authenticator/ctap-model");
|
|
|
75
74
|
const packed = (0, ctap_model_1.packGetInfoResponse)(testResponse);
|
|
76
75
|
const unpacked = (0, ctap_model_1.unpackGetInfoResponse)(packed);
|
|
77
76
|
const rePacked = (0, ctap_model_1.packGetInfoResponse)(unpacked);
|
|
78
|
-
(0,
|
|
79
|
-
(0,
|
|
77
|
+
(0, globals_1.expect)(packed).toEqual(rePacked);
|
|
78
|
+
(0, globals_1.expect)(unpacked).toEqual(testResponse);
|
|
80
79
|
});
|
|
81
80
|
(0, globals_1.test)("Illegal data parse test _ failed to unpack", async () => {
|
|
82
81
|
const response = {};
|
|
83
82
|
const expected = "CTAP error: CTAP2_ERR_INVALID_CBOR (18)";
|
|
84
|
-
(0,
|
|
85
|
-
(0,
|
|
86
|
-
(0,
|
|
83
|
+
(0, globals_1.expect)(() => (0, ctap_model_1.unpackMakeCredentialResponse)(response)).toThrow(expected);
|
|
84
|
+
(0, globals_1.expect)(() => (0, ctap_model_1.unpackGetAssertionResponse)(response)).toThrow(expected);
|
|
85
|
+
(0, globals_1.expect)(() => (0, ctap_model_1.unpackGetInfoResponse)(response)).toThrow(expected);
|
|
87
86
|
});
|
|
88
87
|
(0, globals_1.test)("Illegal cbor data parse test _ failed to unpack", async () => {
|
|
89
88
|
const request = {
|
|
90
89
|
command: ctap_model_1.CTAP_COMMAND.authenticatorMakeCredential,
|
|
91
90
|
data: new Uint8Array([1, 2, 3, 4, 5]),
|
|
92
91
|
};
|
|
93
|
-
(0,
|
|
92
|
+
(0, globals_1.expect)(() => (0, ctap_model_1.unpackRequest)(request)).toThrow("CTAP error: CTAP2_ERR_INVALID_CBOR (18)");
|
|
94
93
|
});
|
|
95
94
|
});
|
|
@@ -343,7 +343,7 @@ const webauthn_test_server_1 = require("./webauthn-test-server");
|
|
|
343
343
|
const emulator = new src_1.default();
|
|
344
344
|
// Origin is different from the RP ID
|
|
345
345
|
const testServer = new webauthn_test_server_1.WebAuthnTestServer("https://test-rp.com", "test-rp2.com");
|
|
346
|
-
const relatedOrigins = ["https://test-
|
|
346
|
+
const relatedOrigins = ["https://test-rp-a.com", "https://test-rp-b.com", "https://test-rp.com"];
|
|
347
347
|
// Register a credential
|
|
348
348
|
const regOptions = await testServer.getRegistrationOptions(user);
|
|
349
349
|
const credential = emulator.createJSON(testServer.origin, regOptions, relatedOrigins);
|
|
@@ -345,7 +345,7 @@ function getAssertion(rpIdHash, clientDataHash, newSignCounter, credential, inte
|
|
|
345
345
|
},
|
|
346
346
|
signCount: newSignCounter,
|
|
347
347
|
};
|
|
348
|
-
const payload =
|
|
348
|
+
const payload = [];
|
|
349
349
|
payload.push(...(0, webauthn_model_1.packAuthenticatorData)(authenticatorData));
|
|
350
350
|
payload.push(...clientDataHash);
|
|
351
351
|
const privateKey = (0, node_crypto_1.createPrivateKey)({
|
|
@@ -128,15 +128,15 @@ function packMakeCredentialRequest(request) {
|
|
|
128
128
|
return {
|
|
129
129
|
command: CTAP_COMMAND.authenticatorMakeCredential,
|
|
130
130
|
data: encode_utils_1.default.encodeCbor({
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
"1": request.clientDataHash,
|
|
132
|
+
"2": request.rp,
|
|
133
|
+
"3": request.user,
|
|
134
|
+
"4": request.pubKeyCredParams,
|
|
135
|
+
"5": request.excludeList,
|
|
136
|
+
"6": request.extensions,
|
|
137
|
+
"7": request.options,
|
|
138
|
+
"8": request.pinAuth,
|
|
139
|
+
"9": request.pinProtocol,
|
|
140
140
|
}),
|
|
141
141
|
};
|
|
142
142
|
}
|
|
@@ -144,13 +144,13 @@ function packGetAssertionRequest(request) {
|
|
|
144
144
|
return {
|
|
145
145
|
command: CTAP_COMMAND.authenticatorGetAssertion,
|
|
146
146
|
data: encode_utils_1.default.encodeCbor({
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
"1": request.rpId,
|
|
148
|
+
"2": request.clientDataHash,
|
|
149
|
+
"3": request.allowList,
|
|
150
|
+
"4": request.extensions,
|
|
151
|
+
"5": request.options,
|
|
152
|
+
"6": request.pinAuth,
|
|
153
|
+
"7": request.pinProtocol,
|
|
154
154
|
}),
|
|
155
155
|
};
|
|
156
156
|
}
|
|
@@ -158,16 +158,16 @@ function packCredentialManagementRequest(request) {
|
|
|
158
158
|
return {
|
|
159
159
|
command: CTAP_COMMAND.authenticatorCredentialManagement,
|
|
160
160
|
data: encode_utils_1.default.encodeCbor({
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
"1": request.subCommand,
|
|
162
|
+
"2": request.subCommandParams
|
|
163
163
|
? {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
"1": request.subCommandParams.credentialId,
|
|
165
|
+
"2": request.subCommandParams.rpId,
|
|
166
|
+
"3": request.subCommandParams.user,
|
|
167
167
|
}
|
|
168
168
|
: undefined,
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
"3": request.pinUvAuthProtocol,
|
|
170
|
+
"4": request.pinUvAuthParam,
|
|
171
171
|
}),
|
|
172
172
|
};
|
|
173
173
|
}
|
|
@@ -211,9 +211,9 @@ function packMakeCredentialResponse(response) {
|
|
|
211
211
|
return {
|
|
212
212
|
status: CTAP_STATUS_CODE.CTAP2_OK,
|
|
213
213
|
data: encode_utils_1.default.encodeCbor({
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
"1": response.fmt,
|
|
215
|
+
"2": response.authData,
|
|
216
|
+
"3": response.attStmt,
|
|
217
217
|
}),
|
|
218
218
|
};
|
|
219
219
|
}
|
|
@@ -236,11 +236,11 @@ function packGetAssertionResponse(response) {
|
|
|
236
236
|
return {
|
|
237
237
|
status: CTAP_STATUS_CODE.CTAP2_OK,
|
|
238
238
|
data: encode_utils_1.default.encodeCbor({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
"1": response.credential,
|
|
240
|
+
"2": response.authData,
|
|
241
|
+
"3": response.signature,
|
|
242
|
+
"4": response.user,
|
|
243
|
+
"5": response.numberOfCredentials,
|
|
244
244
|
}),
|
|
245
245
|
};
|
|
246
246
|
}
|
|
@@ -264,12 +264,12 @@ function packGetInfoResponse(response) {
|
|
|
264
264
|
return {
|
|
265
265
|
status: CTAP_STATUS_CODE.CTAP2_OK,
|
|
266
266
|
data: encode_utils_1.default.encodeCbor({
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
"1": response.versions,
|
|
268
|
+
"2": response.extensions,
|
|
269
|
+
"3": response.aaguid,
|
|
270
|
+
"4": response.options,
|
|
271
|
+
"5": response.maxMsgSize,
|
|
272
|
+
"6": response.pinProtocols,
|
|
273
273
|
}),
|
|
274
274
|
};
|
|
275
275
|
}
|
|
@@ -277,17 +277,17 @@ function packCredentialManagementResponse(response) {
|
|
|
277
277
|
return {
|
|
278
278
|
status: CTAP_STATUS_CODE.CTAP2_OK,
|
|
279
279
|
data: encode_utils_1.default.encodeCbor({
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
280
|
+
"1": response.existingResidentCredentialsCount,
|
|
281
|
+
"2": response.maxPossibleRemainingResidentCredentialsCount,
|
|
282
|
+
"3": response.rp,
|
|
283
|
+
"4": response.rpIDHash,
|
|
284
|
+
"5": response.totalRPs,
|
|
285
|
+
"6": response.user,
|
|
286
|
+
"7": response.credentialID,
|
|
287
|
+
"8": response.publicKey,
|
|
288
|
+
"9": response.totalCredentials,
|
|
289
|
+
"10": response.credProtect,
|
|
290
|
+
"11": response.largeBlobKey,
|
|
291
291
|
}),
|
|
292
292
|
};
|
|
293
293
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { WebAuthnEmulator } from "./webauthn/webauthn-emulator";
|
|
2
2
|
export default WebAuthnEmulator;
|
|
3
|
-
export * from "./webauthn/webauthn-model-json";
|
|
4
|
-
export * from "./webauthn/webauthn-model";
|
|
5
|
-
export * from "./webauthn/webauthn-emulator";
|
|
6
|
-
export * from "./webauthn/cose-key";
|
|
7
3
|
export * from "./authenticator/authenticator-emulator";
|
|
8
4
|
export * from "./authenticator/ctap-model";
|
|
9
|
-
export * from "./repository/credentials-repository";
|
|
10
5
|
export * from "./repository/credentials-file-repository";
|
|
11
6
|
export * from "./repository/credentials-memory-repository";
|
|
7
|
+
export * from "./repository/credentials-repository";
|
|
8
|
+
export * from "./webauthn/cose-key";
|
|
9
|
+
export * from "./webauthn/webauthn-emulator";
|
|
10
|
+
export * from "./webauthn/webauthn-model";
|
|
11
|
+
export * from "./webauthn/webauthn-model-json";
|
|
12
12
|
export declare const BrowserInjection: {
|
|
13
13
|
WebAuthnEmulatorGet: string;
|
|
14
14
|
WebAuthnEmulatorCreate: string;
|
package/dist/src/index.js
CHANGED
|
@@ -18,15 +18,15 @@ exports.BrowserInjection = void 0;
|
|
|
18
18
|
const browser_injection_1 = require("./test-utils/browser-injection");
|
|
19
19
|
const webauthn_emulator_1 = require("./webauthn/webauthn-emulator");
|
|
20
20
|
exports.default = webauthn_emulator_1.WebAuthnEmulator;
|
|
21
|
-
__exportStar(require("./webauthn/webauthn-model-json"), exports);
|
|
22
|
-
__exportStar(require("./webauthn/webauthn-model"), exports);
|
|
23
|
-
__exportStar(require("./webauthn/webauthn-emulator"), exports);
|
|
24
|
-
__exportStar(require("./webauthn/cose-key"), exports);
|
|
25
21
|
__exportStar(require("./authenticator/authenticator-emulator"), exports);
|
|
26
22
|
__exportStar(require("./authenticator/ctap-model"), exports);
|
|
27
|
-
__exportStar(require("./repository/credentials-repository"), exports);
|
|
28
23
|
__exportStar(require("./repository/credentials-file-repository"), exports);
|
|
29
24
|
__exportStar(require("./repository/credentials-memory-repository"), exports);
|
|
25
|
+
__exportStar(require("./repository/credentials-repository"), exports);
|
|
26
|
+
__exportStar(require("./webauthn/cose-key"), exports);
|
|
27
|
+
__exportStar(require("./webauthn/webauthn-emulator"), exports);
|
|
28
|
+
__exportStar(require("./webauthn/webauthn-model"), exports);
|
|
29
|
+
__exportStar(require("./webauthn/webauthn-model-json"), exports);
|
|
30
30
|
exports.BrowserInjection = {
|
|
31
31
|
WebAuthnEmulatorGet: browser_injection_1.WebAuthnEmulatorGet,
|
|
32
32
|
WebAuthnEmulatorCreate: browser_injection_1.WebAuthnEmulatorCreate,
|
|
@@ -4,12 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.WebAuthnEmulator = exports.InvalidRpIdError = exports.NoPublicKeyError = exports.WebAuthnEmulatorError = void 0;
|
|
7
|
-
const authenticator_emulator_1 = require("../authenticator/authenticator-emulator");
|
|
8
|
-
const encode_utils_1 = __importDefault(require("../libs/encode-utils"));
|
|
9
|
-
const webauthn_model_json_1 = require("./webauthn-model-json");
|
|
10
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const authenticator_emulator_1 = require("../authenticator/authenticator-emulator");
|
|
11
9
|
const ctap_model_1 = require("../authenticator/ctap-model");
|
|
10
|
+
const encode_utils_1 = __importDefault(require("../libs/encode-utils"));
|
|
12
11
|
const webauthn_model_1 = require("./webauthn-model");
|
|
12
|
+
const webauthn_model_json_1 = require("./webauthn-model-json");
|
|
13
13
|
class WebAuthnEmulatorError extends Error {
|
|
14
14
|
}
|
|
15
15
|
exports.WebAuthnEmulatorError = WebAuthnEmulatorError;
|
|
@@ -184,7 +184,7 @@ class WebAuthnEmulator {
|
|
|
184
184
|
};
|
|
185
185
|
const clientDataJSON = JSON.stringify(clientData);
|
|
186
186
|
const authenticatorRequest = (0, ctap_model_1.packMakeCredentialRequest)({
|
|
187
|
-
clientDataHash: (0, node_crypto_1.createHash)("sha256").update(clientDataJSON).digest(),
|
|
187
|
+
clientDataHash: encode_utils_1.default.bufferSourceToUint8Array((0, node_crypto_1.createHash)("sha256").update(clientDataJSON).digest()),
|
|
188
188
|
rp: { name: options.publicKey.rp.name, id: rpId.value },
|
|
189
189
|
user: options.publicKey.user,
|
|
190
190
|
pubKeyCredParams: options.publicKey.pubKeyCredParams,
|
|
@@ -26,17 +26,16 @@ class RpId {
|
|
|
26
26
|
}
|
|
27
27
|
/** @see https://www.w3.org/TR/webauthn-3/#sctn-validating-origin */
|
|
28
28
|
validate(origin, relatedOrigins = []) {
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const parsedRpId = (0, tldts_1.parse)(this.value, { validHosts: ["localhost"] });
|
|
29
|
+
const parsedOrigin = (0, tldts_1.parse)(origin, { validHosts: ["localhost"] });
|
|
30
|
+
const expectedOrigins = [this.value, ...relatedOrigins];
|
|
32
31
|
// Check each origin using standard validation
|
|
33
|
-
for (const
|
|
34
|
-
const
|
|
32
|
+
for (const expectedOrigin of expectedOrigins) {
|
|
33
|
+
const parsedExpectedOrigin = (0, tldts_1.parse)(expectedOrigin, { validHosts: ["localhost"] });
|
|
35
34
|
const isValid = Boolean(parsedOrigin.domain &&
|
|
36
35
|
parsedOrigin.subdomain !== null &&
|
|
37
|
-
|
|
38
|
-
parsedOrigin.domain ===
|
|
39
|
-
parsedOrigin.subdomain.endsWith(
|
|
36
|
+
parsedExpectedOrigin.subdomain !== null &&
|
|
37
|
+
parsedOrigin.domain === parsedExpectedOrigin.domain &&
|
|
38
|
+
parsedOrigin.subdomain.endsWith(parsedExpectedOrigin.subdomain));
|
|
40
39
|
if (isValid) {
|
|
41
40
|
return true;
|
|
42
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nid-webauthn-emulator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"test",
|
|
6
6
|
"e2e",
|
|
@@ -15,19 +15,19 @@
|
|
|
15
15
|
"main": "dist/src/index.js",
|
|
16
16
|
"types": "dist/src/index.d.ts",
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@biomejs/biome": "^1.
|
|
19
|
-
"@jest/globals": "^
|
|
20
|
-
"@simplewebauthn/server": "^13.1.
|
|
21
|
-
"@tsconfig/recommended": "^1.0.
|
|
22
|
-
"@types/node": "^
|
|
23
|
-
"jest": "^
|
|
24
|
-
"ts-jest": "^29.
|
|
18
|
+
"@biomejs/biome": "^2.1.2",
|
|
19
|
+
"@jest/globals": "^30.0.4",
|
|
20
|
+
"@simplewebauthn/server": "^13.1.2",
|
|
21
|
+
"@tsconfig/recommended": "^1.0.10",
|
|
22
|
+
"@types/node": "^24.2.0",
|
|
23
|
+
"jest": "^30.0.5",
|
|
24
|
+
"ts-jest": "^29.4.1",
|
|
25
25
|
"ts-node": "^10.9.2",
|
|
26
26
|
"typescript": "^5.8.3"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"cbor": "^10.0.
|
|
30
|
-
"tldts": "^7.0.
|
|
29
|
+
"cbor": "^10.0.10",
|
|
30
|
+
"tldts": "^7.0.10"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"check": "biome check .",
|