@@ -54,7 +54,8 @@ const Login = () => {
5454 localStorage . getItem ( "password" ) || "" ,
5555 )
5656 const [ opt , setOpt ] = createSignal ( "" )
57- const [ useauthn , setuseauthn ] = createSignal ( false )
57+ const [ usePasskey , setUsePasskey ] = createSignal ( false )
58+ const [ passkeyNeedsUsername , setPasskeyNeedsUsername ] = createSignal ( false )
5859 const [ remember , setRemember ] = createStorageSignal ( "remember-pwd" , "false" )
5960 const [ useLdap , setUseLdap ] = createSignal ( false )
6061 const [ loading , data ] = useFetch (
@@ -74,15 +75,17 @@ const Login = () => {
7475 }
7576 } ,
7677 )
77- const [ , postauthnlogin ] = useFetch (
78+ const [ , postPasskeyLogin ] = useFetch (
7879 (
7980 session : string ,
8081 credentials : AuthenticationPublicKeyCredential ,
8182 username : string ,
8283 signal : AbortSignal | undefined ,
8384 ) : Promise < Resp < { token : string } > > =>
8485 r . post (
85- "/authn/webauthn_finish_login?username=" + username ,
86+ `/authn/passkey_finish_login${
87+ username ? `?username=${ encodeURIComponent ( username ) } ` : ""
88+ } `,
8689 JSON . stringify ( credentials ) ,
8790 {
8891 headers : {
@@ -92,13 +95,33 @@ const Login = () => {
9295 } ,
9396 ) ,
9497 )
95- interface Webauthntemp {
98+ interface PasskeyTemp {
9699 session : string
97100 options : CredentialRequestOptionsJSON
101+ require_username ?: boolean
98102 }
99- const [ , getauthntemp ] = useFetch (
100- ( username , signal : AbortSignal | undefined ) : PResp < Webauthntemp > =>
101- r . get ( "/authn/webauthn_begin_login?username=" + username , {
103+ interface LegacyAuthnStatus {
104+ has_legacy : boolean
105+ }
106+ const [ , getPasskeyTemp ] = useFetch (
107+ (
108+ username : string ,
109+ allowCredentials : "yes" | "no" ,
110+ signal : AbortSignal | undefined ,
111+ ) : PResp < PasskeyTemp > => {
112+ const params = new URLSearchParams ( )
113+ params . set ( "allowCredentials" , allowCredentials )
114+ if ( username ) {
115+ params . set ( "username" , username )
116+ }
117+ return r . get ( `/authn/passkey_begin_login?${ params . toString ( ) } ` , {
118+ signal,
119+ } )
120+ } ,
121+ )
122+ const [ , getLegacyAuthnStatus ] = useFetch (
123+ ( signal : AbortSignal | undefined ) : PResp < LegacyAuthnStatus > =>
124+ r . get ( "/authn/passkey_legacy_status" , {
102125 signal,
103126 } ) ,
104127 )
@@ -114,9 +137,34 @@ const Login = () => {
114137 return false
115138 }
116139 }
117- const AuthnSignEnabled = getSettingBool ( "webauthn_login_enabled" )
140+ const passkeySignEnabled = true
141+ const passkeyAutoDisabled = "passkey-auto-login-disabled"
142+ const legacyPasskeyHintShown = "legacy-passkey-upgrade-tip-shown"
143+ const syncLegacyAuthnStatus = async ( signal ?: AbortSignal ) => {
144+ const resp = await getLegacyAuthnStatus ( signal )
145+ handleRespWithoutNotify (
146+ resp ,
147+ ( data ) => {
148+ setPasskeyNeedsUsername ( Boolean ( data . has_legacy ) )
149+ } ,
150+ undefined ,
151+ false ,
152+ )
153+ }
118154 const AuthnSwitch = async ( ) => {
119- setuseauthn ( ! useauthn ( ) )
155+ if ( usePasskey ( ) ) {
156+ AuthnSignal ?. abort ( )
157+ sessionStorage . setItem ( passkeyAutoDisabled , "true" )
158+ setUsePasskey ( false )
159+ setPasskeyNeedsUsername ( false )
160+ return
161+ }
162+ sessionStorage . removeItem ( passkeyAutoDisabled )
163+ await syncLegacyAuthnStatus ( )
164+ setUsePasskey ( true )
165+ if ( ! passkeyNeedsUsername ( ) ) {
166+ await AuthnLogin ( )
167+ }
120168 }
121169 let AuthnSignal : AbortController | null = null
122170 const AuthnLogin = async ( conditional ?: boolean ) => {
@@ -132,14 +180,11 @@ const Login = () => {
132180 AuthnSignal ?. abort ( )
133181 const controller = new AbortController ( )
134182 AuthnSignal = controller
135- const username_login : string = conditional ? "" : username ( )
136- if ( ! conditional && remember ( ) === "true" ) {
137- localStorage . setItem ( "username" , username ( ) )
138- } else {
139- localStorage . removeItem ( "username" )
140- }
141- const resp = await getauthntemp ( username_login , controller . signal )
142- handleResp ( resp , async ( data ) => {
183+
184+ const continuePasskeyLogin = async (
185+ data : PasskeyTemp ,
186+ usernameLogin : string ,
187+ ) => {
143188 try {
144189 const options = parseRequestOptionsFromJSON ( data . options )
145190 options . signal = controller . signal
@@ -148,14 +193,22 @@ const Login = () => {
148193 options . mediation = "conditional"
149194 }
150195 const credentials = await get ( options )
151- const resp = await postauthnlogin (
196+ const resp = await postPasskeyLogin (
152197 data . session ,
153198 credentials ,
154- username_login ,
199+ usernameLogin ,
155200 controller . signal ,
156201 )
157202 handleRespWithoutNotify ( resp , ( data ) => {
203+ if (
204+ usernameLogin &&
205+ ! sessionStorage . getItem ( legacyPasskeyHintShown )
206+ ) {
207+ notify . warning ( t ( "login.passkey_legacy_upgrade_tip" ) )
208+ sessionStorage . setItem ( legacyPasskeyHintShown , "true" )
209+ }
158210 notify . success ( t ( "login.success" ) )
211+ setPasskeyNeedsUsername ( false )
159212 changeToken ( data . token )
160213 to (
161214 decodeURIComponent ( searchParams . redirect || base_path || "/" ) ,
@@ -166,13 +219,43 @@ const Login = () => {
166219 if ( error instanceof Error && error . name != "AbortError" )
167220 notify . error ( error . message )
168221 }
222+ }
223+
224+ const usernameLogin =
225+ ! conditional && passkeyNeedsUsername ( ) ? username ( ) . trim ( ) : ""
226+ const allowCredentials : "yes" | "no" =
227+ ! conditional && passkeyNeedsUsername ( ) ? "yes" : "no"
228+ const resp = await getPasskeyTemp (
229+ usernameLogin ,
230+ allowCredentials ,
231+ controller . signal ,
232+ )
233+ handleResp ( resp , async ( data ) => {
234+ if ( data . require_username && ! usernameLogin ) {
235+ setPasskeyNeedsUsername ( true )
236+ return
237+ }
238+ setPasskeyNeedsUsername ( Boolean ( data . require_username ) )
239+ await continuePasskeyLogin ( data , usernameLogin )
169240 } )
170241 }
171242 const AuthnCleanUpHandler = ( ) => AuthnSignal ?. abort ( )
172243 onMount ( ( ) => {
173- if ( AuthnSignEnabled ) {
244+ if ( passkeySignEnabled ) {
174245 window . addEventListener ( "beforeunload" , AuthnCleanUpHandler )
175- AuthnLogin ( true )
246+ if ( sessionStorage . getItem ( passkeyAutoDisabled ) === "true" ) return
247+ if ( ! supported ( ) ) return
248+ syncLegacyAuthnStatus ( ) . then ( ( ) => {
249+ if ( passkeyNeedsUsername ( ) ) {
250+ setUsePasskey ( true )
251+ return
252+ }
253+ isAuthnConditionalAvailable ( ) . then ( ( available ) => {
254+ if ( ! available ) return
255+ setUsePasskey ( true )
256+ AuthnLogin ( true )
257+ } )
258+ } )
176259 }
177260 } )
178261 onCleanup ( ( ) => {
@@ -181,7 +264,7 @@ const Login = () => {
181264 } )
182265
183266 const Login = async ( ) => {
184- if ( ! useauthn ( ) ) {
267+ if ( ! usePasskey ( ) ) {
185268 if ( remember ( ) === "true" ) {
186269 localStorage . setItem ( "username" , username ( ) )
187270 localStorage . setItem ( "password" , password ( ) )
@@ -254,13 +337,19 @@ const Login = () => {
254337 />
255338 }
256339 >
257- < Input
258- name = "username"
259- placeholder = { t ( "login.username-tips" ) }
260- value = { username ( ) }
261- onInput = { ( e ) => setUsername ( e . currentTarget . value ) }
262- />
263- < Show when = { ! useauthn ( ) } >
340+ < Show when = { ! usePasskey ( ) || passkeyNeedsUsername ( ) } >
341+ < Input
342+ name = "username"
343+ placeholder = {
344+ usePasskey ( )
345+ ? t ( "login.passkey_input_username" )
346+ : t ( "login.username-tips" )
347+ }
348+ value = { username ( ) }
349+ onInput = { ( e ) => setUsername ( e . currentTarget . value ) }
350+ />
351+ </ Show >
352+ < Show when = { ! usePasskey ( ) } >
264353 < Input
265354 name = "password"
266355 placeholder = { t ( "login.password-tips" ) }
@@ -274,29 +363,31 @@ const Login = () => {
274363 } }
275364 />
276365 </ Show >
277- < Flex
278- px = "$1"
279- w = "$full"
280- fontSize = "$sm"
281- color = "$neutral10"
282- justifyContent = "space-between"
283- alignItems = "center"
284- >
285- < Checkbox
286- checked = { remember ( ) === "true" }
287- onChange = { ( ) =>
288- setRemember ( remember ( ) === "true" ? "false" : "true" )
289- }
366+ < Show when = { ! usePasskey ( ) } >
367+ < Flex
368+ px = "$1"
369+ w = "$full"
370+ fontSize = "$sm"
371+ color = "$neutral10"
372+ justifyContent = "space-between"
373+ alignItems = "center"
290374 >
291- { t ( "login.remember" ) }
292- </ Checkbox >
293- < Text as = "a" target = "_blank" href = { t ( "login.forget_url" ) } >
294- { t ( "login.forget" ) }
295- </ Text >
296- </ Flex >
375+ < Checkbox
376+ checked = { remember ( ) === "true" }
377+ onChange = { ( ) =>
378+ setRemember ( remember ( ) === "true" ? "false" : "true" )
379+ }
380+ >
381+ { t ( "login.remember" ) }
382+ </ Checkbox >
383+ < Text as = "a" target = "_blank" href = { t ( "login.forget_url" ) } >
384+ { t ( "login.forget" ) }
385+ </ Text >
386+ </ Flex >
387+ </ Show >
297388 </ Show >
298389 < HStack w = "$full" spacing = "$2" >
299- < Show when = { ! useauthn ( ) } >
390+ < Show when = { ! usePasskey ( ) } >
300391 < Button
301392 colorScheme = "primary"
302393 w = "$full"
@@ -313,7 +404,7 @@ const Login = () => {
313404 </ Button >
314405 </ Show >
315406 < Button w = "$full" loading = { loading ( ) } onClick = { Login } >
316- { t ( "login.login" ) }
407+ { usePasskey ( ) ? t ( "login.continue_with_passkey" ) : t ( "login.login" ) }
317408 </ Button >
318409 </ HStack >
319410 < Show when = { ldapLoginEnabled } >
@@ -348,7 +439,7 @@ const Login = () => {
348439 < SwitchLanguageWhite />
349440 < SwitchColorMode />
350441 < SSOLogin />
351- < Show when = { AuthnSignEnabled } >
442+ < Show when = { passkeySignEnabled } >
352443 < Icon
353444 cursor = "pointer"
354445 boxSize = "$8"
0 commit comments