Skip to content

Add custom email flow methods to auth-next #3223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +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 { resetPassword, updateEmailPassword } from './email_and_password';
import {
applyActionCode,
resetPassword,
updateEmailPassword
} from './email_and_password';

use(chaiAsPromised);

Expand Down Expand Up @@ -141,3 +145,55 @@ describe('api/account_management/updateEmailPassword', () => {
expect(mock.calls[0].request).to.eql(request);
});
});

describe('api/account_management/applyActionCode', () => {
const request = {
oobCode: 'oob-code'
};

let auth: Auth;

beforeEach(async () => {
auth = await testAuth();
mockFetch.setUp();
});

afterEach(mockFetch.tearDown);

it('should POST to the correct endpoint', async () => {
const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});

const response = await applyActionCode(auth, request);
expect(response).to.be.empty;
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.SET_ACCOUNT_INFO,
{
error: {
code: 400,
message: ServerError.INVALID_OOB_CODE,
errors: [
{
message: ServerError.INVALID_OOB_CODE
}
]
}
},
400
);

await expect(applyActionCode(auth, request)).to.be.rejectedWith(
FirebaseError,
'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
);
expect(mock.calls[0].request).to.eql(request);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,21 @@ export async function updateEmailPassword(
UpdateEmailPasswordResponse
>(auth, HttpMethod.POST, Endpoint.SET_ACCOUNT_INFO, request);
}

export interface ApplyActionCodeRequest {
oobCode: string;
}

export interface ApplyActionCodeResponse {}

export async function applyActionCode(
auth: Auth,
request: ApplyActionCodeRequest
): Promise<ApplyActionCodeResponse> {
return _performApiRequest<ApplyActionCodeRequest, ApplyActionCodeResponse>(
auth,
HttpMethod.POST,
Endpoint.SET_ACCOUNT_INFO,
request
);
}
28 changes: 14 additions & 14 deletions packages-exp/auth-exp/src/core/action_code_url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('core/action_code_url', () => {
'continueUrl=' +
encodeURIComponent(continueUrl) +
'&languageCode=en&tenantId=TENANT_ID&state=bla';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
expect(actionCodeUrl!.code).to.eq('CODE');
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
Expand All @@ -54,7 +54,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=signIn&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
});

Expand All @@ -63,7 +63,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=verifyAndChangeEmail&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(
Operation.VERIFY_AND_CHANGE_EMAIL
);
Expand All @@ -74,7 +74,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=verifyEmail&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.VERIFY_EMAIL);
});

Expand All @@ -83,7 +83,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=recoverEmail&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.RECOVER_EMAIL);
});

Expand All @@ -92,7 +92,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=resetPassword&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.PASSWORD_RESET);
});

