diff --git a/packages-exp/auth-exp/src/core/credentials/anonymous.test.ts b/packages-exp/auth-exp/src/core/credentials/anonymous.test.ts new file mode 100644 index 00000000000..f3700b4cc2f --- /dev/null +++ b/packages-exp/auth-exp/src/core/credentials/anonymous.test.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; +import * as mockFetch from '../../../test/mock_fetch'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { testAuth } from '../../../test/mock_auth'; +import { Auth } from '../../model/auth'; +import { AnonymousCredential } from './anonymous'; +import { mockEndpoint } from '../../../test/api/helper'; +import { Endpoint } from '../../api'; +import { APIUserInfo } from '../../api/account_management/account'; + +use(chaiAsPromised); + +describe('core/credentials/anonymous', () => { + let auth: Auth; + let credential: AnonymousCredential; + + beforeEach(async () => { + auth = await testAuth(); + credential = new AnonymousCredential(); + }); + + it('should have an anonymous provider', () => { + expect(credential.providerId).to.eq(ProviderId.ANONYMOUS); + }); + + it('should have an anonymous sign in method', () => { + expect(credential.signInMethod).to.eq(SignInMethod.ANONYMOUS); + }); + + describe('#toJSON', () => { + it('throws', () => { + expect(credential.toJSON).to.throw(Error); + }); + }); + + describe('#_getIdTokenResponse', () => { + const serverUser: APIUserInfo = { + localId: 'local-id' + }; + + beforeEach(() => { + mockFetch.setUp(); + mockEndpoint(Endpoint.SIGN_UP, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + }); + afterEach(mockFetch.tearDown); + + it('calls signUp', 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); + }); + }); + + describe('#_linkToIdToken', () => { + it('throws', async () => { + await expect( + credential._linkToIdToken(auth, 'id-token') + ).to.be.rejectedWith(Error); + }); + }); + + describe('#_matchIdTokenWithUid', () => { + it('throws', () => { + expect(() => credential._matchIdTokenWithUid(auth, 'other-uid')).to.throw( + Error + ); + }); + }); +}); diff --git a/packages-exp/auth-exp/src/core/credentials/anonymous.ts b/packages-exp/auth-exp/src/core/credentials/anonymous.ts new file mode 100644 index 00000000000..3a37f841268 --- /dev/null +++ b/packages-exp/auth-exp/src/core/credentials/anonymous.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; +import { signUp } from '../../api/authentication/sign_up'; +import { Auth } from '../../model/auth'; +import { IdTokenResponse } from '../../model/id_token'; +import { debugFail } from '../util/assert'; +import { AuthCredential } from '.'; + +export class AnonymousCredential implements AuthCredential { + providerId = ProviderId.ANONYMOUS; + signInMethod = SignInMethod.ANONYMOUS; + + toJSON(): never { + debugFail('Method not implemented.'); + } + + static fromJSON(_json: object | string): AnonymousCredential | null { + debugFail('Method not implemented'); + } + + async _getIdTokenResponse(auth: Auth): Promise { + return signUp(auth, { + returnSecureToken: true + }); + } + + async _linkToIdToken(_auth: Auth, _idToken: string): Promise { + debugFail("Can't link to an anonymous credential"); + } + + _matchIdTokenWithUid(_auth: Auth, _uid: string): Promise { + debugFail('Method not implemented.'); + } +} diff --git a/packages-exp/auth-exp/src/core/credentials/email.test.ts b/packages-exp/auth-exp/src/core/credentials/email.test.ts new file mode 100644 index 00000000000..acf2dd5d08a --- /dev/null +++ b/packages-exp/auth-exp/src/core/credentials/email.test.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { testAuth } from '../../../test/mock_auth'; +import { Auth } from '../../model/auth'; +import { EmailAuthProvider } from '../providers/email'; +import { EmailAuthCredential } from './email'; +import * as mockFetch from '../../../test/mock_fetch'; +import { mockEndpoint } from '../../../test/api/helper'; +import { Endpoint } from '../../api'; +import { APIUserInfo } from '../../api/account_management/account'; + +use(chaiAsPromised); + +describe('core/credentials/email', () => { + let auth: Auth; + let apiMock: mockFetch.Route; + const serverUser: APIUserInfo = { + localId: 'local-id' + }; + + beforeEach(async () => { + auth = await testAuth(); + }); + + context('email & password', () => { + const credential = new EmailAuthCredential( + 'some-email', + 'some-password', + EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD + ); + + beforeEach(() => { + mockFetch.setUp(); + apiMock = mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + }); + afterEach(mockFetch.tearDown); + + it('should have an email provider', () => { + expect(credential.providerId).to.eq(ProviderId.PASSWORD); + }); + + it('should have an anonymous sign in method', () => { + expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_PASSWORD); + }); + + describe('#toJSON', () => { + it('throws', () => { + expect(credential.toJSON).to.throw(Error); + }); + }); + + describe('#_getIdTokenResponse', () => { + it('call 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' + }); + }); + }); + + describe('#_linkToIdToken', () => { + it('throws', async () => { + await expect( + credential._linkToIdToken(auth, 'id-token') + ).to.be.rejectedWith(Error); + }); + }); + + describe('#_matchIdTokenWithUid', () => { + it('throws', () => { + expect(() => + credential._matchIdTokenWithUid(auth, 'other-uid') + ).to.throw(Error); + }); + }); + }); + + context('email link', () => { + const credential = new EmailAuthCredential( + 'some-email', + 'oob-code', + EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + ); + + beforeEach(() => { + mockFetch.setUp(); + apiMock = mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + }); + afterEach(mockFetch.tearDown); + + it('should have an email provider', () => { + expect(credential.providerId).to.eq(ProviderId.PASSWORD); + }); + + it('should have an anonymous sign in method', () => { + expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_LINK); + }); + + describe('#toJSON', () => { + it('throws', () => { + expect(credential.toJSON).to.throw(Error); + }); + }); + + describe('#_getIdTokenResponse', () => { + 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' + }); + }); + }); + + describe('#_linkToIdToken', () => { + it('throws', async () => { + await expect( + credential._linkToIdToken(auth, 'id-token') + ).to.be.rejectedWith(Error); + }); + }); + + describe('#_matchIdTokenWithUid', () => { + it('throws', () => { + expect(() => + credential._matchIdTokenWithUid(auth, 'other-uid') + ).to.throw(Error); + }); + }); + }); +}); diff --git a/packages-exp/auth-exp/src/core/credentials/email.ts b/packages-exp/auth-exp/src/core/credentials/email.ts new file mode 100644 index 00000000000..2f6266ca88a --- /dev/null +++ b/packages-exp/auth-exp/src/core/credentials/email.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as externs from '@firebase/auth-types-exp'; +import { signInWithPassword } from '../../api/authentication/email_and_password'; +import { signInWithEmailLink } from '../../api/authentication/email_link'; +import { Auth } from '../../model/auth'; +import { IdTokenResponse } from '../../model/id_token'; +import { AuthErrorCode, AUTH_ERROR_FACTORY } from '../errors'; +import { EmailAuthProvider } from '../providers/email'; +import { debugFail } from '../util/assert'; +import { AuthCredential } from '.'; + +export class EmailAuthCredential implements AuthCredential { + readonly providerId = EmailAuthProvider.PROVIDER_ID; + + constructor( + readonly email: string, + readonly password: string, + readonly signInMethod: externs.SignInMethod + ) {} + + toJSON(): never { + debugFail('Method not implemented.'); + } + + static fromJSON(_json: object | string): EmailAuthCredential | null { + debugFail('Method not implemented'); + } + + async _getIdTokenResponse(auth: Auth): Promise { + switch (this.signInMethod) { + case EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD: + return signInWithPassword(auth, { + returnSecureToken: true, + email: this.email, + password: this.password + }); + case EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD: + return signInWithEmailLink(auth, { + email: this.email, + oobCode: this.password + }); + default: + throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, { + appName: auth.name + }); + } + } + + async _linkToIdToken(_auth: Auth, _idToken: string): Promise { + debugFail('Method not implemented.'); + } + + _matchIdTokenWithUid(_auth: Auth, _uid: string): Promise { + debugFail('Method not implemented.'); + } +} diff --git a/packages-exp/auth-exp/src/model/auth_credential.d.ts b/packages-exp/auth-exp/src/core/credentials/index.d.ts similarity index 73% rename from packages-exp/auth-exp/src/model/auth_credential.d.ts rename to packages-exp/auth-exp/src/core/credentials/index.d.ts index 1c6800a096e..d9215b56b64 100644 --- a/packages-exp/auth-exp/src/model/auth_credential.d.ts +++ b/packages-exp/auth-exp/src/core/credentials/index.d.ts @@ -16,15 +16,13 @@ */ import * as externs from '@firebase/auth-types-exp'; +import { PhoneOrOauthTokenResponse } from '../../api/authentication/mfa'; +import { IdTokenResponse } from '../../model/id_token'; +import { Auth } from '../../model/auth'; -import { Auth } from './auth'; -import { IdTokenResponse } from './id_token'; -import { PhoneOrOauthTokenResponse } from '../api/authentication/mfa'; +export abstract class AuthCredential extends externs.AuthCredential { + static fromJSON(json: object | string): AuthCredential | null; -export interface AuthCredential extends externs.AuthCredential { - readonly providerId: externs.ProviderId; - readonly signInMethod: externs.SignInMethod; - toJSON(): object; _getIdTokenResponse(auth: Auth): Promise; _linkToIdToken(auth: Auth, idToken: string): Promise; _matchIdTokenWithUid(auth: Auth, uid: string): Promise; diff --git a/packages-exp/auth-exp/src/core/strategies/phone_credential.test.ts b/packages-exp/auth-exp/src/core/credentials/phone.test.ts similarity index 97% rename from packages-exp/auth-exp/src/core/strategies/phone_credential.test.ts rename to packages-exp/auth-exp/src/core/credentials/phone.test.ts index b35954b05f6..f0859ee43a7 100644 --- a/packages-exp/auth-exp/src/core/strategies/phone_credential.test.ts +++ b/packages-exp/auth-exp/src/core/credentials/phone.test.ts @@ -23,9 +23,9 @@ import * as fetch from '../../../test/mock_fetch'; import { Endpoint } from '../../api'; import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; -import { PhoneAuthCredential } from './phone_credential'; +import { PhoneAuthCredential } from '../credentials/phone'; -describe('core/strategies/phone_credential', () => { +describe('core/credentials/phone', () => { let auth: Auth; beforeEach(async () => { diff --git a/packages-exp/auth-exp/src/core/strategies/phone_credential.ts b/packages-exp/auth-exp/src/core/credentials/phone.ts similarity index 89% rename from packages-exp/auth-exp/src/core/strategies/phone_credential.ts rename to packages-exp/auth-exp/src/core/credentials/phone.ts index 38b07b1d440..36c7f49979d 100644 --- a/packages-exp/auth-exp/src/core/strategies/phone_credential.ts +++ b/packages-exp/auth-exp/src/core/credentials/phone.ts @@ -25,6 +25,7 @@ import { import { Auth } from '../../model/auth'; import { IdTokenResponse } from '../../model/id_token'; import { debugFail } from '../util/assert'; +import { AuthCredential } from '.'; export interface PhoneAuthCredentialParameters { verificationId?: string; @@ -33,7 +34,8 @@ export interface PhoneAuthCredentialParameters { temporaryProof?: string; } -export class PhoneAuthCredential implements externs.AuthCredential { +export class PhoneAuthCredential + implements AuthCredential, externs.PhoneAuthCredential { readonly providerId = externs.ProviderId.PHONE; readonly signInMethod = externs.SignInMethod.PHONE; @@ -92,7 +94,7 @@ export class PhoneAuthCredential implements externs.AuthCredential { return obj; } - static fromJSON(json: string | object): externs.AuthCredential | null { + static fromJSON(json: object | string): PhoneAuthCredential | null { if (typeof json === 'string') { json = JSON.parse(json); } @@ -120,10 +122,3 @@ export class PhoneAuthCredential implements externs.AuthCredential { }); } } - -/** PhoneAuthCredential for public export; has a private constructor */ -export class ExternPhoneAuthCredential extends PhoneAuthCredential { - private constructor(params: PhoneAuthCredentialParameters) { - super(params); - } -} diff --git a/packages-exp/auth-exp/src/core/errors.ts b/packages-exp/auth-exp/src/core/errors.ts index 31089f9b6a4..6b1de2da373 100644 --- a/packages-exp/auth-exp/src/core/errors.ts +++ b/packages-exp/auth-exp/src/core/errors.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +// eslint-disable-next-line import/no-extraneous-dependencies import { ErrorFactory, ErrorMap } from '@firebase/util'; import { AppName } from '../model/auth'; import { User } from '../model/user'; diff --git a/packages-exp/auth-exp/src/core/providers/anonymous.test.ts b/packages-exp/auth-exp/src/core/providers/anonymous.test.ts index b52010a12c6..d1baa242385 100644 --- a/packages-exp/auth-exp/src/core/providers/anonymous.test.ts +++ b/packages-exp/auth-exp/src/core/providers/anonymous.test.ts @@ -18,68 +18,16 @@ import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import { testAuth } from '../../../test/mock_auth'; -import { Auth } from '../../model/auth'; -import { AnonymousCredential, AnonymousProvider } from './anonymous'; +import { AnonymousProvider } from './anonymous'; use(chaiAsPromised); describe('core/providers/anonymous', () => { - let auth: Auth; - - beforeEach(async () => { - auth = await testAuth(); - }); - - describe('AnonymousCredential', () => { - const credential = new AnonymousCredential(); - - it('should have an anonymous provider', () => { + describe('.credential', () => { + it('should return an anonymous credential', () => { + const credential = AnonymousProvider.credential(); expect(credential.providerId).to.eq(ProviderId.ANONYMOUS); - }); - - it('should have an anonymous sign in method', () => { expect(credential.signInMethod).to.eq(SignInMethod.ANONYMOUS); }); - - describe('#toJSON', () => { - it('throws', () => { - expect(credential.toJSON).to.throw(Error); - }); - }); - - describe('#_getIdTokenResponse', () => { - it('throws', async () => { - await expect(credential._getIdTokenResponse(auth)).to.be.rejectedWith( - Error - ); - }); - }); - - describe('#_linkToIdToken', () => { - it('throws', async () => { - await expect( - credential._linkToIdToken(auth, 'id-token') - ).to.be.rejectedWith(Error); - }); - }); - - describe('#_matchIdTokenWithUid', () => { - it('throws', () => { - expect(() => - credential._matchIdTokenWithUid(auth, 'other-uid') - ).to.throw(Error); - }); - }); - }); - - describe('AnonymousProvider', () => { - describe('.credential', () => { - it('should return an anonymous credential', () => { - const credential = AnonymousProvider.credential(); - expect(credential.providerId).to.eq(ProviderId.ANONYMOUS); - expect(credential.signInMethod).to.eq(SignInMethod.ANONYMOUS); - }); - }); }); }); diff --git a/packages-exp/auth-exp/src/core/providers/anonymous.ts b/packages-exp/auth-exp/src/core/providers/anonymous.ts index bf079d2331e..f6a47088329 100644 --- a/packages-exp/auth-exp/src/core/providers/anonymous.ts +++ b/packages-exp/auth-exp/src/core/providers/anonymous.ts @@ -15,43 +15,8 @@ * limitations under the License. */ -import { - AuthCredential, - ProviderId, - SignInMethod, - AuthProvider -} from '@firebase/auth-types-exp'; -import { signUp } from '../../api/authentication/sign_up'; -import { Auth } from '../../model/auth'; -import { IdTokenResponse } from '../../model/id_token'; -import { debugFail } from '../util/assert'; - -export class AnonymousCredential implements AuthCredential { - providerId = ProviderId.ANONYMOUS; - signInMethod = SignInMethod.ANONYMOUS; - - toJSON(): never { - debugFail('Method not implemented.'); - } - - static fromJSON(): never { - debugFail('Method not implemented'); - } - - async _getIdTokenResponse(auth: Auth): Promise { - return signUp(auth, { - returnSecureToken: true - }); - } - - async _linkToIdToken(_auth: Auth, _idToken: string): Promise { - debugFail("Can't link to an anonymous credential"); - } - - _matchIdTokenWithUid(_auth: Auth, _uid: string): Promise { - debugFail('Method not implemented.'); - } -} +import { AuthProvider, ProviderId } from '@firebase/auth-types-exp'; +import { AnonymousCredential } from '../credentials/anonymous'; export class AnonymousProvider implements AuthProvider { providerId = ProviderId.ANONYMOUS; diff --git a/packages-exp/auth-exp/src/core/providers/email.test.ts b/packages-exp/auth-exp/src/core/providers/email.test.ts new file mode 100644 index 00000000000..b628ceabe34 --- /dev/null +++ b/packages-exp/auth-exp/src/core/providers/email.test.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { FirebaseError } from '@firebase/util'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { testAuth } from '../../../test/mock_auth'; +import { Auth } from '../../model/auth'; +import { EmailAuthProvider } from './email'; + +use(chaiAsPromised); + +describe('core/providers/email', () => { + let auth: Auth; + + beforeEach(async () => { + auth = await testAuth(); + }); + + describe('.credential', () => { + it('should return an email & password credential', () => { + const credential = EmailAuthProvider.credential( + 'some-email', + 'some-password' + ); + expect(credential.email).to.eq('some-email'); + expect(credential.password).to.eq('some-password'); + expect(credential.providerId).to.eq(ProviderId.PASSWORD); + expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_PASSWORD); + }); + }); + + describe('.credentialWithLink', () => { + it('should return an email link credential', () => { + const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; + const actionLink = + 'https://www.example.com/finishSignIn?' + + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + + 'continueUrl=' + + encodeURIComponent(continueUrl) + + '&languageCode=en&state=bla'; + + const credential = EmailAuthProvider.credentialWithLink( + auth, + 'some-email', + actionLink + ); + expect(credential.email).to.eq('some-email'); + expect(credential.password).to.eq('CODE'); + expect(credential.providerId).to.eq(ProviderId.PASSWORD); + expect(credential.signInMethod).to.eq(SignInMethod.EMAIL_LINK); + }); + + context('invalid email link', () => { + it('should throw an error', () => { + const actionLink = 'https://www.example.com/finishSignIn?'; + expect(() => + EmailAuthProvider.credentialWithLink(auth, 'some-email', actionLink) + ).to.throw(FirebaseError, 'Firebase: Error (auth/argument-error)'); + }); + }); + + context('mismatched tenant ID', () => { + it('should throw an error', () => { + const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; + const actionLink = + 'https://www.example.com/finishSignIn?' + + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + + 'continueUrl=' + + encodeURIComponent(continueUrl) + + '&languageCode=en&tenantId=OTHER_TENANT_ID&state=bla'; + expect(() => + EmailAuthProvider.credentialWithLink(auth, 'some-email', actionLink) + ).to.throw( + FirebaseError, + "Firebase: The provided tenant ID does not match the Auth instance's tenant ID (auth/tenant-id-mismatch)." + ); + }); + }); + }); +}); diff --git a/packages-exp/auth-exp/src/core/providers/email.ts b/packages-exp/auth-exp/src/core/providers/email.ts new file mode 100644 index 00000000000..aee2ed697b8 --- /dev/null +++ b/packages-exp/auth-exp/src/core/providers/email.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as externs from '@firebase/auth-types-exp'; + +import { Auth } from '../../model/auth'; +import { ActionCodeURL } from '../action_code_url'; +import { EmailAuthCredential } from '../credentials/email'; +import { AuthErrorCode } from '../errors'; +import { assert } from '../util/assert'; + +export class EmailAuthProvider implements externs.EmailAuthProvider { + static readonly PROVIDER_ID = externs.ProviderId.PASSWORD; + static readonly EMAIL_PASSWORD_SIGN_IN_METHOD = + externs.SignInMethod.EMAIL_PASSWORD; + static readonly EMAIL_LINK_SIGN_IN_METHOD = externs.SignInMethod.EMAIL_LINK; + readonly providerId = EmailAuthProvider.PROVIDER_ID; + + static credential( + email: string, + password: string, + signInMethod?: externs.SignInMethod + ): EmailAuthCredential { + return new EmailAuthCredential( + email, + password, + signInMethod || this.EMAIL_PASSWORD_SIGN_IN_METHOD + ); + } + + static credentialWithLink( + auth: Auth, + email: string, + emailLink: string + ): EmailAuthCredential { + const actionCodeUrl = ActionCodeURL._fromLink(auth, emailLink); + assert(actionCodeUrl, auth.name, AuthErrorCode.ARGUMENT_ERROR); + + // Check if the tenant ID in the email link matches the tenant ID on Auth + // instance. + assert( + actionCodeUrl.tenantId === (auth.tenantId || null), + auth.name, + AuthErrorCode.TENANT_ID_MISMATCH + ); + + return this.credential( + email, + actionCodeUrl.code, + EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD + ); + } +} diff --git a/packages-exp/auth-exp/src/core/providers/phone.ts b/packages-exp/auth-exp/src/core/providers/phone.ts index ab3fd623e3a..a7a013d989e 100644 --- a/packages-exp/auth-exp/src/core/providers/phone.ts +++ b/packages-exp/auth-exp/src/core/providers/phone.ts @@ -16,15 +16,16 @@ */ import * as externs from '@firebase/auth-types-exp'; -import { FirebaseError } from '@firebase/util'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { FirebaseError } from '@firebase/util'; import { Auth } from '../../model/auth'; import { initializeAuth } from '../auth/auth_impl'; +import { PhoneAuthCredential } from '../credentials/phone'; import { _verifyPhoneNumber } from '../strategies/phone'; -import { PhoneAuthCredential } from '../strategies/phone_credential'; import { debugFail } from '../util/assert'; -export class PhoneAuthProvider implements externs.AuthProvider { +export class PhoneAuthProvider implements externs.PhoneAuthProvider { static readonly PROVIDER_ID = externs.ProviderId.PHONE; static readonly PHONE_SIGN_IN_METHOD = externs.SignInMethod.PHONE; @@ -50,21 +51,21 @@ export class PhoneAuthProvider implements externs.AuthProvider { return new PhoneAuthCredential({ verificationId, verificationCode }); } - static credentialFromResult( + static _credentialFromResult( userCredential: externs.UserCredential ): externs.AuthCredential | null { void userCredential; return debugFail('not implemented'); } - static credentialFromError( + static _credentialFromError( error: FirebaseError ): externs.AuthCredential | null { void error; return debugFail('not implemented'); } - static credentialFromJSON(json: string | object): externs.AuthCredential { + static _credentialFromJSON(json: string | object): externs.AuthCredential { void json; return debugFail('not implemented'); } diff --git a/packages-exp/auth-exp/src/core/strategies/credential.ts b/packages-exp/auth-exp/src/core/strategies/credential.ts index 0e7c26649c7..ff534453ab5 100644 --- a/packages-exp/auth-exp/src/core/strategies/credential.ts +++ b/packages-exp/auth-exp/src/core/strategies/credential.ts @@ -16,10 +16,10 @@ */ import * as externs from '@firebase/auth-types-exp'; -import { OperationType, UserCredential } from '@firebase/auth-types-exp'; +import { OperationType, UserCredential } from '@firebase/auth-types-exp'; import { Auth } from '../../model/auth'; -import { AuthCredential } from '../../model/auth_credential'; +import { AuthCredential } from '../credentials'; import { UserCredentialImpl } from '../user/user_credential_impl'; export async function signInWithCredential( diff --git a/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts b/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts index 10019d7f46a..ebb6376f27d 100644 --- a/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts +++ b/packages-exp/auth-exp/src/core/strategies/email_and_password.test.ts @@ -32,8 +32,15 @@ import { checkActionCode, confirmPasswordReset, sendPasswordResetEmail, - verifyPasswordResetCode + verifyPasswordResetCode, + signInWithEmailAndPassword } from './email_and_password'; +import { APIUserInfo } from '../../api/account_management/account'; +import { + SignInMethod, + OperationType, + ProviderId +} from '@firebase/auth-types-exp'; use(chaiAsPromised); use(sinonChai); @@ -316,3 +323,38 @@ describe('core/strategies/verifyPasswordResetCode', () => { expect(mock.calls.length).to.eq(1); }); }); + +describe('core/strategies/email_and_password/signInWithEmailAndPassword', () => { + let auth: Auth; + const serverUser: APIUserInfo = { + localId: 'local-id' + }; + + beforeEach(async () => { + auth = await testAuth(); + mockFetch.setUp(); + mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { + users: [serverUser] + }); + }); + afterEach(mockFetch.tearDown); + + it('should sign in the user', async () => { + const { + credential, + user, + operationType + } = await signInWithEmailAndPassword(auth, 'some-email', 'some-password'); + expect(credential?.providerId).to.eq(ProviderId.PASSWORD); + expect(credential?.signInMethod).to.eq(SignInMethod.EMAIL_PASSWORD); + expect(operationType).to.eq(OperationType.SIGN_IN); + expect(user.uid).to.eq(serverUser.localId); + expect(user.isAnonymous).to.be.false; + }); +}); diff --git a/packages-exp/auth-exp/src/core/strategies/email_and_password.ts b/packages-exp/auth-exp/src/core/strategies/email_and_password.ts index a18954056a2..d52c6e0a7b0 100644 --- a/packages-exp/auth-exp/src/core/strategies/email_and_password.ts +++ b/packages-exp/auth-exp/src/core/strategies/email_and_password.ts @@ -21,8 +21,10 @@ import { resetPassword } from '../../api/account_management/email_and_password'; import * as api from '../../api/authentication/email_and_password'; import { Operation } from '../../model/action_code_info'; import { Auth } from '../../model/auth'; -import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors'; +import { AuthErrorCode, AUTH_ERROR_FACTORY } from '../errors'; +import { EmailAuthProvider } from '../providers/email'; import { setActionCodeSettingsOnRequest } from './action_code_settings'; +import { signInWithCredential } from './credential'; export async function sendPasswordResetEmail( auth: externs.Auth, @@ -82,3 +84,14 @@ export async function verifyPasswordResetCode( // Email should always be present since a code was sent to it return data.email!; } + +export function signInWithEmailAndPassword( + auth: externs.Auth, + email: string, + password: string +): Promise { + return signInWithCredential( + auth, + EmailAuthProvider.credential(email, password) + ); +} diff --git a/packages-exp/auth-exp/src/core/strategies/email_link.test.ts b/packages-exp/auth-exp/src/core/strategies/email_link.test.ts index 20ac960e5b0..58f4ce3f643 100644 --- a/packages-exp/auth-exp/src/core/strategies/email_link.test.ts +++ b/packages-exp/auth-exp/src/core/strategies/email_link.test.ts @@ -28,7 +28,17 @@ import { Endpoint } from '../../api'; import { ServerError } from '../../api/errors'; import { Operation } from '../../model/action_code_info'; import { Auth } from '../../model/auth'; -import { isSignInWithEmailLink, sendSignInLinkToEmail } from './email_link'; +import { + isSignInWithEmailLink, + sendSignInLinkToEmail, + signInWithEmailLink +} from './email_link'; +import { + ProviderId, + SignInMethod, + OperationType +} from '@firebase/auth-types-exp'; +import { APIUserInfo } from '../../api/account_management/account'; use(chaiAsPromised); use(sinonChai); @@ -193,3 +203,45 @@ describe('core/strategies/isSignInWithEmailLink', () => { }); }); }); + +describe('core/strategies/email_and_password/signInWithEmailLink', () => { + let auth: Auth; + const serverUser: APIUserInfo = { + localId: 'local-id' + }; + + beforeEach(async () => { + auth = await testAuth(); + mockFetch.setUp(); + mockEndpoint(Endpoint.SIGN_IN_WITH_EMAIL_LINK, { + idToken: 'id-token', + refreshToken: 'refresh-token', + expiresIn: '1234', + localId: serverUser.localId! + }); + mockEndpoint(Endpoint.GET_ACCOUNT_INFO, { + users: [serverUser] + }); + }); + afterEach(mockFetch.tearDown); + + it('should sign in the user', async () => { + const continueUrl = 'https://www.example.com/path/to/file?a=1&b=2#c=3'; + const actionLink = + 'https://www.example.com/finishSignIn?' + + 'oobCode=CODE&mode=signIn&apiKey=API_KEY&' + + 'continueUrl=' + + encodeURIComponent(continueUrl) + + '&languageCode=en&state=bla'; + const { credential, user, operationType } = await signInWithEmailLink( + auth, + 'some-email', + actionLink + ); + expect(credential?.providerId).to.eq(ProviderId.PASSWORD); + expect(credential?.signInMethod).to.eq(SignInMethod.EMAIL_LINK); + expect(operationType).to.eq(OperationType.SIGN_IN); + expect(user.uid).to.eq(serverUser.localId); + expect(user.isAnonymous).to.be.false; + }); +}); diff --git a/packages-exp/auth-exp/src/core/strategies/email_link.ts b/packages-exp/auth-exp/src/core/strategies/email_link.ts index 6c10563847a..28c1ec5fe1d 100644 --- a/packages-exp/auth-exp/src/core/strategies/email_link.ts +++ b/packages-exp/auth-exp/src/core/strategies/email_link.ts @@ -21,7 +21,10 @@ import * as api from '../../api/authentication/email_and_password'; import { Operation } from '../../model/action_code_info'; import { Auth } from '../../model/auth'; import { ActionCodeURL } from '../action_code_url'; +import { EmailAuthProvider } from '../providers/email'; +import { _getCurrentUrl } from '../util/location'; import { setActionCodeSettingsOnRequest } from './action_code_settings'; +import { signInWithCredential } from './credential'; export async function sendSignInLinkToEmail( auth: externs.Auth, @@ -46,3 +49,18 @@ export function isSignInWithEmailLink( const actionCodeUrl = ActionCodeURL._fromLink(auth as Auth, emailLink); return actionCodeUrl?.operation === Operation.EMAIL_SIGNIN; } + +export async function signInWithEmailLink( + auth: externs.Auth, + email: string, + emailLink?: string +): Promise { + return signInWithCredential( + auth, + EmailAuthProvider.credentialWithLink( + auth as Auth, + email, + emailLink || _getCurrentUrl() + ) + ); +} diff --git a/packages-exp/auth-exp/src/core/strategies/phone.ts b/packages-exp/auth-exp/src/core/strategies/phone.ts index fa8a8b1f475..30a82e676d6 100644 --- a/packages-exp/auth-exp/src/core/strategies/phone.ts +++ b/packages-exp/auth-exp/src/core/strategies/phone.ts @@ -24,7 +24,7 @@ import { AuthErrorCode } from '../errors'; import { PhoneAuthProvider } from '../providers/phone'; import { assert } from '../util/assert'; import { signInWithCredential } from './credential'; -import { PhoneAuthCredential } from './phone_credential'; +import { PhoneAuthCredential } from '../credentials/phone'; interface OnConfirmationCallback { (credential: PhoneAuthCredential): Promise; diff --git a/packages-exp/auth-exp/src/core/user/reload.test.ts b/packages-exp/auth-exp/src/core/user/reload.test.ts index ca3333b3785..74f690bc6ef 100644 --- a/packages-exp/auth-exp/src/core/user/reload.test.ts +++ b/packages-exp/auth-exp/src/core/user/reload.test.ts @@ -140,7 +140,6 @@ describe('core/user/reload', () => { ] }); await _reloadWithoutSaving(user); - console.warn(user.providerData); expect(user.providerData).to.eql([ { ...BASIC_USER_INFO }, { diff --git a/packages-exp/auth-exp/src/core/user/user_credential_impl.test.ts b/packages-exp/auth-exp/src/core/user/user_credential_impl.test.ts index fa910e143f5..12ee62ae61b 100644 --- a/packages-exp/auth-exp/src/core/user/user_credential_impl.test.ts +++ b/packages-exp/auth-exp/src/core/user/user_credential_impl.test.ts @@ -33,9 +33,9 @@ import * as mockFetch from '../../../test/mock_fetch'; import { Endpoint } from '../../api'; import { APIUserInfo } from '../../api/account_management/account'; import { Auth } from '../../model/auth'; -import { AuthCredential } from '../../model/auth_credential'; import { IdTokenResponse } from '../../model/id_token'; import { UserCredentialImpl } from './user_credential_impl'; +import { AuthCredential } from '../credentials'; use(chaiAsPromised); use(sinonChai); diff --git a/packages-exp/auth-exp/src/core/user/user_credential_impl.ts b/packages-exp/auth-exp/src/core/user/user_credential_impl.ts index 0ba809a0d1f..81d3ee04c08 100644 --- a/packages-exp/auth-exp/src/core/user/user_credential_impl.ts +++ b/packages-exp/auth-exp/src/core/user/user_credential_impl.ts @@ -18,10 +18,10 @@ import * as externs from '@firebase/auth-types-exp'; import { Auth } from '../../model/auth'; -import { AuthCredential } from '../../model/auth_credential'; import { IdTokenResponse } from '../../model/id_token'; import { User, UserCredential } from '../../model/user'; import { UserImpl } from './user_impl'; +import { AuthCredential } from '../credentials'; export class UserCredentialImpl implements UserCredential { constructor( diff --git a/packages-exp/auth-exp/src/index.ts b/packages-exp/auth-exp/src/index.ts index b90ea022118..d2db8095be8 100644 --- a/packages-exp/auth-exp/src/index.ts +++ b/packages-exp/auth-exp/src/index.ts @@ -28,6 +28,7 @@ export { indexedDBLocalPersistence } from './core/persistence/indexed_db'; // core/providers export { AnonymousProvider } from './core/providers/anonymous'; +export { EmailAuthProvider } from './core/providers/email'; export { PhoneAuthProvider } from './core/providers/phone'; // core/strategies @@ -38,17 +39,18 @@ export { sendPasswordResetEmail, confirmPasswordReset, checkActionCode, - verifyPasswordResetCode + verifyPasswordResetCode, + signInWithEmailAndPassword } from './core/strategies/email_and_password'; export { sendSignInLinkToEmail, - isSignInWithEmailLink + isSignInWithEmailLink, + signInWithEmailLink } from './core/strategies/email_link'; export { fetchSignInMethodsForEmail, sendEmailVerification } from './core/strategies/email'; -export { ExternPhoneAuthCredential as PhoneAuthCredential } from './core/strategies/phone_credential'; export { signInWithPhoneNumber } from './core/strategies/phone'; // core/user diff --git a/packages-exp/auth-exp/test/mock_auth_credential.ts b/packages-exp/auth-exp/test/mock_auth_credential.ts index 891c5f87794..c0bf5d8c44c 100644 --- a/packages-exp/auth-exp/test/mock_auth_credential.ts +++ b/packages-exp/auth-exp/test/mock_auth_credential.ts @@ -18,8 +18,8 @@ import { ProviderId, SignInMethod } from '@firebase/auth-types-exp'; import { PhoneOrOauthTokenResponse } from '../src/api/authentication/mfa'; import { Auth } from '../src/model/auth'; -import { AuthCredential } from '../src/model/auth_credential'; import { IdTokenResponse } from '../src/model/id_token'; +import { AuthCredential } from '../src/core/credentials'; export class MockAuthCredential implements AuthCredential { response?: PhoneOrOauthTokenResponse; diff --git a/packages-exp/auth-types-exp/index.d.ts b/packages-exp/auth-types-exp/index.d.ts index 8be3ddea88e..53c41d23108 100644 --- a/packages-exp/auth-types-exp/index.d.ts +++ b/packages-exp/auth-types-exp/index.d.ts @@ -184,21 +184,23 @@ export const enum SignInMethod { } export abstract class AuthCredential { + static fromJSON(json: object | string): AuthCredential | null; + readonly providerId: ProviderId; readonly signInMethod: SignInMethod; toJSON(): object; } -export abstract class OAuthCredential implements AuthCredential { +export abstract class OAuthCredential extends AuthCredential { + static fromJSON(json: object | string): OAuthCredential | null; + readonly accessToken?: string; readonly idToken?: string; readonly secret?: string; - readonly providerId: ProviderId; - readonly signInMethod: SignInMethod; - - constructor(); +} - toJSON(): object; +export abstract class PhoneAuthCredential extends AuthCredential { + static fromJSON(json: object | string): PhoneAuthCredential | null; } export const enum OperationType { @@ -236,3 +238,42 @@ export interface PhoneInfoOptions { export interface AuthProvider { readonly providerId: ProviderId; } + +/** + * A provider for generating phone credentials + */ +export class PhoneAuthProvider implements AuthProvider { + static readonly PROVIDER_ID: string; + static readonly PHONE_SIGN_IN_METHOD: string; + static credential( + verificationId: string, + verificationCode: string + ): AuthCredential; + + constructor(auth?: Auth | null); + + readonly providerId: ProviderId; + + verifyPhoneNumber( + phoneNumber: string, + applicationVerifier: ApplicationVerifier + /* multiFactorSession?: MultiFactorSession */ + ): Promise; +} + +/** + * A provider for generating email & password and email link credentials + */ +export abstract class EmailAuthProvider implements AuthProvider { + private constructor(); + static readonly PROVIDER_ID: string; + static readonly EMAIL_PASSWORD_SIGN_IN_METHOD: string; + static readonly EMAIL_LINK_SIGN_IN_METHOD: string; + static credential(email: string, password: string): AuthCredential; + static credentialWithLink( + auth: Auth, + email: string, + emailLink: string + ): AuthCredential; + readonly providerId: ProviderId; +}