@@ -10,6 +10,7 @@ class CyberPanelWebAuthn {
1010 this . apiEndpoints = {
1111 registrationStart : '/webauthn/registration/start/' ,
1212 registrationComplete : '/webauthn/registration/complete/' ,
13+ authenticationOptions : '/webauthn/authentication/options/' ,
1314 authenticationStart : '/webauthn/authentication/start/' ,
1415 authenticationComplete : '/webauthn/authentication/complete/' ,
1516 credentialsList : '/webauthn/credentials/' ,
@@ -60,18 +61,10 @@ class CyberPanelWebAuthn {
6061 addLoginButtons ( ) {
6162 const loginForm = document . querySelector ( '#loginForm' ) ;
6263 if ( ! loginForm ) return ;
63-
64- // Add WebAuthn login button
65- const webauthnButton = document . createElement ( 'button' ) ;
66- webauthnButton . type = 'button' ;
67- webauthnButton . className = 'btn btn-primary btn-block' ;
68- webauthnButton . innerHTML = '<i class="fas fa-fingerprint"></i> Login with Passkey' ;
69- webauthnButton . onclick = ( ) => this . startPasswordlessLogin ( ) ;
70-
71- // Insert after password field
72- const passwordField = loginForm . querySelector ( 'input[type="password"]' ) ;
73- if ( passwordField ) {
74- passwordField . parentNode . insertBefore ( webauthnButton , passwordField . parentNode . nextSibling ) ;
64+ const existingBtn = document . getElementById ( 'webauthn-login-btn' ) ;
65+ if ( existingBtn && ! existingBtn . dataset . bound ) {
66+ existingBtn . dataset . bound = '1' ;
67+ existingBtn . onclick = ( ) => this . startPasskeyFirstLogin ( ) ;
7568 }
7669 }
7770
@@ -80,34 +73,90 @@ class CyberPanelWebAuthn {
8073 // Implementation depends on the specific UI structure
8174 }
8275
76+ arrayBufferToBase64url ( buffer ) {
77+ const bytes = new Uint8Array ( buffer ) ;
78+ let binary = '' ;
79+ for ( let i = 0 ; i < bytes . byteLength ; i ++ ) binary += String . fromCharCode ( bytes [ i ] ) ;
80+ return btoa ( binary ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = + $ / , '' ) ;
81+ }
82+
83+ base64urlToArrayBuffer ( str ) {
84+ let base64 = str . replace ( / - / g, '+' ) . replace ( / _ / g, '/' ) ;
85+ const pad = ( 4 - ( base64 . length % 4 ) ) % 4 ;
86+ for ( let i = 0 ; i < pad ; i ++ ) base64 += '=' ;
87+ const binary = atob ( base64 ) ;
88+ const bytes = new Uint8Array ( binary . length ) ;
89+ for ( let i = 0 ; i < binary . length ; i ++ ) bytes [ i ] = binary . charCodeAt ( i ) ;
90+ return bytes . buffer ;
91+ }
92+
93+ async startPasskeyFirstLogin ( ) {
94+ try {
95+ this . showLoading ( 'Signing in with passkey...' ) ;
96+ const optsUrl = this . apiEndpoints . authenticationOptions + '?return=' + encodeURIComponent ( window . location . pathname || '/' ) ;
97+ const optsResponse = await fetch ( optsUrl , { method : 'GET' , credentials : 'same-origin' } ) ;
98+ const optsData = await optsResponse . json ( ) ;
99+ if ( ! optsData . publicKey ) {
100+ throw new Error ( optsData . error || 'Failed to get options' ) ;
101+ }
102+ const publicKey = optsData . publicKey ;
103+ publicKey . challenge = this . base64urlToArrayBuffer ( publicKey . challenge ) ;
104+ if ( publicKey . allowCredentials && publicKey . allowCredentials . length ) {
105+ publicKey . allowCredentials = publicKey . allowCredentials . map ( function ( c ) {
106+ return {
107+ type : c . type || 'public-key' ,
108+ id : typeof c . id === 'string' ? this . base64urlToArrayBuffer ( c . id ) : c . id ,
109+ transports : c . transports
110+ } ;
111+ } . bind ( this ) ) ;
112+ }
113+ const credential = await navigator . credentials . get ( { publicKey } ) ;
114+ if ( ! credential ) throw new Error ( 'No credential' ) ;
115+ const credentialJson = {
116+ id : credential . id ,
117+ rawId : this . arrayBufferToBase64url ( credential . rawId ) ,
118+ type : credential . type ,
119+ response : {
120+ clientDataJSON : this . arrayBufferToBase64url ( credential . response . clientDataJSON ) ,
121+ authenticatorData : this . arrayBufferToBase64url ( credential . response . authenticatorData ) ,
122+ signature : this . arrayBufferToBase64url ( credential . response . signature ) ,
123+ userHandle : credential . response . userHandle ? this . arrayBufferToBase64url ( credential . response . userHandle ) : null
124+ }
125+ } ;
126+ const authResponse = await this . makeRequest ( 'POST' , this . apiEndpoints . authenticationComplete , {
127+ credential : credentialJson
128+ } ) ;
129+ if ( authResponse . success && authResponse . redirect ) {
130+ window . location . href = authResponse . redirect ;
131+ return ;
132+ }
133+ throw new Error ( authResponse . error || 'Verification failed' ) ;
134+ } catch ( error ) {
135+ if ( error . name === 'NotAllowedError' || ( error . message && ( error . message . indexOf ( 'cancel' ) !== - 1 || error . message . indexOf ( 'timed out' ) !== - 1 ) ) ) {
136+ this . hideLoading ( ) ;
137+ return ;
138+ }
139+ console . error ( 'WebAuthn passkey-first error:' , error ) ;
140+ this . showError ( error . message || 'Passkey sign-in failed' ) ;
141+ } finally {
142+ this . hideLoading ( ) ;
143+ }
144+ }
145+
83146 async startPasswordlessLogin ( ) {
84147 try {
85148 const username = document . querySelector ( 'input[name="username"]' ) . value ;
86149 if ( ! username ) {
87150 this . showError ( 'Please enter your username first' ) ;
88151 return ;
89152 }
90-
91153 this . showLoading ( 'Starting passkey authentication...' ) ;
92-
93- // Get authentication challenge
94- const challengeResponse = await this . makeRequest ( 'POST' , this . apiEndpoints . authenticationStart , {
95- username : username
96- } ) ;
97-
154+ const challengeResponse = await this . makeRequest ( 'POST' , this . apiEndpoints . authenticationStart , { username : username } ) ;
98155 if ( ! challengeResponse . success ) {
99156 throw new Error ( challengeResponse . error || 'Failed to start authentication' ) ;
100157 }
101-
102- // Convert challenge to proper format
103158 const challenge = this . convertChallenge ( challengeResponse . challenge ) ;
104-
105- // Get credential
106- const credential = await navigator . credentials . get ( {
107- publicKey : challenge
108- } ) ;
109-
110- // Complete authentication
159+ const credential = await navigator . credentials . get ( { publicKey : challenge } ) ;
111160 const authResponse = await this . makeRequest ( 'POST' , this . apiEndpoints . authenticationComplete , {
112161 challenge_id : challengeResponse . challenge_id ,
113162 credential : {
@@ -117,19 +166,14 @@ class CyberPanelWebAuthn {
117166 client_data_json : this . arrayBufferToBase64 ( credential . response . clientDataJSON ) ,
118167 authenticator_data : this . arrayBufferToBase64 ( credential . response . authenticatorData ) ,
119168 signature : this . arrayBufferToBase64 ( credential . response . signature ) ,
120- user_handle : credential . response . userHandle ?
121- this . arrayBufferToBase64 ( credential . response . userHandle ) : null
169+ user_handle : credential . response . userHandle ? this . arrayBufferToBase64 ( credential . response . userHandle ) : null
122170 } ) ;
123-
124171 if ( authResponse . success ) {
125172 this . showSuccess ( 'Authentication successful! Redirecting...' ) ;
126- setTimeout ( ( ) => {
127- window . location . href = '/' ;
128- } , 1000 ) ;
173+ setTimeout ( ( ) => { window . location . href = authResponse . redirect || '/' ; } , 1000 ) ;
129174 } else {
130175 throw new Error ( authResponse . error || 'Authentication failed' ) ;
131176 }
132-
133177 } catch ( error ) {
134178 console . error ( 'WebAuthn authentication error:' , error ) ;
135179 this . showError ( error . message || 'Authentication failed' ) ;
@@ -138,9 +182,10 @@ class CyberPanelWebAuthn {
138182 }
139183 }
140184
141- async registerPasskey ( username , credentialName = '' ) {
185+ async registerPasskey ( username , credentialName = '' , options = { } ) {
186+ const silent = options && options . silent === true ;
142187 try {
143- this . showLoading ( 'Starting passkey registration...' ) ;
188+ if ( ! silent ) this . showLoading ( 'Starting passkey registration...' ) ;
144189
145190 // Get registration challenge
146191 const challengeResponse = await this . makeRequest ( 'POST' , this . apiEndpoints . registrationStart , {
@@ -172,18 +217,18 @@ class CyberPanelWebAuthn {
172217 } ) ;
173218
174219 if ( regResponse . success ) {
175- this . showSuccess ( 'Passkey registered successfully!' ) ;
220+ if ( ! silent ) this . showSuccess ( 'Passkey registered successfully!' ) ;
176221 return regResponse ;
177222 } else {
178223 throw new Error ( regResponse . error || 'Registration failed' ) ;
179224 }
180225
181226 } catch ( error ) {
182227 console . error ( 'WebAuthn registration error:' , error ) ;
183- this . showError ( error . message || 'Registration failed' ) ;
228+ if ( ! silent ) this . showError ( error . message || 'Registration failed' ) ;
184229 throw error ;
185230 } finally {
186- this . hideLoading ( ) ;
231+ if ( ! silent ) this . hideLoading ( ) ;
187232 }
188233 }
189234
@@ -265,23 +310,25 @@ class CyberPanelWebAuthn {
265310 }
266311
267312 convertChallenge ( challenge ) {
268- // Convert base64 challenge to ArrayBuffer
269- const challengeBytes = this . base64ToArrayBuffer ( challenge . challenge ) ;
270-
313+ const ch = challenge . challenge ;
314+ const challengeBytes = ( typeof ch === 'string' && ( ch . indexOf ( '-' ) !== - 1 || ch . indexOf ( '_' ) !== - 1 ) )
315+ ? this . base64urlToArrayBuffer ( ch ) : this . base64ToArrayBuffer ( ch ) ;
316+ const userId = challenge . user && challenge . user . id ;
317+ const userIdBuf = ! userId ? undefined : ( typeof userId === 'string' && ( userId . indexOf ( '-' ) !== - 1 || userId . indexOf ( '_' ) !== - 1 )
318+ ? this . base64urlToArrayBuffer ( userId ) : this . base64ToArrayBuffer ( userId ) ) ;
271319 return {
272320 ...challenge ,
273321 challenge : challengeBytes ,
274- user : {
275- ...challenge . user ,
276- id : this . base64ToArrayBuffer ( challenge . user . id )
277- } ,
322+ user : challenge . user ? { ...challenge . user , id : userIdBuf } : undefined ,
278323 excludeCredentials : challenge . excludeCredentials ?. map ( cred => ( {
279324 ...cred ,
280- id : this . base64ToArrayBuffer ( cred . id )
325+ id : typeof cred . id === 'string' && ( cred . id . indexOf ( '-' ) !== - 1 || cred . id . indexOf ( '_' ) !== - 1 )
326+ ? this . base64urlToArrayBuffer ( cred . id ) : this . base64ToArrayBuffer ( cred . id )
281327 } ) ) || [ ] ,
282328 allowCredentials : challenge . allowCredentials ?. map ( cred => ( {
283329 ...cred ,
284- id : this . base64ToArrayBuffer ( cred . id )
330+ id : typeof cred . id === 'string' && ( cred . id . indexOf ( '-' ) !== - 1 || cred . id . indexOf ( '_' ) !== - 1 )
331+ ? this . base64urlToArrayBuffer ( cred . id ) : this . base64ToArrayBuffer ( cred . id )
285332 } ) ) || [ ]
286333 } ;
287334 }
@@ -383,12 +430,17 @@ class CyberPanelWebAuthn {
383430 }
384431}
385432
386- // Initialize WebAuthn when DOM is loaded
387- document . addEventListener ( 'DOMContentLoaded' , function ( ) {
433+ // Initialize WebAuthn - run now if DOM ready, else on DOMContentLoaded (script often loads after DOM is ready)
434+ function initCyberPanelWebAuthn ( ) {
388435 if ( CyberPanelWebAuthn . isSupported ( ) ) {
389436 window . cyberPanelWebAuthn = new CyberPanelWebAuthn ( ) ;
390437 }
391- } ) ;
438+ }
439+ if ( document . readyState === 'loading' ) {
440+ document . addEventListener ( 'DOMContentLoaded' , initCyberPanelWebAuthn ) ;
441+ } else {
442+ initCyberPanelWebAuthn ( ) ;
443+ }
392444
393445// Export for use in other scripts
394446if ( typeof module !== 'undefined' && module . exports ) {
0 commit comments