Expand All @@ -101,7 +101,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE&mode=revertSecondFactorAddition&apiKey=API_KEY&' +
'languageCode=en';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(
Operation.REVERT_SECOND_FACTOR_ADDITION
);
Expand All @@ -112,7 +112,7 @@ describe('core/action_code_url', () => {
const actionLink =
'https://www.example.com:8080/finishSignIn?' +
'oobCode=CODE&mode=signIn&apiKey=API_KEY&state=bla';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
expect(actionCodeUrl!.code).to.eq('CODE');
expect(actionCodeUrl!.apiKey).to.eq('API_KEY');
Expand All @@ -126,7 +126,7 @@ describe('core/action_code_url', () => {
'https://www.example.com/finishSignIn?' +
'oobCode=CODE1&mode=signIn&apiKey=API_KEY1&state=bla' +
'#oobCode=CODE2&mode=signIn&apiKey=API_KEY2&state=bla';
const actionCodeUrl = ActionCodeURL._fromLink(auth, actionLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, actionLink);
expect(actionCodeUrl!.operation).to.eq(Operation.EMAIL_SIGNIN);
expect(actionCodeUrl!.code).to.eq('CODE1');
expect(actionCodeUrl!.apiKey).to.eq('API_KEY1');
Expand All @@ -138,31 +138,31 @@ describe('core/action_code_url', () => {
context('invalid links', () => {
it('should handle missing API key, code & mode', () => {
const actionLink = 'https://www.example.com/finishSignIn';
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
});

it('should handle invalid mode', () => {
const actionLink =
'https://www.example.com/finishSignIn?oobCode=CODE&mode=INVALID_MODE&apiKey=API_KEY';
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
});

it('should handle missing code', () => {
const actionLink =
'https://www.example.com/finishSignIn?mode=signIn&apiKey=API_KEY';
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
});

it('should handle missing API key', () => {
const actionLink =
'https://www.example.com/finishSignIn?oobCode=CODE&mode=signIn';
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
});

it('should handle missing mode', () => {
const actionLink =
'https://www.example.com/finishSignIn?oobCode=CODE&apiKey=API_KEY';
expect(ActionCodeURL._fromLink(auth, actionLink)).to.be.null;
expect(ActionCodeURL.parseLink(auth, actionLink)).to.be.null;
});
});
});
Expand Down
18 changes: 13 additions & 5 deletions packages-exp/auth-exp/src/core/action_code_url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
*/

import * as externs from '@firebase/auth-types-exp';

import { Auth } from '../model/auth';
import { AUTH_ERROR_FACTORY, AuthErrorCode } from './errors';
import { AuthErrorCode, AUTH_ERROR_FACTORY } from './errors';

/**
* Enums for fields in URL query string.
Expand Down Expand Up @@ -73,7 +71,7 @@ export class ActionCodeURL implements externs.ActionCodeURL {
readonly operation: externs.Operation;
readonly tenantId: string | null;

constructor(auth: Auth, actionLink: string) {
constructor(auth: externs.Auth, actionLink: string) {
const uri = new URL(actionLink);
const apiKey = uri.searchParams.get(QueryField.API_KEY);
const code = uri.searchParams.get(QueryField.CODE);
Expand All @@ -92,7 +90,10 @@ export class ActionCodeURL implements externs.ActionCodeURL {
this.tenantId = uri.searchParams.get(QueryField.TENANT_ID);
}

static _fromLink(auth: Auth, link: string): ActionCodeURL | null {
static parseLink(
auth: externs.Auth,
link: string
): externs.ActionCodeURL | null {
const actionLink = parseDeepLink(link);
try {
return new ActionCodeURL(auth, actionLink);
Expand All @@ -101,3 +102,10 @@ export class ActionCodeURL implements externs.ActionCodeURL {
}
}
}

export function parseActionCodeURL(
auth: externs.Auth,
link: string
): externs.ActionCodeURL | null {
return ActionCodeURL.parseLink(auth, link);
}
2 changes: 1 addition & 1 deletion packages-exp/auth-exp/src/core/providers/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class EmailAuthProvider implements externs.EmailAuthProvider {
email: string,
emailLink: string
): EmailAuthCredential {
const actionCodeUrl = ActionCodeURL._fromLink(auth, emailLink);
const actionCodeUrl = ActionCodeURL.parseLink(auth, emailLink);
assert(actionCodeUrl, auth.name, AuthErrorCode.ARGUMENT_ERROR);

// Check if the tenant ID in the email link matches the tenant ID on Auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ import {
createUserWithEmailAndPassword,
sendPasswordResetEmail,
signInWithEmailAndPassword,
verifyPasswordResetCode
verifyPasswordResetCode,
applyActionCode
} from './email_and_password';

use(chaiAsPromised);
Expand Down Expand Up @@ -187,6 +188,45 @@ describe('core/strategies/confirmPasswordReset', () => {
});
});

describe('core/strategies/applyActionCode', () => {
const oobCode = 'oob-code';

let auth: Auth;

beforeEach(async () => {
auth = await testAuth();
mockFetch.setUp();
});

afterEach(mockFetch.tearDown);

it('should apply the oob code', async () => {
const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
await applyActionCode(auth, oobCode);
expect(mock.calls[0].request).to.eql({
oobCode
});
});

it('should surface errors', async () => {
const mock = mockEndpoint(
Endpoint.SET_ACCOUNT_INFO,
{
error: {
code: 400,
message: ServerError.INVALID_OOB_CODE
}
},
400
);
await expect(applyActionCode(auth, oobCode)).to.be.rejectedWith(
FirebaseError,
'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
);
expect(mock.calls.length).to.eq(1);
});
});

describe('core/strategies/checkActionCode', () => {
const oobCode = 'oob-code';
const email = '[email protected]';
Expand All @@ -210,7 +250,7 @@ describe('core/strategies/checkActionCode', () => {
expect(response).to.eql({
data: {
email,
fromEmail: null
previousEmail: null
},
operation: Operation.PASSWORD_RESET
});
Expand All @@ -229,7 +269,7 @@ describe('core/strategies/checkActionCode', () => {
expect(response).to.eql({
data: {
email,
fromEmail: newEmail
previousEmail: newEmail
},
operation: Operation.PASSWORD_RESET
});
Expand Down Expand Up @@ -284,7 +324,8 @@ describe('core/strategies/verifyPasswordResetCode', () => {
it('should verify the oob code', async () => {
const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
requestType: Operation.PASSWORD_RESET,
email: '[email protected]'
email: '[email protected]',
previousEmail: null
});
const response = await verifyPasswordResetCode(auth, oobCode);
expect(response).to.eq(email);
Expand Down
Loading