FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
JavaScriptSolidServer/src/idp/passkey.js at gh-pages · JavaScriptSolidServer/JavaScriptSolidServer · GitHub
JavaScriptSolidServer
JavaScriptSolidServer
Repository navigation
Code
Issues
164
(164)
Pull requests
14
(14)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
JavaScriptSolidServer
/
src
/
idp
/
passkey.js
Copy path
More file actions
More file actions
Latest commit
History
History
History
311 lines (265 loc) · 8.98 KB
Breadcrumbs
JavaScriptSolidServer
/
src
/
idp
/
passkey.js
Copy path
File metadata and controls
311 lines (265 loc) · 8.98 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/**
* Passkey (WebAuthn) authentication endpoints
* Handles registration and authentication of passkey credentials
*/
import
{
generateRegistrationOptions
,
verifyRegistrationResponse
,
generateAuthenticationOptions
,
verifyAuthenticationResponse
}
from
'@simplewebauthn/server'
;
import
crypto
from
'crypto'
;
import
*
as
accounts
from
'./accounts.js'
;
// Temporary challenge storage (in-memory, cleared on restart)
// For production clusters, use Redis or session storage
const
challenges
=
new
Map
(
)
;
const
MAX_CHALLENGES
=
10000
;
// Prevent unbounded growth
// Clean up expired challenges periodically
// Use unref() so this timer doesn't prevent process exit (important for tests)
const
cleanupInterval
=
setInterval
(
(
)
=>
{
const
now
=
Date
.
now
(
)
;
for
(
const
[
key
,
value
]
of
challenges
.
entries
(
)
)
{
if
(
now
>
value
.
expires
)
{
challenges
.
delete
(
key
)
;
}
}
}
,
60000
)
;
cleanupInterval
.
unref
(
)
;
/**
* Store a challenge with size limit enforcement
*/
function
storeChallenge
(
key
,
value
)
{
// If at capacity, remove oldest expired entries first
if
(
challenges
.
size
>=
MAX_CHALLENGES
)
{
const
now
=
Date
.
now
(
)
;
for
(
const
[
k
,
v
]
of
challenges
.
entries
(
)
)
{
if
(
now
>
v
.
expires
)
{
challenges
.
delete
(
k
)
;
}
if
(
challenges
.
size
<
MAX_CHALLENGES
)
break
;
}
}
// If still at capacity, reject (DoS protection)
if
(
challenges
.
size
>=
MAX_CHALLENGES
)
{
return
false
;
}
challenges
.
set
(
key
,
value
)
;
return
true
;
}
/**
* Get Relying Party configuration from request
* Handles both IPv4 (with port) and IPv6 addresses correctly
*/
function
getRP
(
request
)
{
let
hostname
;
try
{
// Use URL parsing to correctly extract hostname (handles IPv6)
const
url
=
new
URL
(
`
${
request
.
protocol
}
://
${
request
.
hostname
}
`
)
;
hostname
=
url
.
hostname
;
}
catch
{
// Fallback: strip port from hostname (IPv4 only)
hostname
=
String
(
request
.
hostname
||
''
)
.
split
(
':'
)
[
0
]
;
}
return
{
name
:
'Solid Pod'
,
id
:
hostname
}
;
}
/**
* Get origin from request
*/
function
getOrigin
(
request
)
{
return
`
${
request
.
protocol
}
://
${
request
.
hostname
}
`
;
}
/**
* POST /idp/passkey/register/options
* Generate registration options for a logged-in user
*/
export
async
function
registrationOptions
(
request
,
reply
)
{
const
{
accountId
}
=
request
.
body
||
{
}
;
if
(
!
accountId
)
{
return
reply
.
code
(
401
)
.
send
(
{
error
:
'Must provide accountId'
}
)
;
}
const
account
=
await
accounts
.
findById
(
accountId
)
;
if
(
!
account
)
{
return
reply
.
code
(
404
)
.
send
(
{
error
:
'Account not found'
}
)
;
}
const
rp
=
getRP
(
request
)
;
const
options
=
await
generateRegistrationOptions
(
{
rpName
:
rp
.
name
,
rpID
:
rp
.
id
,
userID
:
new
TextEncoder
(
)
.
encode
(
account
.
id
)
,
userName
:
account
.
username
,
userDisplayName
:
account
.
username
,
attestationType
:
'none'
,
// Don't require attestation for privacy
excludeCredentials
:
(
account
.
passkeys
||
[
]
)
.
map
(
pk
=>
(
{
id
:
Buffer
.
from
(
pk
.
credentialId
,
'base64url'
)
,
type
:
'public-key'
,
transports
:
pk
.
transports
}
)
)
,
authenticatorSelection
:
{
residentKey
:
'preferred'
,
userVerification
:
'preferred'
}
}
)
;
// Store challenge for verification with unique key (prevents race conditions from multiple tabs)
const
challengeKey
=
crypto
.
randomUUID
(
)
;
const
stored
=
storeChallenge
(
challengeKey
,
{
challenge
:
options
.
challenge
,
type
:
'registration'
,
accountId
:
account
.
id
,
expires
:
Date
.
now
(
)
+
60000
// 1 minute
}
)
;
if
(
!
stored
)
{
return
reply
.
code
(
503
)
.
send
(
{
error
:
'Server busy, try again later'
}
)
;
}
return
reply
.
send
(
{
...
options
,
challengeKey
}
)
;
}
/**
* POST /idp/passkey/register/verify
* Verify and store the registration response
*/
export
async
function
registrationVerify
(
request
,
reply
)
{
const
{
accountId
,
credential
,
name
,
challengeKey
}
=
request
.
body
||
{
}
;
if
(
!
accountId
||
!
credential
||
!
challengeKey
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Missing required fields'
}
)
;
}
const
stored
=
challenges
.
get
(
challengeKey
)
;
if
(
!
stored
||
stored
.
type
!==
'registration'
||
Date
.
now
(
)
>
stored
.
expires
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Challenge expired or invalid'
}
)
;
}
// Verify the accountId matches the challenge
if
(
stored
.
accountId
!==
accountId
)
{
return
reply
.
code
(
403
)
.
send
(
{
error
:
'Account mismatch'
}
)
;
}
const
account
=
await
accounts
.
findById
(
accountId
)
;
if
(
!
account
)
{
return
reply
.
code
(
404
)
.
send
(
{
error
:
'Account not found'
}
)
;
}
const
rp
=
getRP
(
request
)
;
try
{
const
verification
=
await
verifyRegistrationResponse
(
{
response
:
credential
,
expectedChallenge
:
stored
.
challenge
,
expectedOrigin
:
getOrigin
(
request
)
,
expectedRPID
:
rp
.
id
}
)
;
if
(
!
verification
.
verified
||
!
verification
.
registrationInfo
)
{
request
.
log
.
warn
(
{
verified
:
verification
.
verified
,
hasInfo
:
!
!
verification
.
registrationInfo
}
,
'Passkey registration verification failed'
)
;
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Verification failed'
}
)
;
}
const
{
credential
:
regCredential
}
=
verification
.
registrationInfo
;
await
accounts
.
addPasskey
(
accountId
,
{
credentialId
:
regCredential
.
id
,
// Already base64url string
publicKey
:
Buffer
.
from
(
regCredential
.
publicKey
)
.
toString
(
'base64url'
)
,
counter
:
regCredential
.
counter
,
transports
:
regCredential
.
transports
||
credential
.
response
?.
transports
||
[
]
,
name
:
name
||
'Security Key'
}
)
;
challenges
.
delete
(
challengeKey
)
;
return
reply
.
send
(
{
success
:
true
}
)
;
}
catch
(
err
)
{
request
.
log
.
error
(
{
err
}
,
'Passkey registration error'
)
;
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Passkey registration failed'
}
)
;
}
}
/**
* POST /idp/passkey/login/options
* Generate authentication options
*/
export
async
function
authenticationOptions
(
request
,
reply
)
{
const
{
username
}
=
request
.
body
||
{
}
;
const
rp
=
getRP
(
request
)
;
let
allowCredentials
=
[
]
;
let
accountId
=
null
;
// If username provided, limit to that user's credentials
if
(
username
)
{
const
account
=
await
accounts
.
findByUsername
(
username
)
;
if
(
account
&&
account
.
passkeys
?.
length
)
{
accountId
=
account
.
id
;
allowCredentials
=
account
.
passkeys
.
map
(
pk
=>
(
{
id
:
Buffer
.
from
(
pk
.
credentialId
,
'base64url'
)
,
type
:
'public-key'
,
transports
:
pk
.
transports
}
)
)
;
}
}
const
options
=
await
generateAuthenticationOptions
(
{
rpID
:
rp
.
id
,
allowCredentials
,
userVerification
:
'preferred'
}
)
;
// Store challenge - use visitorId for anonymous requests
const
challengeKey
=
accountId
||
request
.
body
?.
visitorId
||
crypto
.
randomUUID
(
)
;
const
stored
=
storeChallenge
(
challengeKey
,
{
challenge
:
options
.
challenge
,
type
:
'authentication'
,
accountId
,
expires
:
Date
.
now
(
)
+
60000
// 1 minute
}
)
;
if
(
!
stored
)
{
return
reply
.
code
(
503
)
.
send
(
{
error
:
'Server busy, try again later'
}
)
;
}
return
reply
.
send
(
{
...
options
,
challengeKey
}
)
;
}
/**
* POST /idp/passkey/login/verify
* Verify authentication and return account info
*/
export
async
function
authenticationVerify
(
request
,
reply
)
{
const
{
challengeKey
,
credential
}
=
request
.
body
||
{
}
;
if
(
!
challengeKey
||
!
credential
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Missing challengeKey or credential'
}
)
;
}
const
stored
=
challenges
.
get
(
challengeKey
)
;
if
(
!
stored
||
stored
.
type
!==
'authentication'
||
Date
.
now
(
)
>
stored
.
expires
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Challenge expired or invalid'
}
)
;
}
// Find account by credential ID
const
credentialId
=
credential
.
id
;
const
account
=
stored
.
accountId
?
await
accounts
.
findById
(
stored
.
accountId
)
:
await
accounts
.
findByCredentialId
(
credentialId
)
;
if
(
!
account
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Unknown credential'
}
)
;
}
const
passkey
=
account
.
passkeys
?.
find
(
pk
=>
pk
.
credentialId
===
credentialId
)
;
if
(
!
passkey
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Credential not found'
}
)
;
}
const
rp
=
getRP
(
request
)
;
try
{
const
verification
=
await
verifyAuthenticationResponse
(
{
response
:
credential
,
expectedChallenge
:
stored
.
challenge
,
expectedOrigin
:
getOrigin
(
request
)
,
expectedRPID
:
rp
.
id
,
credential
:
{
id
:
Buffer
.
from
(
passkey
.
credentialId
,
'base64url'
)
,
publicKey
:
Buffer
.
from
(
passkey
.
publicKey
,
'base64url'
)
,
counter
:
passkey
.
counter
}
}
)
;
if
(
!
verification
.
verified
)
{
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Verification failed'
}
)
;
}
// Update counter to prevent replay attacks
await
accounts
.
updatePasskeyCounter
(
account
.
id
,
credentialId
,
verification
.
authenticationInfo
.
newCounter
)
;
// Update last login
await
accounts
.
updateLastLogin
(
account
.
id
)
;
challenges
.
delete
(
challengeKey
)
;
// Return account info for session creation
return
reply
.
send
(
{
success
:
true
,
accountId
:
account
.
id
,
webId
:
account
.
webId
}
)
;
}
catch
(
err
)
{
request
.
log
.
error
(
{
err
}
,
'Passkey authentication error'
)
;
return
reply
.
code
(
400
)
.
send
(
{
error
:
'Authentication failed'
}
)
;
}
}
Back
|
FazBrowse Home
|
New Git URL