Skip to content

Commit fc917b6

Browse files
authored
Merge pull request #20194 from mozilla/FXA-13061
feat(passkeys): Add passkey generate and validate challenge methods
2 parents 68edb2b + ca36910 commit fc917b6

7 files changed

Lines changed: 860 additions & 276 deletions

libs/accounts/passkey/src/lib/passkey.challenge.manager.in.spec.ts

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ afterAll(async () => {
7272
describe('PasskeyChallengeManager (integration)', () => {
7373
describe('generateRegistrationChallenge', () => {
7474
it('stores challenge with uid and type=registration', async () => {
75-
const challenge = await manager.generateRegistrationChallenge({
76-
uid: 'deadbeef',
77-
});
75+
const challenge =
76+
await manager.generateRegistrationChallenge('deadbeef');
7877

79-
const stored = await manager.validateChallenge(challenge, 'registration');
78+
const stored = await manager.consumeRegistrationChallenge(
79+
'deadbeef',
80+
challenge
81+
);
8082

8183
expect(stored?.challenge).toBe(challenge);
8284
expect(stored?.type).toBe('registration');
@@ -85,65 +87,55 @@ describe('PasskeyChallengeManager (integration)', () => {
8587
});
8688

8789
describe('generateAuthenticationChallenge', () => {
88-
it('stores challenge with no uid and type=authentication', async () => {
89-
const challenge = await manager.generateAuthenticationChallenge();
90+
it('stores challenge with uid and type=authentication', async () => {
91+
const challenge =
92+
await manager.generateAuthenticationChallenge('deadbeef');
9093

91-
const stored = await manager.validateChallenge(
92-
challenge,
93-
'authentication'
94+
const stored = await manager.consumeAuthenticationChallenge(
95+
'deadbeef',
96+
challenge
9497
);
9598

9699
expect(stored?.challenge).toBe(challenge);
97100
expect(stored?.type).toBe('authentication');
98-
expect(stored?.uid).toBeUndefined();
101+
expect(stored?.uid).toBe('deadbeef');
99102
});
100103
});
101104

102105
describe('generateUpgradeChallenge', () => {
103106
it('stores challenge with uid and type=upgrade', async () => {
104-
const challenge = await manager.generateUpgradeChallenge({
105-
uid: 'cafebabe',
106-
});
107+
const challenge =
108+
await manager.generateUpgradeChallenge('cafebabe');
107109

108-
const stored = await manager.validateChallenge(challenge, 'upgrade');
110+
const stored = await manager.consumeUpgradeChallenge(
111+
'cafebabe',
112+
challenge
113+
);
109114

110115
expect(stored?.challenge).toBe(challenge);
111116
expect(stored?.type).toBe('upgrade');
112117
expect(stored?.uid).toBe('cafebabe');
113118
});
114119
});
115120

116-
describe('validateChallenge', () => {
117-
it('returns the stored challenge data', async () => {
118-
const challenge = await manager.generateRegistrationChallenge({
119-
uid: 'deadbeef',
120-
});
121-
122-
const stored = await manager.validateChallenge(challenge, 'registration');
123-
124-
expect(stored?.challenge).toBe(challenge);
125-
expect(stored?.type).toBe('registration');
126-
expect(stored?.uid).toBe('deadbeef');
127-
});
128-
129-
it('returns null on second validate (single-use enforcement)', async () => {
130-
const challenge = await manager.generateRegistrationChallenge({
131-
uid: 'deadbeef',
132-
});
121+
describe('consumeChallenge', () => {
122+
it('returns null on second consume (single-use enforcement)', async () => {
123+
const challenge =
124+
await manager.generateRegistrationChallenge('deadbeef');
133125

134-
await manager.validateChallenge(challenge, 'registration');
126+
await manager.consumeRegistrationChallenge('deadbeef', challenge);
135127

136-
const secondValidation = await manager.validateChallenge(
137-
challenge,
138-
'registration'
128+
const secondAttempt = await manager.consumeRegistrationChallenge(
129+
'deadbeef',
130+
challenge
139131
);
140-
expect(secondValidation).toBeNull();
132+
expect(secondAttempt).toBeNull();
141133
});
142134

143135
it('returns null for an unknown challenge', async () => {
144-
const result = await manager.validateChallenge(
145-
'nonexistent-challenge',
146-
'registration'
136+
const result = await manager.consumeRegistrationChallenge(
137+
'deadbeef',
138+
'nonexistent-challenge'
147139
);
148140
expect(result).toBeNull();
149141
});
@@ -159,38 +151,40 @@ describe('PasskeyChallengeManager (integration)', () => {
159151
const moduleRef = await buildTestModule(redis, shortConfig, mockLogger);
160152

161153
const shortManager = moduleRef.get(PasskeyChallengeManager);
162-
const challenge = await shortManager.generateRegistrationChallenge({
163-
uid: 'deadbeef',
164-
});
154+
const challenge =
155+
await shortManager.generateRegistrationChallenge('deadbeef');
165156

166157
// Wait longer than the TTL to ensure the challenge expires
167158
await new Promise((resolve) => setTimeout(resolve, 1100));
168159

169-
const result = await shortManager.validateChallenge(
170-
challenge,
171-
'registration'
160+
const result = await shortManager.consumeRegistrationChallenge(
161+
'deadbeef',
162+
challenge
172163
);
173164
expect(result).toBeNull();
174165
}, 5_000);
175166
});
176167

177168
describe('deleteChallenge', () => {
178169
it('removes the key from Redis', async () => {
179-
const challenge = await manager.generateRegistrationChallenge({
180-
uid: 'deadbeef',
181-
});
170+
const challenge =
171+
await manager.generateRegistrationChallenge('deadbeef');
182172

183-
await manager.deleteChallenge(challenge, 'registration');
173+
await manager.deleteChallenge(challenge, 'registration', 'deadbeef');
184174

185-
const result = await manager.validateChallenge(challenge, 'registration');
175+
const result = await manager.consumeRegistrationChallenge(
176+
'deadbeef',
177+
challenge
178+
);
186179

187180
expect(result).toBeNull();
188181
});
189182

190183
it('does not throw when the key does not exist', async () => {
191184
const result = await manager.deleteChallenge(
192185
'nonexistent-challenge',
193-
'registration'
186+
'registration',
187+
'deadbeef'
194188
);
195189
expect(result).toBeUndefined();
196190
});

0 commit comments

Comments
 (0)