From 6a4953c2874bb3eab672c922885335a5aca970d0 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 16 Jun 2020 15:34:48 -0700 Subject: [PATCH 1/4] Add linking and reauth to email credential --- .../src/api/authentication/email_link.ts | 16 ++++- .../src/core/credentials/email.test.ts | 69 ++++++++++++++----- .../auth-exp/src/core/credentials/email.ts | 37 +++++++--- 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.ts b/packages-exp/auth-exp/src/api/authentication/email_link.ts index 10a0efdf445..630af3fe471 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Endpoint, HttpMethod, _performSignInRequest } from '..'; +import { _performSignInRequest, Endpoint, HttpMethod } from '../'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; @@ -38,3 +38,17 @@ export async function signInWithEmailLink( SignInWithEmailLinkResponse >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); } + +export interface SignInWithEmailLinkForLinkingRequest extends SignInWithEmailLinkRequest { + idToken: string; +} + +export async function signInWithEmailLinkForLinking( + auth: Auth, + request: SignInWithEmailLinkForLinkingRequest +): Promise { + return _performSignInRequest< + SignInWithEmailLinkForLinkingRequest, + SignInWithEmailLinkResponse + >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); +} \ No newline at end of file diff --git a/packages-exp/auth-exp/src/core/credentials/email.test.ts b/packages-exp/auth-exp/src/core/credentials/email.test.ts index 323532d2fce..25eae81c902 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.test.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.test.ts @@ -75,7 +75,7 @@ describe('core/credentials/email', () => { }); describe('#_getIdTokenResponse', () => { - it('call sign in with password', async () => { + it('calls sign in with password', async () => { const idTokenResponse = await credential._getIdTokenResponse(auth); expect(idTokenResponse.idToken).to.eq('id-token'); expect(idTokenResponse.refreshToken).to.eq('refresh-token'); @@ -90,18 +90,40 @@ describe('core/credentials/email', () => { }); describe('#_linkToIdToken', () => { - it('throws', async () => { - await expect( - credential._linkToIdToken(auth, 'id-token') - ).to.be.rejectedWith(Error); + it('calls update email password', async () => { + apiMock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + + const idTokenResponse = await credential._linkToIdToken(auth, 'id-token-2'); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + idToken: 'id-token-2', + returnSecureToken: true, + email: 'some-email', + password: 'some-password' + }); }); }); describe('#_getReauthenticationResolver', () => { - it('throws', () => { - expect(() => credential._getReauthenticationResolver(auth)).to.throw( - Error - ); + it('calls sign in with password', async () => { + const idTokenResponse = await credential._getIdTokenResponse(auth); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + returnSecureToken: true, + email: 'some-email', + password: 'some-password' + }); }); }); }); @@ -153,18 +175,31 @@ describe('core/credentials/email', () => { }); describe('#_linkToIdToken', () => { - it('throws', async () => { - await expect( - credential._linkToIdToken(auth, 'id-token') - ).to.be.rejectedWith(Error); + it('calls sign in with the new token', async () => { + const idTokenResponse = await credential._linkToIdToken(auth, 'id-token-2'); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + idToken: 'id-token-2', + email: 'some-email', + oobCode: 'oob-code' + }); }); }); describe('#_matchIdTokenWithUid', () => { - it('throws', () => { - expect(() => credential._getReauthenticationResolver(auth)).to.throw( - Error - ); + it('call sign in with email link', async () => { + const idTokenResponse = await credential._getIdTokenResponse(auth); + expect(idTokenResponse.idToken).to.eq('id-token'); + expect(idTokenResponse.refreshToken).to.eq('refresh-token'); + expect(idTokenResponse.expiresIn).to.eq('1234'); + expect(idTokenResponse.localId).to.eq(serverUser.localId); + expect(apiMock.calls[0].request).to.eql({ + email: 'some-email', + oobCode: 'oob-code' + }); }); }); }); diff --git a/packages-exp/auth-exp/src/core/credentials/email.ts b/packages-exp/auth-exp/src/core/credentials/email.ts index 741e460b68d..6c47d40bef2 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.ts @@ -17,13 +17,16 @@ import * as externs from '@firebase/auth-types-exp'; +import { updateEmailPassword } from '../../api/account_management/email_and_password'; import { signInWithPassword } from '../../api/authentication/email_and_password'; -import { signInWithEmailLink } from '../../api/authentication/email_link'; +import { + signInWithEmailLink, signInWithEmailLinkForLinking +} from '../../api/authentication/email_link'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; -import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors'; +import { AuthErrorCode } from '../errors'; import { EmailAuthProvider } from '../providers/email'; -import { debugFail } from '../util/assert'; +import { debugFail, fail } from '../util/assert'; import { AuthCredential } from './'; export class EmailAuthCredential implements AuthCredential { @@ -57,17 +60,31 @@ export class EmailAuthCredential implements AuthCredential { oobCode: this.password }); default: - throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, { - appName: auth.name - }); + fail(auth.name, AuthErrorCode.INTERNAL_ERROR); } } - async _linkToIdToken(_auth: Auth, _idToken: string): Promise { - debugFail('Method not implemented.'); + async _linkToIdToken(auth: Auth, idToken: string): Promise { + switch (this.signInMethod) { + case EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD: + return updateEmailPassword(auth, { + idToken, + returnSecureToken: true, + email: this.email, + password: this.password + }); + case EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD: + return signInWithEmailLinkForLinking(auth, { + idToken, + email: this.email, + oobCode: this.password + }); + default: + fail(auth.name, AuthErrorCode.INTERNAL_ERROR); + } } - _getReauthenticationResolver(_auth: Auth): Promise { - debugFail('Method not implemented.'); + _getReauthenticationResolver(auth: Auth): Promise { + return this._getIdTokenResponse(auth); } } From 22383363c3ad40515c8ea8069121a4fb65901566 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 16 Jun 2020 15:35:06 -0700 Subject: [PATCH 2/4] Formatting --- .../auth-exp/src/api/authentication/email_link.ts | 5 +++-- .../auth-exp/src/core/credentials/email.test.ts | 10 ++++++++-- packages-exp/auth-exp/src/core/credentials/email.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.ts b/packages-exp/auth-exp/src/api/authentication/email_link.ts index 630af3fe471..a14c81cb0bd 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.ts @@ -39,7 +39,8 @@ export async function signInWithEmailLink( >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); } -export interface SignInWithEmailLinkForLinkingRequest extends SignInWithEmailLinkRequest { +export interface SignInWithEmailLinkForLinkingRequest + extends SignInWithEmailLinkRequest { idToken: string; } @@ -51,4 +52,4 @@ export async function signInWithEmailLinkForLinking( SignInWithEmailLinkForLinkingRequest, SignInWithEmailLinkResponse >(auth, HttpMethod.POST, Endpoint.SIGN_IN_WITH_EMAIL_LINK, request); -} \ No newline at end of file +} diff --git a/packages-exp/auth-exp/src/core/credentials/email.test.ts b/packages-exp/auth-exp/src/core/credentials/email.test.ts index 25eae81c902..4cc4727d132 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.test.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.test.ts @@ -98,7 +98,10 @@ describe('core/credentials/email', () => { localId: serverUser.localId! }); - const idTokenResponse = await credential._linkToIdToken(auth, 'id-token-2'); + const idTokenResponse = await credential._linkToIdToken( + auth, + 'id-token-2' + ); expect(idTokenResponse.idToken).to.eq('id-token'); expect(idTokenResponse.refreshToken).to.eq('refresh-token'); expect(idTokenResponse.expiresIn).to.eq('1234'); @@ -176,7 +179,10 @@ describe('core/credentials/email', () => { describe('#_linkToIdToken', () => { it('calls sign in with the new token', async () => { - const idTokenResponse = await credential._linkToIdToken(auth, 'id-token-2'); + const idTokenResponse = await credential._linkToIdToken( + auth, + 'id-token-2' + ); expect(idTokenResponse.idToken).to.eq('id-token'); expect(idTokenResponse.refreshToken).to.eq('refresh-token'); expect(idTokenResponse.expiresIn).to.eq('1234'); diff --git a/packages-exp/auth-exp/src/core/credentials/email.ts b/packages-exp/auth-exp/src/core/credentials/email.ts index 6c47d40bef2..a4dee25afa3 100644 --- a/packages-exp/auth-exp/src/core/credentials/email.ts +++ b/packages-exp/auth-exp/src/core/credentials/email.ts @@ -20,7 +20,8 @@ import * as externs from '@firebase/auth-types-exp'; import { updateEmailPassword } from '../../api/account_management/email_and_password'; import { signInWithPassword } from '../../api/authentication/email_and_password'; import { - signInWithEmailLink, signInWithEmailLinkForLinking + signInWithEmailLink, + signInWithEmailLinkForLinking } from '../../api/authentication/email_link'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; From 23e852f7b4c51e102690a23b2c53af18973fab25 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 16 Jun 2020 15:39:06 -0700 Subject: [PATCH 3/4] Add tests for new api method --- .../src/api/authentication/email_link.test.ts | 134 ++++++++++++------ 1 file changed, 93 insertions(+), 41 deletions(-) diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts index e9bd649b170..12e8be9d7d5 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts @@ -26,16 +26,11 @@ import { testAuth } from '../../../test/mock_auth'; import * as mockFetch from '../../../test/mock_fetch'; import { Auth } from '../../model/auth'; import { ServerError } from '../errors'; -import { signInWithEmailLink } from './email_link'; +import { signInWithEmailLink, signInWithEmailLinkForLinking } from './email_link'; use(chaiAsPromised); -describe('api/authentication/signInWithEmailLink', () => { - const request = { - email: 'foo@bar.com', - oobCode: 'my-code' - }; - +describe('api/authentication/email_link', () => { let auth: Auth; beforeEach(async () => { @@ -45,44 +40,101 @@ describe('api/authentication/signInWithEmailLink', () => { afterEach(mockFetch.tearDown); - it('should POST to the correct endpoint', async () => { - const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { - displayName: 'my-name', - email: 'test@foo.com' + + context('signInWithEmailLink', () => { + const request = { + email: 'foo@bar.com', + oobCode: 'my-code' + }; + + it('should POST to the correct endpoint', async () => { + const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + displayName: 'my-name', + email: 'test@foo.com' + }); + + const response = await signInWithEmailLink(auth, request); + expect(response.displayName).to.eq('my-name'); + expect(response.email).to.eq('test@foo.com'); + expect(mock.calls[0].request).to.eql(request); + expect(mock.calls[0].method).to.eq('POST'); + expect(mock.calls[0].headers).to.eql({ + 'Content-Type': 'application/json', + 'X-Client-Version': 'testSDK/0.0.0' + }); }); - const response = await signInWithEmailLink(auth, request); - expect(response.displayName).to.eq('my-name'); - expect(response.email).to.eq('test@foo.com'); - expect(mock.calls[0].request).to.eql(request); - expect(mock.calls[0].method).to.eq('POST'); - expect(mock.calls[0].headers).to.eql({ - 'Content-Type': 'application/json', - 'X-Client-Version': 'testSDK/0.0.0' + it('should handle errors', async () => { + const mock = mockEndpoint( + Endpoint.SIGN_IN_WITH_EMAIL_LINK, + { + error: { + code: 400, + message: ServerError.INVALID_EMAIL, + errors: [ + { + message: ServerError.INVALID_EMAIL + } + ] + } + }, + 400 + ); + + await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith( + FirebaseError, + 'Firebase: The email address is badly formatted. (auth/invalid-email).' + ); + expect(mock.calls[0].request).to.eql(request); }); }); - it('should handle errors', async () => { - const mock = mockEndpoint( - Endpoint.SIGN_IN_WITH_EMAIL_LINK, - { - error: { - code: 400, - message: ServerError.INVALID_EMAIL, - errors: [ - { - message: ServerError.INVALID_EMAIL - } - ] - } - }, - 400 - ); - - await expect(signInWithEmailLink(auth, request)).to.be.rejectedWith( - FirebaseError, - 'Firebase: The email address is badly formatted. (auth/invalid-email).' - ); - expect(mock.calls[0].request).to.eql(request); + context('signInWithEmailLinkForLinking', () => { + const request = { + email: 'foo@bar.com', + oobCode: 'my-code', + idToken: 'id-token-2', + }; + + it('should POST to the correct endpoint', async () => { + const mock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + displayName: 'my-name', + email: 'test@foo.com' + }); + + const response = await signInWithEmailLinkForLinking(auth, request); + expect(response.displayName).to.eq('my-name'); + expect(response.email).to.eq('test@foo.com'); + expect(mock.calls[0].request).to.eql(request); + expect(mock.calls[0].method).to.eq('POST'); + expect(mock.calls[0].headers).to.eql({ + 'Content-Type': 'application/json', + 'X-Client-Version': 'testSDK/0.0.0' + }); + }); + + it('should handle errors', async () => { + const mock = mockEndpoint( + Endpoint.SIGN_IN_WITH_EMAIL_LINK, + { + error: { + code: 400, + message: ServerError.INVALID_EMAIL, + errors: [ + { + message: ServerError.INVALID_EMAIL + } + ] + } + }, + 400 + ); + + await expect(signInWithEmailLinkForLinking(auth, request)).to.be.rejectedWith( + FirebaseError, + 'Firebase: The email address is badly formatted. (auth/invalid-email).' + ); + expect(mock.calls[0].request).to.eql(request); + }); }); }); From dac1ad04f86fb416125ce207b1de15308df2db50 Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Tue, 16 Jun 2020 15:39:22 -0700 Subject: [PATCH 4/4] Formatting --- .../src/api/authentication/email_link.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts index 12e8be9d7d5..abd8706f43d 100644 --- a/packages-exp/auth-exp/src/api/authentication/email_link.test.ts +++ b/packages-exp/auth-exp/src/api/authentication/email_link.test.ts @@ -26,7 +26,10 @@ import { testAuth } from '../../../test/mock_auth'; import * as mockFetch from '../../../test/mock_fetch'; import { Auth } from '../../model/auth'; import { ServerError } from '../errors'; -import { signInWithEmailLink, signInWithEmailLinkForLinking } from './email_link'; +import { + signInWithEmailLink, + signInWithEmailLinkForLinking +} from './email_link'; use(chaiAsPromised); @@ -40,7 +43,6 @@ describe('api/authentication/email_link', () => { afterEach(mockFetch.tearDown); - context('signInWithEmailLink', () => { const request = { email: 'foo@bar.com', @@ -93,7 +95,7 @@ describe('api/authentication/email_link', () => { const request = { email: 'foo@bar.com', oobCode: 'my-code', - idToken: 'id-token-2', + idToken: 'id-token-2' }; it('should POST to the correct endpoint', async () => { @@ -130,7 +132,9 @@ describe('api/authentication/email_link', () => { 400 ); - await expect(signInWithEmailLinkForLinking(auth, request)).to.be.rejectedWith( + await expect( + signInWithEmailLinkForLinking(auth, request) + ).to.be.rejectedWith( FirebaseError, 'Firebase: The email address is badly formatted. (auth/invalid-email).' );