diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx
index 8ec5592c47a0c..4ced2cdf8e5c2 100644
--- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx
+++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.story.tsx
@@ -26,11 +26,38 @@ export default {
title: 'Teleport/AuthnDialog',
};
-export const Loaded = () => ;
+export const Loaded = () => {
+ const props: Props = {
+ ...defaultProps,
+ mfa: {
+ ...defaultProps.mfa,
+ ssoChallenge: {
+ redirectUrl: 'hi',
+ requestId: '123',
+ channelId: '123',
+ device: {
+ connectorId: '123',
+ connectorType: 'saml',
+ displayName: 'Okta',
+ },
+ },
+ },
+ };
+ return ;
+};
-export const Error = () => ;
+export const Error = () => {
+ const props: Props = {
+ ...defaultProps,
+ mfa: {
+ ...defaultProps.mfa,
+ errorText: 'Something went wrong',
+ },
+ };
+ return ;
+};
-const props: Props = {
+const defaultProps: Props = {
mfa: makeDefaultMfaState(),
onCancel: () => null,
};
diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx
new file mode 100644
index 0000000000000..cc6e864a8073b
--- /dev/null
+++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.test.tsx
@@ -0,0 +1,97 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { render, screen, fireEvent } from 'design/utils/testing';
+
+import { makeDefaultMfaState, MfaState } from 'teleport/lib/useMfa';
+import { SSOChallenge } from 'teleport/services/auth';
+
+import AuthnDialog from './AuthnDialog';
+
+const mockSsoChallenge: SSOChallenge = {
+ redirectUrl: 'url',
+ requestId: '123',
+ device: {
+ displayName: 'Okta',
+ connectorId: '123',
+ connectorType: 'saml',
+ },
+ channelId: '123',
+};
+
+function makeMockState(partial: Partial): MfaState {
+ const mfa = makeDefaultMfaState();
+ return {
+ ...mfa,
+ ...partial,
+ };
+}
+
+describe('AuthnDialog', () => {
+ const mockOnCancel = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders the dialog with basic content', () => {
+ const mfa = makeMockState({ ssoChallenge: mockSsoChallenge });
+ render();
+
+ expect(screen.getByText('Multi-factor authentication')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Re-enter your multi-factor authentication in the browser to continue.'
+ )
+ ).toBeInTheDocument();
+ expect(screen.getByText('Okta')).toBeInTheDocument();
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
+ });
+
+ test('displays error text when provided', () => {
+ const errorText = 'Authentication failed';
+ const mfa = makeMockState({ errorText });
+ render();
+
+ expect(screen.getByTestId('danger-alert')).toBeInTheDocument();
+ expect(screen.getByText(errorText)).toBeInTheDocument();
+ });
+
+ test('sso button renders with callback', async () => {
+ const mfa = makeMockState({
+ ssoChallenge: mockSsoChallenge,
+ onSsoAuthenticate: jest.fn(),
+ });
+ render();
+ const ssoButton = screen.getByText('Okta');
+ fireEvent.click(ssoButton);
+ expect(mfa.onSsoAuthenticate).toHaveBeenCalledTimes(1);
+ });
+
+ test('webauthn button renders with callback', async () => {
+ const mfa = makeMockState({
+ webauthnPublicKey: { challenge: new ArrayBuffer(0) },
+ onWebauthnAuthenticate: jest.fn(),
+ });
+ render();
+ const webauthn = screen.getByText('Passkey/Hardware Key');
+ fireEvent.click(webauthn);
+ expect(mfa.onWebauthnAuthenticate).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx
index 05685c0d6a3eb..805d4d0c79804 100644
--- a/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx
+++ b/web/packages/teleport/src/components/AuthnDialog/AuthnDialog.tsx
@@ -23,13 +23,16 @@ import Dialog, {
DialogContent,
} from 'design/Dialog';
import { Danger } from 'design/Alert';
-import { Text, ButtonPrimary, ButtonSecondary, Flex } from 'design';
+import { Text, ButtonSecondary, Flex, ButtonLink } from 'design';
+
+import { guessProviderType } from 'shared/components/ButtonSso';
+import { SSOIcon } from 'shared/components/ButtonSso/ButtonSso';
import { MfaState } from 'teleport/lib/useMfa';
export default function AuthnDialog({ mfa, onCancel }: Props) {
return (
-