@meith/accounts 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/accounts",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "hash-wasm": "^4.12.0",
23
- "@meith/i18n": "0.34.0",
24
- "@meith/subscriptions": "0.34.0"
23
+ "@meith/i18n": "0.35.0",
24
+ "@meith/subscriptions": "0.35.0"
25
25
  }
26
26
  }
@@ -135,10 +135,14 @@ function profileFrom(claims: IdTokenClaims, userinfo: IdTokenClaims): ProviderPr
135
135
  throw new ValidationError(msg('error.accounts.identity-provider-sent-account-with'))
136
136
  }
137
137
 
138
+ const emailFromToken = stringClaim(claims, 'email')
139
+ const emailSource = emailFromToken !== null ? claims : userinfo
140
+ const email = emailFromToken ?? stringClaim(userinfo, 'email')
141
+
138
142
  return {
139
143
  subject,
140
- email: stringClaim(merged, 'email') ?? stringClaim(userinfo, 'email'),
141
- emailVerified: readBoolean(merged, 'email_verified') || readBoolean(userinfo, 'email_verified'),
144
+ email,
145
+ emailVerified: email !== null && readBoolean(emailSource, 'email_verified'),
142
146
  username:
143
147
  stringClaim(merged, 'preferred_username') ??
144
148
  stringClaim(merged, 'nickname') ??
@@ -524,11 +524,12 @@ class MemoryPasskeys implements PasskeyRepository {
524
524
  return true
525
525
  }
526
526
 
527
- async markUsed(passkeyId: number, signCount: number, now: Date): Promise<void> {
527
+ async markUsed(passkeyId: number, signCount: number, now: Date): Promise<boolean> {
528
528
  const record = this.byId.get(passkeyId)
529
- if (record !== undefined) {
530
- this.byId.set(passkeyId, { ...record, signCount, lastUsedAt: now })
531
- }
529
+ if (record === undefined) return false
530
+ if (signCount !== 0 && signCount <= record.signCount) return false
531
+ this.byId.set(passkeyId, { ...record, signCount, lastUsedAt: now })
532
+ return true
532
533
  }
533
534
  }
534
535
 
package/src/ports.ts CHANGED
@@ -230,7 +230,7 @@ export interface PasskeyRepository {
230
230
  listForUser(userId: number): Promise<readonly PasskeyRecord[]>
231
231
  create(input: NewPasskey): Promise<PasskeyRecord>
232
232
  remove(userId: number, passkeyId: number): Promise<boolean>
233
- markUsed(passkeyId: number, signCount: number, now: Date): Promise<void>
233
+ markUsed(passkeyId: number, signCount: number, now: Date): Promise<boolean>
234
234
  }
235
235
 
236
236
  export interface TwoFactorRecord {
@@ -49,25 +49,19 @@ export class SessionService {
49
49
  return this.mintSession(userId, this.now(), context)
50
50
  }
51
51
 
52
- async startRemembered(userId: number, context: RequestContext = {}): Promise<RememberedLogin> {
52
+ async issueRemember(
53
+ userId: number,
54
+ ): Promise<{ readonly rememberToken: string; readonly rememberExpiresAt: Date }> {
53
55
  const at = this.now()
54
- const familyId = generateToken()
55
56
  const rememberToken = generateToken()
56
57
  const rememberExpiresAt = new Date(at.getTime() + this.rememberDays * DAY_MS)
57
58
  await this.store.remember.issue({
58
59
  tokenHash: await hashToken(rememberToken),
59
- familyId,
60
+ familyId: generateToken(),
60
61
  userId,
61
62
  expiresAt: rememberExpiresAt,
62
63
  })
63
- const session = await this.mintSession(userId, at, context)
64
- return {
65
- userId,
66
- sessionToken: session.token,
67
- sessionExpiresAt: session.expiresAt,
68
- rememberToken,
69
- rememberExpiresAt,
70
- }
64
+ return { rememberToken, rememberExpiresAt }
71
65
  }
72
66
 
73
67
  async resume(rememberToken: string, context: RequestContext = {}): Promise<ResumeOutcome> {
@@ -194,8 +194,11 @@ export class PasskeyService {
194
194
  throw new ForbiddenError(msg('error.accounts.account-behind-passkey-longer-exists'))
195
195
  }
196
196
 
197
+ if (!(await this.passkeys.markUsed(passkey.id, verified.signCount, this.now()))) {
198
+ throw new ForbiddenError(msg('error.accounts.passkey-replayed-counter-board-already'))
199
+ }
200
+
197
201
  const login = await this.identity.startSessionFor(account, input.context ?? {})
198
- await this.passkeys.markUsed(passkey.id, verified.signCount, this.now())
199
202
 
200
203
  return { account, login }
201
204
  }
@@ -218,8 +221,7 @@ export class PasskeyService {
218
221
  storedSignCount: passkey.signCount,
219
222
  })
220
223
 
221
- await this.passkeys.markUsed(passkey.id, verified.signCount, this.now())
222
- return true
224
+ return this.passkeys.markUsed(passkey.id, verified.signCount, this.now())
223
225
  }
224
226
 
225
227
  async remove(input: {