@@ -66,11 +66,16 @@ router.post('/passkeys/register/begin', authenticateToken, async (req, res) => {
6666 attestationType : 'none' ,
6767 excludeCredentials,
6868 userId : new TextEncoder ( ) . encode ( String ( userId ) . padStart ( 16 , '0' ) ) ,
69+ authenticatorSelection : {
70+ residentKey : 'required' ,
71+ userVerification : 'preferred' ,
72+ } ,
6973 } ) ;
7074
71- challengeMap . set ( userId , {
75+ challengeMap . set ( options . challenge , {
7276 challenge : options . challenge ,
7377 timestamp : Date . now ( ) ,
78+ userId,
7479 } ) ;
7580
7681 res . json ( { options } ) ;
@@ -89,12 +94,13 @@ router.post('/passkeys/register/complete', authenticateToken, async (req, res) =
8994 return res . status ( 400 ) . json ( { error : 'Registration response is required' } ) ;
9095 }
9196
92- const expectedChallenge = challengeMap . get ( userId ) ;
97+ const challengeFromResponse = JSON . parse ( isoBase64URL . toUTF8String ( response . response . clientDataJSON ) ) . challenge ;
98+ const expectedChallenge = challengeMap . get ( challengeFromResponse ) ;
9399 if ( ! expectedChallenge ) {
94100 return res . status ( 400 ) . json ( { error : 'No registration in progress. Please try again.' } ) ;
95101 }
96102
97- challengeMap . delete ( userId ) ;
103+ challengeMap . delete ( challengeFromResponse ) ;
98104
99105 const { rpID, origin } = getWebAuthnConfig ( req ) ;
100106 const verification = await verifyRegistrationResponse ( {
@@ -134,50 +140,51 @@ router.post('/passkeys/register/complete', authenticateToken, async (req, res) =
134140router . post ( '/passkeys/login/begin' , async ( req , res ) => {
135141 try {
136142 const { email } = req . body ;
137- if ( ! email || typeof email !== 'string' ) {
138- return res . status ( 400 ) . json ( { error : 'Email is required' } ) ;
139- }
140-
141- const users = await query ( 'SELECT id, email, username, auth_restricted FROM users WHERE email = ?' , [ email ] ) ;
142- if ( ! users . length ) {
143- return res . status ( 404 ) . json ( { error : 'No account found with this email' } ) ;
144- }
145-
146- const user = users [ 0 ] ;
147- if ( user . auth_restricted ) {
148- return res . status ( 403 ) . json ( { error : 'Your account has been restricted. Contact support for assistance.' } ) ;
149- }
150-
151- const passkeys = await query (
152- 'SELECT id, credential_id, transports FROM passkeys WHERE user_id = ?' ,
153- [ user . id ]
154- ) ;
155-
156- if ( ! passkeys . length ) {
157- return res . status ( 404 ) . json ( { error : 'No passkeys registered for this account' } ) ;
143+ let userId = null ;
144+ let allowCredentials = [ ] ;
145+
146+ if ( email && typeof email === 'string' ) {
147+ const users = await query ( 'SELECT id, email, username, auth_restricted FROM users WHERE email = ?' , [ email ] ) ;
148+ if ( ! users . length ) {
149+ return res . status ( 404 ) . json ( { error : 'No account found with this email' } ) ;
150+ }
151+
152+ const user = users [ 0 ] ;
153+ if ( user . auth_restricted ) {
154+ return res . status ( 403 ) . json ( { error : 'Your account has been restricted. Contact support for assistance.' } ) ;
155+ }
156+
157+ userId = user . id ;
158+ const passkeys = await query (
159+ 'SELECT id, credential_id, transports FROM passkeys WHERE user_id = ?' ,
160+ [ user . id ]
161+ ) ;
162+
163+ if ( ! passkeys . length ) {
164+ return res . status ( 404 ) . json ( { error : 'No passkeys registered for this account' } ) ;
165+ }
166+
167+ allowCredentials = passkeys . map ( k => ( {
168+ id : k . credential_id ,
169+ type : 'public-key' ,
170+ transports : k . transports ? k . transports . split ( ',' ) : [ 'internal' ] ,
171+ } ) ) ;
158172 }
159173
160- const allowCredentials = passkeys . map ( k => ( {
161- id : k . credential_id ,
162- type : 'public-key' ,
163- transports : k . transports ? k . transports . split ( ',' ) : [ 'internal' ] ,
164- } ) ) ;
165-
166174 const { rpID } = getWebAuthnConfig ( req ) ;
167175 const options = await generateAuthenticationOptions ( {
168176 rpID,
169177 allowCredentials,
170178 userVerification : 'preferred' ,
171179 } ) ;
172180
173- challengeMap . set ( `login: ${ user . id } ` , {
181+ challengeMap . set ( options . challenge , {
174182 challenge : options . challenge ,
175183 timestamp : Date . now ( ) ,
176- email,
177- userId : user . id ,
184+ userId,
178185 } ) ;
179186
180- res . json ( { options, userId : user . id } ) ;
187+ res . json ( { options, userId } ) ;
181188 } catch ( err ) {
182189 console . error ( 'Passkey login begin error:' , err . message ) ;
183190 res . status ( 500 ) . json ( { error : 'Failed to initiate passkey login' } ) ;
@@ -186,17 +193,28 @@ router.post('/passkeys/login/begin', async (req, res) => {
186193
187194router . post ( '/passkeys/login/complete' , async ( req , res ) => {
188195 try {
189- const { response, userId } = req . body ;
190- if ( ! response || ! userId ) {
191- return res . status ( 400 ) . json ( { error : 'Response and userId are required' } ) ;
196+ const { response, userId : bodyUserId } = req . body ;
197+ if ( ! response ) {
198+ return res . status ( 400 ) . json ( { error : 'Response is required' } ) ;
192199 }
193200
194- const expectedChallenge = challengeMap . get ( `login:${ userId } ` ) ;
201+ const challengeFromResponse = JSON . parse ( isoBase64URL . toUTF8String ( response . response . clientDataJSON ) ) . challenge ;
202+ const expectedChallenge = challengeMap . get ( challengeFromResponse ) ;
195203 if ( ! expectedChallenge ) {
196204 return res . status ( 400 ) . json ( { error : 'No login in progress. Please try again.' } ) ;
197205 }
198206
199- challengeMap . delete ( `login:${ userId } ` ) ;
207+ challengeMap . delete ( challengeFromResponse ) ;
208+
209+ let userId = bodyUserId || expectedChallenge . userId ;
210+ if ( ! userId && response . response . userHandle ) {
211+ const userHandleBytes = isoBase64URL . toBuffer ( response . response . userHandle ) ;
212+ userId = parseInt ( new TextDecoder ( ) . decode ( userHandleBytes ) , 10 ) ;
213+ }
214+
215+ if ( ! userId ) {
216+ return res . status ( 400 ) . json ( { error : 'Could not identify user. Try logging in with email.' } ) ;
217+ }
200218
201219 const passkeys = await query (
202220 'SELECT id, credential_id, public_key, counter, transports FROM passkeys WHERE user_id = ?' ,
0 commit comments