Skip to content

Commit 6e3fe1e

Browse files
committed
Add recaptcha enterprise support to the link API (which calls signUp)
1 parent 5e1683e commit 6e3fe1e

File tree

2 files changed

+215
-4
lines changed

2 files changed

+215
-4
lines changed

packages/auth/src/core/credentials/email.test.ts

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,210 @@ describe('core/credentials/email', () => {
308308
idToken: 'id-token-2',
309309
returnSecureToken: true,
310310
email: 'some-email',
311-
password: 'some-password'
311+
password: 'some-password',
312+
clientType: RecaptchaClientType.WEB
313+
});
314+
});
315+
context('#recaptcha', () => {
316+
beforeEach(async () => {
317+
apiMock = mockEndpoint(Endpoint.SIGN_UP, {
318+
idToken: 'id-token',
319+
refreshToken: 'refresh-token',
320+
expiresIn: '1234',
321+
localId: serverUser.localId!
322+
});
323+
324+
});
325+
326+
afterEach(() => {
327+
sinon.restore();
328+
});
329+
330+
const recaptchaConfigResponseEnforce = {
331+
recaptchaKey: 'foo/bar/to/site-key',
332+
recaptchaEnforcementState: [
333+
{ provider: 'EMAIL_PASSWORD_PROVIDER', enforcementState: 'ENFORCE' }
334+
]
335+
};
336+
const recaptchaConfigResponseOff = {
337+
recaptchaKey: 'foo/bar/to/site-key',
338+
recaptchaEnforcementState: [
339+
{ provider: 'EMAIL_PASSWORD_PROVIDER', enforcementState: 'OFF' }
340+
]
341+
};
342+
343+
it('calls sign up with password with recaptcha enabled', async () => {
344+
const recaptcha = new MockGreCAPTCHATopLevel();
345+
if (typeof window === 'undefined') {
346+
return;
347+
}
348+
window.grecaptcha = recaptcha;
349+
sinon
350+
.stub(recaptcha.enterprise, 'execute')
351+
.returns(Promise.resolve('recaptcha-response'));
352+
mockEndpointWithParams(
353+
Endpoint.GET_RECAPTCHA_CONFIG,
354+
{
355+
clientType: RecaptchaClientType.WEB,
356+
version: RecaptchaVersion.ENTERPRISE
357+
},
358+
recaptchaConfigResponseEnforce
359+
);
360+
await _initializeRecaptchaConfig(auth);
361+
362+
const idTokenResponse = await credential._linkToIdToken(
363+
auth,
364+
'id-token-2'
365+
);
366+
expect(idTokenResponse.idToken).to.eq('id-token');
367+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
368+
expect(idTokenResponse.expiresIn).to.eq('1234');
369+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
370+
expect(apiMock.calls[0].request).to.eql({
371+
captchaResponse: 'recaptcha-response',
372+
recaptchaVersion: RecaptchaVersion.ENTERPRISE,
373+
idToken: 'id-token-2',
374+
returnSecureToken: true,
375+
email: 'some-email',
376+
password: 'some-password',
377+
clientType: 'CLIENT_TYPE_WEB'
378+
});
379+
});
380+
381+
it('calls sign up with password with recaptcha disabled', async () => {
382+
const recaptcha = new MockGreCAPTCHATopLevel();
383+
if (typeof window === 'undefined') {
384+
return;
385+
}
386+
window.grecaptcha = recaptcha;
387+
sinon
388+
.stub(recaptcha.enterprise, 'execute')
389+
.returns(Promise.resolve('recaptcha-response'));
390+
mockEndpointWithParams(
391+
Endpoint.GET_RECAPTCHA_CONFIG,
392+
{
393+
clientType: RecaptchaClientType.WEB,
394+
version: RecaptchaVersion.ENTERPRISE
395+
},
396+
recaptchaConfigResponseOff
397+
);
398+
await _initializeRecaptchaConfig(auth);
399+
const idTokenResponse = await credential._linkToIdToken(
400+
auth,
401+
'id-token-2'
402+
);
403+
expect(idTokenResponse.idToken).to.eq('id-token');
404+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
405+
expect(idTokenResponse.expiresIn).to.eq('1234');
406+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
407+
expect(apiMock.calls[0].request).to.eql({
408+
idToken: 'id-token-2',
409+
returnSecureToken: true,
410+
email: 'some-email',
411+
password: 'some-password',
412+
clientType: 'CLIENT_TYPE_WEB'
413+
});
414+
});
415+
416+
it('calls sign up with password with recaptcha forced refresh', async () => {
417+
if (typeof window === 'undefined') {
418+
return;
419+
}
420+
// Mock recaptcha js loading method but not set window.recaptcha to simulate recaptcha token retrieval failure
421+
sinon
422+
.stub(jsHelpers, '_loadJS')
423+
.returns(Promise.resolve(new Event('')));
424+
window.grecaptcha = undefined;
425+
426+
const getRecaptchaConfigMock = mockEndpointWithParams(
427+
Endpoint.GET_RECAPTCHA_CONFIG,
428+
{
429+
clientType: RecaptchaClientType.WEB,
430+
version: RecaptchaVersion.ENTERPRISE
431+
},
432+
recaptchaConfigResponseEnforce
433+
);
434+
await _initializeRecaptchaConfig(auth);
435+
auth._agentRecaptchaConfig!.siteKey = 'cached-site-key';
436+
437+
await expect(credential._linkToIdToken(auth, 'id-token-2')).to.be.rejectedWith(
438+
'No reCAPTCHA enterprise script loaded.'
439+
);
440+
// Should call getRecaptchaConfig once to refresh the cached recaptcha config
441+
expect(getRecaptchaConfigMock.calls.length).to.eq(2);
442+
expect(auth._agentRecaptchaConfig?.siteKey).to.eq('site-key');
443+
});
444+
445+
it('calls fallback to recaptcha flow when receiving MISSING_RECAPTCHA_TOKEN error', async () => {
446+
if (typeof window === 'undefined') {
447+
return;
448+
}
449+
450+
// First call without recaptcha token should fail with MISSING_RECAPTCHA_TOKEN error
451+
mockEndpointWithParams(
452+
Endpoint.SIGN_UP,
453+
{
454+
idToken: 'id-token-2',
455+
email: 'some-email',
456+
password: 'some-password',
457+
returnSecureToken: true,
458+
clientType: RecaptchaClientType.WEB
459+
},
460+
{
461+
error: {
462+
code: 400,
463+
message: ServerError.MISSING_RECAPTCHA_TOKEN
464+
}
465+
},
466+
400
467+
);
468+
469+
// Second call with a valid recaptcha token (captchaResp) should succeed
470+
mockEndpointWithParams(
471+
Endpoint.SIGN_UP,
472+
{
473+
captchaResponse: 'recaptcha-response',
474+
clientType: RecaptchaClientType.WEB,
475+
email: 'some-email',
476+
password: 'some-password',
477+
recaptchaVersion: RecaptchaVersion.ENTERPRISE,
478+
returnSecureToken: true
479+
},
480+
{
481+
idToken: 'id-token',
482+
refreshToken: 'refresh-token',
483+
expiresIn: '1234',
484+
localId: serverUser.localId!
485+
}
486+
);
487+
488+
// Mock recaptcha js loading method and manually set window.recaptcha
489+
sinon
490+
.stub(jsHelpers, '_loadJS')
491+
.returns(Promise.resolve(new Event('')));
492+
const recaptcha = new MockGreCAPTCHATopLevel();
493+
window.grecaptcha = recaptcha;
494+
const stub = sinon.stub(recaptcha.enterprise, 'execute');
495+
stub
496+
.withArgs('site-key', {
497+
action: RecaptchaActionName.SIGN_IN_WITH_PASSWORD
498+
})
499+
.returns(Promise.resolve('recaptcha-response'));
500+
501+
mockEndpointWithParams(
502+
Endpoint.GET_RECAPTCHA_CONFIG,
503+
{
504+
clientType: RecaptchaClientType.WEB,
505+
version: RecaptchaVersion.ENTERPRISE
506+
},
507+
recaptchaConfigResponseEnforce
508+
);
509+
510+
const idTokenResponse = await credential._linkToIdToken(auth, "id-token-2");
511+
expect(idTokenResponse.idToken).to.eq('id-token');
512+
expect(idTokenResponse.refreshToken).to.eq('refresh-token');
513+
expect(idTokenResponse.expiresIn).to.eq('1234');
514+
expect(idTokenResponse.localId).to.eq(serverUser.localId);
312515
});
313516
});
314517
});

packages/auth/src/core/credentials/email.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { _fail } from '../util/assert';
3333
import { AuthCredential } from './auth_credential';
3434
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
3535
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
36+
import { SignUpRequest } from '../../api/authentication/sign_up';
3637
/**
3738
* Interface that represents the credentials returned by {@link EmailAuthProvider} for
3839
* {@link ProviderId}.PASSWORD
@@ -146,12 +147,19 @@ export class EmailAuthCredential extends AuthCredential {
146147
): Promise<IdTokenResponse> {
147148
switch (this.signInMethod) {
148149
case SignInMethod.EMAIL_PASSWORD:
149-
return linkEmailPassword(auth, {
150+
const request: SignUpRequest = {
150151
idToken,
151152
returnSecureToken: true,
152153
email: this._email,
153-
password: this._password
154-
});
154+
password: this._password,
155+
clientType: RecaptchaClientType.WEB
156+
}
157+
return handleRecaptchaFlow(
158+
auth,
159+
request,
160+
RecaptchaActionName.SIGN_UP_PASSWORD,
161+
linkEmailPassword
162+
);
155163
case SignInMethod.EMAIL_LINK:
156164
return signInWithEmailLinkForLinking(auth, {
157165
idToken,

0 commit comments

Comments
 (0)