@@ -6,6 +6,52 @@ import {
66 browserLogin , setupVirtualAuthenticator , teardownVirtualAuthenticator ,
77} from './helpers' ;
88
9+ // Base64url helpers injected into browser context via page.addScriptTag.
10+ // page.evaluate callbacks run in an isolated browser scope and cannot import
11+ // Node modules, so we inject these once per page instead of duplicating them
12+ // inside every evaluate call.
13+ const B64U_HELPERS = `
14+ function b64u2buf(s) {
15+ var b = s.replace(/-/g, '+').replace(/_/g, '/');
16+ while (b.length % 4) b += '=';
17+ var r = atob(b);
18+ var a = new Uint8Array(r.length);
19+ for (var i = 0; i < r.length; i++) a[i] = r.charCodeAt(i);
20+ return a.buffer;
21+ }
22+ function buf2b64u(b) {
23+ var u = new Uint8Array(b);
24+ var s = '';
25+ for (var i = 0; i < u.length; i++) s += String.fromCharCode(u[i]);
26+ return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
27+ }
28+ ` ;
29+
30+ /** Inject b64u helpers into every frame of the page. */
31+ async function injectB64UHelpers ( page : import ( '@playwright/test' ) . Page ) {
32+ await page . addScriptTag ( { content : B64U_HELPERS } ) ;
33+ for ( const frame of page . frames ( ) ) {
34+ try { await frame . addScriptTag ( { content : B64U_HELPERS } ) ; }
35+ catch { /* frame may not accept scripts */ }
36+ }
37+ }
38+
39+ /** Wait for the NicTool frameset to finish loading after login. */
40+ async function waitForFrameset ( page : import ( '@playwright/test' ) . Page ) {
41+ await page . waitForFunction (
42+ ( ) => window . frames . length > 0 ,
43+ { timeout : 10000 } ,
44+ ) ;
45+ }
46+
47+ /** Wait for the login page to be ready after navigation. */
48+ async function waitForLoginPage ( page : import ( '@playwright/test' ) . Page ) {
49+ await page . waitForSelector (
50+ 'input[name="username"]' ,
51+ { timeout : 10000 } ,
52+ ) ;
53+ }
54+
955// -------------------------------------------------------------------------
1056// W1: CSRF protection on webauthn.cgi
1157// -------------------------------------------------------------------------
@@ -128,30 +174,16 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
128174 const { cdpSession, authenticatorId } = await setupVirtualAuthenticator ( page ) ;
129175 try {
130176 await browserLogin ( page ) ;
131- await page . waitForTimeout ( 2000 ) ;
177+ await waitForFrameset ( page ) ;
132178
133179 const bodyFrame = page . frames ( ) . find ( f =>
134180 f . url ( ) . includes ( 'group.cgi' ) ) ;
135181 expect ( bodyFrame ) . toBeTruthy ( ) ;
136182
183+ await injectB64UHelpers ( page ) ;
184+
137185 // Run registration ceremony inside the frame's browser context
138186 const result = await bodyFrame ! . evaluate ( async ( baseUrl : string ) => {
139- function b64u2buf ( s : string ) : ArrayBuffer {
140- let b = s . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
141- while ( b . length % 4 ) b += '=' ;
142- const r = atob ( b ) ;
143- const a = new Uint8Array ( r . length ) ;
144- for ( let i = 0 ; i < r . length ; i ++ ) a [ i ] = r . charCodeAt ( i ) ;
145- return a . buffer ;
146- }
147- function buf2b64u ( b : ArrayBuffer ) : string {
148- const u = new Uint8Array ( b ) ;
149- let s = '' ;
150- for ( let i = 0 ; i < u . length ; i ++ )
151- s += String . fromCharCode ( u [ i ] ) ;
152- return btoa ( s ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
153- }
154-
155187 const csrf = document . cookie . match ( / N i c T o o l _ c s r f = ( [ ^ ; ] + ) / ) ?. [ 1 ] || '' ;
156188
157189 // Step 1: get options
@@ -234,28 +266,15 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
234266 try {
235267 // --- Phase 1: Register a passkey while logged in ---
236268 await browserLogin ( page ) ;
237- await page . waitForTimeout ( 2000 ) ;
269+ await waitForFrameset ( page ) ;
238270
239271 const bodyFrame = page . frames ( ) . find ( f =>
240272 f . url ( ) . includes ( 'group.cgi' ) ) ;
241273 expect ( bodyFrame ) . toBeTruthy ( ) ;
242274
275+ await injectB64UHelpers ( page ) ;
276+
243277 const regResult = await bodyFrame ! . evaluate ( async ( baseUrl : string ) => {
244- function b64u2buf ( s : string ) : ArrayBuffer {
245- let b = s . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
246- while ( b . length % 4 ) b += '=' ;
247- const r = atob ( b ) ;
248- const a = new Uint8Array ( r . length ) ;
249- for ( let i = 0 ; i < r . length ; i ++ ) a [ i ] = r . charCodeAt ( i ) ;
250- return a . buffer ;
251- }
252- function buf2b64u ( b : ArrayBuffer ) : string {
253- const u = new Uint8Array ( b ) ;
254- let s = '' ;
255- for ( let i = 0 ; i < u . length ; i ++ )
256- s += String . fromCharCode ( u [ i ] ) ;
257- return btoa ( s ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
258- }
259278 const csrf = document . cookie . match ( / N i c T o o l _ c s r f = ( [ ^ ; ] + ) / ) ?. [ 1 ] || '' ;
260279 const optRes = await fetch ( `${ baseUrl } /webauthn.cgi` , {
261280 method : 'POST' ,
@@ -317,28 +336,15 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
317336 await page . goto ( `${ BASE } /index.cgi?logout=1` , {
318337 headers : { Cookie : `NicTool=${ sessionCk } ; NicTool_csrf=${ csrfCk } ` } ,
319338 } ) ;
320- await page . waitForTimeout ( 1000 ) ;
339+ await waitForLoginPage ( page ) ;
321340
322341 // --- Phase 3: Passkey login without username ---
323342 await page . goto ( `${ BASE } /index.cgi` ) ;
324- await page . waitForTimeout ( 500 ) ;
343+ await waitForLoginPage ( page ) ;
344+
345+ await injectB64UHelpers ( page ) ;
325346
326347 const loginResult = await page . evaluate ( async ( baseUrl : string ) => {
327- function b64u2buf ( s : string ) : ArrayBuffer {
328- let b = s . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
329- while ( b . length % 4 ) b += '=' ;
330- const r = atob ( b ) ;
331- const a = new Uint8Array ( r . length ) ;
332- for ( let i = 0 ; i < r . length ; i ++ ) a [ i ] = r . charCodeAt ( i ) ;
333- return a . buffer ;
334- }
335- function buf2b64u ( b : ArrayBuffer ) : string {
336- const u = new Uint8Array ( b ) ;
337- let s = '' ;
338- for ( let i = 0 ; i < u . length ; i ++ )
339- s += String . fromCharCode ( u [ i ] ) ;
340- return btoa ( s ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
341- }
342348 const csrf = document . cookie . match ( / N i c T o o l _ c s r f = ( [ ^ ; ] + ) / ) ?. [ 1 ]
343349 || document . querySelector < HTMLInputElement > ( 'input[name="csrf_token"]' ) ?. value
344350 || '' ;
@@ -413,7 +419,7 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
413419 }
414420 } ) ;
415421
416- test ( 'W6: credential list and rename ' , async ( { playwright } ) => {
422+ test ( 'W6: credential list' , async ( { playwright } ) => {
417423 test . skip ( ! webauthnConfigured , 'WebAuthn not configured on server' ) ;
418424
419425 const { sessionCookie, csrfCookie } = await apiLogin ( playwright ) ;
@@ -431,27 +437,14 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
431437 try {
432438 // Register a passkey
433439 await browserLogin ( page ) ;
434- await page . waitForTimeout ( 2000 ) ;
440+ await waitForFrameset ( page ) ;
435441 const bodyFrame = page . frames ( ) . find ( f =>
436442 f . url ( ) . includes ( 'group.cgi' ) ) ;
437443 expect ( bodyFrame ) . toBeTruthy ( ) ;
438444
445+ await injectB64UHelpers ( page ) ;
446+
439447 const regResult = await bodyFrame ! . evaluate ( async ( baseUrl : string ) => {
440- function b64u2buf ( s : string ) : ArrayBuffer {
441- let b = s . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
442- while ( b . length % 4 ) b += '=' ;
443- const r = atob ( b ) ;
444- const a = new Uint8Array ( r . length ) ;
445- for ( let i = 0 ; i < r . length ; i ++ ) a [ i ] = r . charCodeAt ( i ) ;
446- return a . buffer ;
447- }
448- function buf2b64u ( b : ArrayBuffer ) : string {
449- const u = new Uint8Array ( b ) ;
450- let s = '' ;
451- for ( let i = 0 ; i < u . length ; i ++ )
452- s += String . fromCharCode ( u [ i ] ) ;
453- return btoa ( s ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
454- }
455448 const csrf = document . cookie . match ( / N i c T o o l _ c s r f = ( [ ^ ; ] + ) / ) ?. [ 1 ] || '' ;
456449 const optRes = await fetch ( `${ baseUrl } /webauthn.cgi` , {
457450 method : 'POST' ,
@@ -509,29 +502,16 @@ test.describe('W4-W7: WebAuthn ceremonies', () => {
509502
510503 // Logout
511504 await page . goto ( `${ BASE } /index.cgi?logout=1` ) ;
512- await page . waitForTimeout ( 1000 ) ;
505+ await waitForLoginPage ( page ) ;
513506
514- // Try passkey login — should fail because credential is revoked
507+ // Try passkey login -- should fail because credential is revoked
515508 // (The virtual authenticator still has the key, but server rejects it)
516509 await page . goto ( `${ BASE } /index.cgi` ) ;
517- await page . waitForTimeout ( 500 ) ;
510+ await waitForLoginPage ( page ) ;
511+
512+ await injectB64UHelpers ( page ) ;
518513
519514 const loginResult = await page . evaluate ( async ( baseUrl : string ) => {
520- function b64u2buf ( s : string ) : ArrayBuffer {
521- let b = s . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
522- while ( b . length % 4 ) b += '=' ;
523- const r = atob ( b ) ;
524- const a = new Uint8Array ( r . length ) ;
525- for ( let i = 0 ; i < r . length ; i ++ ) a [ i ] = r . charCodeAt ( i ) ;
526- return a . buffer ;
527- }
528- function buf2b64u ( b : ArrayBuffer ) : string {
529- const u = new Uint8Array ( b ) ;
530- let s = '' ;
531- for ( let i = 0 ; i < u . length ; i ++ )
532- s += String . fromCharCode ( u [ i ] ) ;
533- return btoa ( s ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
534- }
535515 const csrf = document . cookie . match ( / N i c T o o l _ c s r f = ( [ ^ ; ] + ) / ) ?. [ 1 ]
536516 || document . querySelector < HTMLInputElement > ( 'input[name="csrf_token"]' ) ?. value
537517 || '' ;
0 commit comments