Skip to content
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
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dependencies": {
"@firebase-ui/translations": "workspace:*",
"nanostores": "catalog:",
"qrcode-generator": "^2.0.4",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
90 changes: 90 additions & 0 deletions packages/core/src/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
signInWithCredential,
signInAnonymously,
signInWithProvider,
generateTotpQrCode,
} from "./auth";

vi.mock("firebase/auth", () => ({
Expand Down Expand Up @@ -56,6 +57,7 @@ import {
Auth,
ConfirmationResult,
AuthProvider,
TotpSecret,
} from "firebase/auth";
import { hasBehavior, getBehavior } from "./behaviors";
import { handleFirebaseError } from "./errors";
Expand Down Expand Up @@ -924,3 +926,91 @@ describe("signInWithProvider", () => {
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["idle"]]);
});
});

describe("generateTotpQrCode", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should generate QR code successfully with authenticated user", () => {
const mockUI = createMockUI({
auth: { currentUser: { email: "[email protected]" } } as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn().mockReturnValue("otpauth://totp/[email protected]?secret=ABC123&issuer=TestApp"),
} as unknown as TotpSecret;

const result = generateTotpQrCode(mockUI, mockSecret);

expect(mockSecret.generateQrCodeUrl).toHaveBeenCalledWith("[email protected]", undefined);
expect(result).toMatch(/^data:image\/gif;base64,/);
});

it("should generate QR code with custom account name and issuer", () => {
const mockUI = createMockUI({
auth: { currentUser: { email: "[email protected]" } } as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn().mockReturnValue("otpauth://totp/CustomAccount?secret=ABC123&issuer=CustomIssuer"),
} as unknown as TotpSecret;

const result = generateTotpQrCode(mockUI, mockSecret, "CustomAccount", "CustomIssuer");

expect(mockSecret.generateQrCodeUrl).toHaveBeenCalledWith("CustomAccount", "CustomIssuer");
expect(result).toMatch(/^data:image\/gif;base64,/);
});

it("should use user email as account name when no custom account name provided", () => {
const mockUI = createMockUI({
auth: { currentUser: { email: "[email protected]" } } as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn().mockReturnValue("otpauth://totp/[email protected]?secret=ABC123"),
} as unknown as TotpSecret;

generateTotpQrCode(mockUI, mockSecret);

expect(mockSecret.generateQrCodeUrl).toHaveBeenCalledWith("[email protected]", undefined);
});

it("should use empty string as account name when user has no email", () => {
const mockUI = createMockUI({
auth: { currentUser: { email: null } } as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn().mockReturnValue("otpauth://totp/?secret=ABC123"),
} as unknown as TotpSecret;

generateTotpQrCode(mockUI, mockSecret);

expect(mockSecret.generateQrCodeUrl).toHaveBeenCalledWith("", undefined);
});

it("should throw error when user is not authenticated", () => {
const mockUI = createMockUI({
auth: { currentUser: null } as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn(),
} as unknown as TotpSecret;

expect(() => generateTotpQrCode(mockUI, mockSecret)).toThrow(
"User must be authenticated to generate a TOTP QR code"
);
expect(mockSecret.generateQrCodeUrl).not.toHaveBeenCalled();
});

it("should throw error when currentUser is undefined", () => {
const mockUI = createMockUI({
auth: { currentUser: undefined } as unknown as Auth,
});
const mockSecret = {
generateQrCodeUrl: vi.fn(),
} as unknown as TotpSecret;

expect(() => generateTotpQrCode(mockUI, mockSecret)).toThrow(
"User must be authenticated to generate a TOTP QR code"
);
expect(mockSecret.generateQrCodeUrl).not.toHaveBeenCalled();
});
});
22 changes: 22 additions & 0 deletions packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import {
PhoneAuthProvider,
UserCredential,
AuthCredential,
TotpSecret,
} from "firebase/auth";
import QRCode from "qrcode-generator";
import { FirebaseUIConfiguration } from "./config";
import { handleFirebaseError } from "./errors";
import { hasBehavior, getBehavior } from "./behaviors/index";
Expand Down Expand Up @@ -274,3 +276,23 @@ export async function completeEmailLinkSignIn(
window.localStorage.removeItem("emailForSignIn");
}
}

export function generateTotpQrCode(
ui: FirebaseUIConfiguration,
secret: TotpSecret,
accountName?: string,
issuer?: string
): string {
const currentUser = ui.auth.currentUser;

if (!currentUser) {
throw new Error("User must be authenticated to generate a TOTP QR code");
}

const uri = secret.generateQrCodeUrl(accountName || currentUser.email || "", issuer);

const qr = QRCode(0, "L");
qr.addData(uri);
qr.make();
return qr.createDataURL();
}
Loading