OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity commits / fdd46ed

Fix challenge-response auth flow (integration audit)
Three issues found when testing hyper-identity as first consumer: 1. ChallengeManager generated hex challenges but browser ANProto signs base64 — changed to base64 encoding to match pear-git/hypernote 2. AuthManager was missing generateChallenge() method — consumers had no way to create challenges for the challenge-response flow 3. WebIdentityProvider duplicated user creation logic instead of calling registry.autoRegister() — refactored to use autoRegister() which handles first-user-is-admin automatically Also added get() and consume() methods to ChallengeManager to match pear-git's pattern (separate lookup from consumption), and added error logging to AuthManager.authenticate() provider loop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fdd46edcc69e120f1cdb274045f7048c822db969
by plankatron <fleeky@gmail.com> · 2026-03-25T00:10:08.000Z

Changed files (3) +62 -54

lib/auth.js +11 -1
……@@ -154,7 +154,8 @@ export class AuthManager {
154154 user,
155155 verified: true
156156 }
157 } catch {
157 } catch (err) {
158 console.error(`[AuthManager] Provider ${provider.name} error:`, err.message)
158159 continue
159160 }
160161 }
……@@ -162,6 +163,15 @@ export class AuthManager {
162163 return null
163164 }
164165
166 /**
167 * Generate a challenge for challenge-response auth
168 * @param {Object} opts - { publicKey?, metadata? }
169 * @returns {Promise<Object>} { challengeId, challenge, expiresAt }
170 */
171 async generateChallenge (opts = {}) {
172 return this.challengeManager.generate(opts)
173 }
174
165175 /**
166176 * Format user permissions
167177 * Uses user.resources (generic) instead of application-specific terms
lib/auth/challenge-manager.js +36 -11
……@@ -23,7 +23,7 @@ export class ChallengeManager {
2323 */
2424 async generate (opts = {}) {
2525 const challengeId = b4a.toString(crypto.randomBytes(16), 'hex')
26 const challenge = b4a.toString(crypto.randomBytes(32), 'hex')
26 const challenge = b4a.toString(crypto.randomBytes(32), 'base64')
2727 const expiresAt = Date.now() + this.ttl
2828
2929 const entry = {
……@@ -47,12 +47,11 @@ export class ChallengeManager {
4747 }
4848
4949 /**
50 * Verify and consume a challenge
51 * @param {string} challengeId - Challenge ID
52 * @param {string} challenge - Challenge value to verify
53 * @returns {Promise<Object|null>} Challenge entry or null
50 * Get a challenge by ID (without consuming it)
51 * @param {string} challengeId
52 * @returns {Promise<Object|null>} Challenge entry or null if expired/consumed
5453 */
55 async verify (challengeId, challenge) {
54 async get (challengeId) {
5655 let entry = this._cache.get(challengeId)
5756
5857 if (!entry && this.db) {
……@@ -63,15 +62,41 @@ export class ChallengeManager {
6362 if (!entry) return null
6463 if (entry.consumed) return null
6564 if (entry.expiresAt < Date.now()) return null
66 if (entry.challenge !== challenge) return null
6765
68 // Mark consumed
69 entry.consumed = true
70 this._cache.set(challengeId, entry)
66 return entry
67 }
68
69 /**
70 * Consume a challenge (mark as used, single-use)
71 * @param {string} challengeId
72 */
73 async consume (challengeId) {
74 const entry = this._cache.get(challengeId)
75 if (entry) {
76 entry.consumed = true
77 this._cache.set(challengeId, entry)
78 }
7179 if (this.db) {
72 await this.db.put(`challenge:${challengeId}`, entry)
80 const stored = await this.db.get(`challenge:${challengeId}`)
81 if (stored) {
82 stored.value.consumed = true
83 await this.db.put(`challenge:${challengeId}`, stored.value)
84 }
7385 }
86 }
87
88 /**
89 * Verify and consume a challenge (combined operation)
90 * @param {string} challengeId - Challenge ID
91 * @param {string} challenge - Challenge value to verify
92 * @returns {Promise<Object|null>} Challenge entry or null
93 */
94 async verify (challengeId, challenge) {
95 const entry = await this.get(challengeId)
96 if (!entry) return null
97 if (entry.challenge !== challenge) return null
7498
99 await this.consume(challengeId)
75100 return entry
76101 }
77102
lib/auth/providers/web-identity.js +15 -42
……@@ -1,6 +1,5 @@
11import { BaseProvider } from '../base-provider.js'
22import { ANProto } from '../../anproto.js'
3import b4a from 'b4a'
43
54/**
65 * WebIdentityProvider — Browser ANProto authentication
……@@ -29,48 +28,30 @@ export class WebIdentityProvider extends BaseProvider {
2928 const { publicKey, signature, challengeId } = credentials
3029
3130 if (signature && challengeId) {
32 // Signed auth — verify challenge signature
33 const challenge = await this.challengeManager.verify(challengeId, challengeId)
34 if (!challenge) {
35 // Try direct challenge verification
36 const stored = await this._getChallenge(challengeId)
37 if (!stored) throw new Error('Invalid or expired challenge')
31 // Look up the challenge by ID
32 const challenge = await this.challengeManager.get(challengeId)
33 if (!challenge) throw new Error('Invalid or expired challenge')
3834
39 const valid = ANProto.verify(stored.challenge, signature, publicKey)
40 if (!valid) throw new Error('Invalid signature')
41 } else {
42 const valid = ANProto.verify(challenge.challenge, signature, publicKey)
43 if (!valid) throw new Error('Invalid signature')
44 }
35 // Verify signature against the challenge value
36 const valid = ANProto.verify(challenge.challenge, signature, publicKey)
37 if (!valid) throw new Error('Invalid signature')
38
39 // Consume the challenge (single-use)
40 await this.challengeManager.consume(challengeId)
4541 }
4642
4743 // Look up or auto-register identity
4844 let identity = await this.registry.getIdentityByPublicKey(publicKey)
4945
5046 if (!identity) {
51 // Auto-register new web identity
52 const identityId = publicKey // Use public key as identity ID
53 await this.registry.registerIdentity({
54 identityId,
47 // Auto-register: creates user + identity atomically
48 // First user gets admin, subsequent get read-only
49 const result = await this.registry.autoRegister({
50 identityId: publicKey,
5551 method: 'web-identity',
56 publicKey,
57 createdAt: Date.now()
58 })
59
60 // Auto-create user
61 const user = await this.registry.createUser({
62 name: 'Anonymous',
63 globalPermissions: ['read'],
64 resources: [{ name: '*', permissions: ['read'] }],
65 identities: [identityId]
52 credentials: { publicKey }
6653 })
67
68 // Link identity to user
69 if (user && user.userId) {
70 await this.registry.linkIdentityToUser(identityId, user.userId)
71 }
72
73 identity = await this.registry.getIdentity(identityId)
54 identity = result.identity
7455 }
7556
7657 return {
……@@ -79,14 +60,6 @@ export class WebIdentityProvider extends BaseProvider {
7960 credentials: { publicKey }
8061 }
8162 }
82
83 async _getChallenge (challengeId) {
84 try {
85 return await this.challengeManager.verify(challengeId, challengeId)
86 } catch {
87 return null
88 }
89 }
9063}
9164
9265export default WebIdentityProvider