diff --git a/web/packages/shared/services/consts.ts b/web/packages/shared/services/consts.ts
new file mode 100644
index 0000000000000..3a0c4dcd05ebe
--- /dev/null
+++ b/web/packages/shared/services/consts.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2022 Gravitational, Inc.
+ *
+ * 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.
+ */
+
+export const privateKeyEnablingPolicies = [
+ 'hardware_key',
+ 'hardware_key_touch',
+] as const;
diff --git a/web/packages/shared/services/index.ts b/web/packages/shared/services/index.ts
index c6063bd9efa40..61db3159abc73 100644
--- a/web/packages/shared/services/index.ts
+++ b/web/packages/shared/services/index.ts
@@ -13,4 +13,5 @@ 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.
*/
+export * from './consts';
export * from './types';
diff --git a/web/packages/shared/services/types.ts b/web/packages/shared/services/types.ts
index 16e10239d658a..9d608e3a9abb3 100644
--- a/web/packages/shared/services/types.ts
+++ b/web/packages/shared/services/types.ts
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { privateKeyEnablingPolicies } from './consts';
+
export type AuthProviderType = 'oidc' | 'saml' | 'github';
export type Auth2faType = 'otp' | 'off' | 'optional' | 'on' | 'webauthn';
@@ -42,3 +44,7 @@ export type AuthProvider = {
type: AuthProviderType;
url: string;
};
+
+export type PrivateKeyPolicy =
+ | 'none'
+ | typeof privateKeyEnablingPolicies[number];
diff --git a/web/packages/shared/utils/errorType.ts b/web/packages/shared/utils/errorType.ts
index 674fa746bacd1..79e7aab24bc74 100644
--- a/web/packages/shared/utils/errorType.ts
+++ b/web/packages/shared/utils/errorType.ts
@@ -14,6 +14,12 @@
* limitations under the License.
*/
+import { privateKeyEnablingPolicies } from 'shared/services';
+
+export function isPrivateKeyRequiredError(err: Error) {
+ return privateKeyEnablingPolicies.some(p => err.message.includes(p));
+}
+
// getErrMessage first checks if the error is of type Error
// before attempting to access the error message field.
// Used with try catch blocks, where the error caught
diff --git a/web/packages/teleport/src/Login/Login.test.tsx b/web/packages/teleport/src/Login/Login.test.tsx
index 7002400342996..61ee3d1d1d1d2 100644
--- a/web/packages/teleport/src/Login/Login.test.tsx
+++ b/web/packages/teleport/src/Login/Login.test.tsx
@@ -16,6 +16,7 @@
import React from 'react';
import { render, fireEvent, screen, waitFor } from 'design/utils/testing';
+import { privateKeyEnablingPolicies } from 'shared/services/consts';
import auth from 'teleport/services/auth/auth';
import history from 'teleport/services/history';
@@ -78,6 +79,41 @@ test('login with SSO', () => {
);
});
+test('login with private key policy enabled through cluster wide', () => {
+ jest
+ .spyOn(cfg, 'getPrivateKeyPolicy')
+ .mockImplementation(() => 'hardware_key');
+
+ render();
+
+ expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument();
+ expect(screen.getByText(/login disabled/i)).toBeInTheDocument();
+});
+
+test('login with private key policy enabled through role setting', async () => {
+ // Just needs any of these enabling keywords in error message
+ jest
+ .spyOn(auth, 'login')
+ .mockRejectedValue(new Error(privateKeyEnablingPolicies[0]));
+
+ render();
+
+ // Fill form.
+ const username = screen.getByPlaceholderText(/username/i);
+ const password = screen.getByPlaceholderText(/password/i);
+ fireEvent.change(username, { target: { value: 'username' } });
+ fireEvent.change(password, { target: { value: '123' } });
+
+ // Test logging in with private key error return renders private policy error.
+ fireEvent.click(screen.getByText('Sign In'));
+ await waitFor(() => {
+ expect(auth.login).toHaveBeenCalledWith('username', '123', '');
+ });
+
+ expect(screen.queryByPlaceholderText(/username/i)).not.toBeInTheDocument();
+ expect(screen.getByText(/login disabled/i)).toBeInTheDocument();
+});
+
describe('test MOTD', () => {
test('show motd only if motd is set', async () => {
// default login form
diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts
index f99d9a2b2dd89..8cc60b3a751bd 100644
--- a/web/packages/teleport/src/Login/useLogin.ts
+++ b/web/packages/teleport/src/Login/useLogin.ts
@@ -17,6 +17,7 @@
import { useState } from 'react';
import { useAttempt } from 'shared/hooks';
import { AuthProvider } from 'shared/services';
+import { isPrivateKeyRequiredError } from 'shared/utils/errorType';
import history from 'teleport/services/history';
import cfg from 'teleport/config';
@@ -24,6 +25,15 @@ import auth, { UserCredentials } from 'teleport/services/auth';
export default function useLogin() {
const [attempt, attemptActions] = useAttempt({ isProcessing: false });
+ // privateKeyPolicyEnabled can be enabled through cluster wide config,
+ // or through a role setting.
+ // Cluster wide config takes precedence and the user will not
+ // see a login form which prevents login attempts.
+ // Role setting requires the user to try a successful
+ // attempt at logging in to determine if private key policy was enabled.
+ const [privateKeyPolicyEnabled, setPrivateKeyPolicyEnabled] = useState(
+ cfg.getPrivateKeyPolicy() != 'none'
+ );
const authProviders = cfg.getAuthProviders();
const auth2faType = cfg.getAuth2faType();
@@ -48,6 +58,10 @@ export default function useLogin() {
.login(email, password, token)
.then(onSuccess)
.catch(err => {
+ if (isPrivateKeyRequiredError(err)) {
+ setPrivateKeyPolicyEnabled(true);
+ return;
+ }
attemptActions.error(err);
});
}
@@ -58,6 +72,10 @@ export default function useLogin() {
.loginWithWebauthn(creds)
.then(onSuccess)
.catch(err => {
+ if (isPrivateKeyRequiredError(err)) {
+ setPrivateKeyPolicyEnabled(true);
+ return;
+ }
attemptActions.error(err);
});
}
@@ -81,7 +99,7 @@ export default function useLogin() {
clearAttempt: attemptActions.clear,
isPasswordlessEnabled: cfg.isPasswordlessEnabled(),
primaryAuthType: cfg.getPrimaryAuthType(),
- privateKeyPolicyEnabled: false,
+ privateKeyPolicyEnabled,
motd,
showMotd,
acknowledgeMotd,
diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx
index 5fa612fa07c19..d2f2d7ec6267f 100644
--- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx
+++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.test.tsx
@@ -57,6 +57,14 @@ test('story.SuccessReset', () => {
const { container } = render();
expect(container.firstChild).toMatchSnapshot();
});
+test('story.SuccessAndPrivateKeyEnabledRegister', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+});
+test('story.SuccessAndPrivateKeyEnabledReset', () => {
+ const { container } = render();
+ expect(container.firstChild).toMatchSnapshot();
+});
test('story.SuccessRegisterDashboard', () => {
const { container } = render();
expect(container.firstChild).toMatchSnapshot();
diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx
index 32220615b66c0..48138e7347ff5 100644
--- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx
+++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.story.tsx
@@ -137,6 +137,19 @@ export const SuccessReset = () =>
resetMode: true,
});
+export const SuccessAndPrivateKeyEnabledRegister = () =>
+ renderNewCredentials({
+ success: true,
+ privateKeyPolicyEnabled: true,
+ });
+
+export const SuccessAndPrivateKeyEnabledReset = () =>
+ renderNewCredentials({
+ success: true,
+ resetMode: true,
+ privateKeyPolicyEnabled: true,
+ });
+
export const SuccessRegisterDashboard = () =>
renderNewCredentials({
success: true,
diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx
index d425739e3348e..e788f685c7524 100644
--- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx
+++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.test.tsx
@@ -28,6 +28,7 @@ import { makeTestUserContext } from 'teleport/User/testHelpers/makeTestUserConte
const attempt: Attempt = { status: '' };
const failedAttempt: Attempt = { status: 'failed' };
const processingAttempt: Attempt = { status: 'processing' };
+const successAttempt: Attempt = { status: 'success', statusText: 'hey' };
const resetToken: ResetToken = {
tokenId: 'tokenId',
@@ -79,9 +80,32 @@ test.each(nullCases)('renders $attempt as null', testCase => {
expect(container).toBeEmptyDOMElement();
});
+test('renders Reset Complete for success and private key policy enabled during reset', () => {
+ const props = makeProps();
+ props.fetchAttempt = successAttempt;
+ props.success = true;
+ props.privateKeyPolicyEnabled = true;
+ props.resetMode = true;
+ render();
+
+ expect(screen.getByText(/Reset Complete/i)).toBeInTheDocument();
+});
+
+test('renders Registration Complete for success and private key policy enabled during registration', () => {
+ const props = makeProps();
+ props.fetchAttempt = { status: 'success' };
+ props.success = true;
+ props.privateKeyPolicyEnabled = true;
+ props.resetMode = false;
+ render();
+
+ expect(screen.getByText(/Registration Complete/i)).toBeInTheDocument();
+});
+
test('renders Register Success on success', () => {
const props = makeProps();
props.fetchAttempt = { status: 'success' };
+ props.privateKeyPolicyEnabled = false;
props.recoveryCodes = undefined;
props.success = true;
render();
diff --git a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx
index 88c864b8dbb64..49a6f7ec6d958 100644
--- a/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx
+++ b/web/packages/teleport/src/Welcome/NewCredentials/NewCredentials.tsx
@@ -22,6 +22,7 @@ import { OnboardCard } from 'design/Onboard/OnboardCard';
import { Box } from 'design';
import RecoveryCodes from 'teleport/components/RecoveryCodes';
+import { PrivateKeyLoginDisabledCard } from 'teleport/components/PrivateKeyPolicy';
import cfg from 'teleport/config';
import { loginFlows } from 'teleport/Welcome/NewCredentials/constants';
@@ -60,6 +61,7 @@ export function NewCredentials(props: NewCredentialsProps) {
primaryAuthType,
success,
finishedRegister,
+ privateKeyPolicyEnabled,
isDashboard,
displayOnboardingQuestionnaire = false,
setDisplayOnboardingQuestionnaire = false,
@@ -84,6 +86,14 @@ export function NewCredentials(props: NewCredentialsProps) {
return null;
}
+ if (success && privateKeyPolicyEnabled) {
+ return (
+
+ );
+ }
+
if (
success &&
!resetMode &&
diff --git a/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap b/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap
index 30eb388b0aad8..ebaca56c218c1 100644
--- a/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap
+++ b/web/packages/teleport/src/Welcome/NewCredentials/__snapshots__/NewCredentials.story.test.tsx.snap
@@ -3110,6 +3110,598 @@ exports[`story.PrimaryPasswordlessError 1`] = `
`;
+exports[`story.SuccessAndPrivateKeyEnabledRegister 1`] = `
+.c6 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px;
+ box-sizing: border-box;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.24);
+ margin: 0 0 24px 0;
+ min-height: 40px;
+ padding: 8px 16px;
+ overflow: auto;
+ word-break: break-word;
+ line-height: 1.5;
+ margin-top: 32px;
+ margin-bottom: 32px;
+ background: #FF6257;
+ color: #000000;
+}
+
+.c6 a {
+ color: #FFFFFF;
+}
+
+.c5 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 400;
+ font-size: 18px;
+ line-height: 32px;
+ margin: 0px;
+ padding-top: 24px;
+ color: #FFFFFF;
+ text-align: center;
+}
+
+.c7 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 300;
+ font-size: 12px;
+ line-height: 24px;
+ margin: 0px;
+ margin-bottom: 8px;
+}
+
+.c11 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0px;
+}
+
+.c8 {
+ color: #009EFF;
+ font-weight: normal;
+ background: none;
+ text-decoration: underline;
+ text-transform: none;
+ color: #FFFFFF;
+}
+
+.c12 {
+ color: #009EFF;
+ font-weight: normal;
+ background: none;
+ text-decoration: underline;
+ text-transform: none;
+}
+
+.c1 {
+ box-sizing: border-box;
+ height: 100%;
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+}
+
+.c2 {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+}
+
+.c10 {
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ gap: 50px;
+}
+
+.c9 {
+ padding-bottom: 24px;
+ width: 100%;
+ color: white;
+}
+
+.c13 {
+ color: white;
+ text-decoration: none;
+}
+
+.c13:hover,
+.c13:active,
+.c13:focus {
+ color: rgba(255,255,255,0.54);
+}
+
+.c0 {
+ position: absolute;
+ width: 100vw;
+ height: 100vh;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ z-index: -2;
+ background: url('file_stub');
+ -webkit-background-size: cover;
+ -moz-background-size: cover;
+ -o-background-size: cover;
+ background-size: cover;
+}
+
+.c0::after {
+ content: '';
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ position: absolute;
+ z-index: -1;
+ background-color: black;
+ opacity: 0.25;
+ backdrop-filter: blur(17.5px);
+}
+
+.c3 {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 24px 0;
+}
+
+.c4 {
+ box-sizing: border-box;
+ box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px rgba(0,0,0,0.14),0px 1px 18px rgba(0,0,0,0.12);
+ border-radius: 8px;
+ background-color: #222C59;
+ width: 600px;
+ padding: 24px;
+ text-align: left;
+ margin: 16px auto 16px auto;
+ overflow-y: auto;
+}
+
+@media screen and (max-width:800px) {
+ .c10 {
+ flex-direction: column-reverse;
+ text-align: center;
+ gap: 10px;
+ }
+}
+
+@media screen and (max-width:800px) {
+ .c4 {
+ width: auto;
+ margin: 20px;
+ }
+}
+
+@media screen and (max-height:760px) {
+ .c4 {
+ height: calc(100vh - 250px);
+ }
+}
+
+
+
+
+
+
+
+
+
+ Registration Complete
+
+
+ Web UI Login Disabled
+
+
+ This Teleport Cluster requires that user
+
+
+ private keys
+
+
+ be stored on hardware authentication devices. Since these keys are not accessible by web browsers, Web UI login has been disabled. Please use
+
+
+ Teleport Connect
+
+
+ or
+
+
+ tsh
+
+
+ to log in.
+
+ This Teleport Cluster requires that user
+
+
+ private keys
+
+
+ be stored on hardware authentication devices. Since these keys are not accessible by web browsers, Web UI login has been disabled. Please use
+
+
+ Teleport Connect
+
+
+ or
+
+
+ tsh
+
+
+ to log in.
+
+
+
+
+
+
+`;
+
exports[`story.SuccessRegister 1`] = `
.c9 {
line-height: 1.5;
diff --git a/web/packages/teleport/src/Welcome/useToken.ts b/web/packages/teleport/src/Welcome/useToken.ts
index de7d970ace264..7bf579969bafe 100644
--- a/web/packages/teleport/src/Welcome/useToken.ts
+++ b/web/packages/teleport/src/Welcome/useToken.ts
@@ -32,6 +32,7 @@ export default function useToken(tokenId: string): UseTokenState {
const [resetToken, setResetToken] = useState();
const [recoveryCodes, setRecoveryCodes] = useState();
const [success, setSuccess] = useState(false); // TODO rename
+ const [privateKeyPolicyEnabled, setPrivateKeyPolicyEnabled] = useState(false);
const fetchAttempt = useAttempt('');
const submitAttempt = useAttempt('');
@@ -46,6 +47,9 @@ export default function useToken(tokenId: string): UseTokenState {
}, []);
function handleResponse(res: ChangedUserAuthn) {
+ if (res.privateKeyPolicyEnabled) {
+ setPrivateKeyPolicyEnabled(true);
+ }
if (res.recovery.createdDate) {
setRecoveryCodes(res.recovery);
} else {
@@ -105,6 +109,6 @@ export default function useToken(tokenId: string): UseTokenState {
redirect,
success,
finishedRegister,
- privateKeyPolicyEnabled: false,
+ privateKeyPolicyEnabled,
};
}
diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx
index 24d3c0ea59449..4a97005cfdb89 100644
--- a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx
+++ b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx
@@ -120,6 +120,10 @@ export const LocalWithSsoAndPwdless = () => {
);
};
+export const PrivateKeyPolicyEnabled = () => (
+
+);
+
export const LocalDisabledWithSso = () => {
const ssoProvider = [
{ name: 'github', type: 'oidc', url: '' } as const,
diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx
index d66e248187b7c..0639d675690ef 100644
--- a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx
+++ b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx
@@ -44,6 +44,7 @@ import {
import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions';
import { StepSlider, StepComponentProps } from 'design/StepSlider';
+import { PrivateKeyLoginDisabledCard } from 'teleport/components/PrivateKeyPolicy';
import { UserCredentials } from 'teleport/services/auth';
import SSOButtonList from './SsoButtons';
@@ -54,7 +55,18 @@ export default function LoginForm(props: Props) {
attempt,
isLocalAuthEnabled = true,
authProviders = [],
+ privateKeyPolicyEnabled,
+ isRecoveryEnabled,
+ onRecover,
} = props;
+ if (privateKeyPolicyEnabled) {
+ return (
+
+ );
+ }
const ssoEnabled = authProviders?.length > 0;
@@ -533,7 +545,7 @@ export type Props = {
title?: string;
isLocalAuthEnabled?: boolean;
isPasswordlessEnabled: boolean;
- privateKeyPolicyEnabled: boolean;
+ privateKeyPolicyEnabled?: boolean;
authProviders?: AuthProvider[];
auth2faType?: Auth2faType;
primaryAuthType: PrimaryAuthType;
diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx
new file mode 100644
index 0000000000000..55131f2535f8e
--- /dev/null
+++ b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.story.tsx
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2022 Gravitational, Inc.
+ *
+ * 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 React from 'react';
+
+import {
+ PrivateKeyLoginDisabledCard,
+ PrivateKeyAccessRequestDialogue,
+} from './PrivateKeyPolicy';
+
+export default {
+ title: 'Teleport/PrivateKeyPolicy',
+};
+
+export const CardDefault = () => (
+
+);
+
+export const CardCloud = () => (
+ null}
+ />
+);
+
+export const DialogueWithLocalAuth = () => (
+ null} {...tshLoginProps} />
+);
+
+export const DialogueWithSso = () => (
+ null}
+ {...tshLoginProps}
+ btnText="custom btn text"
+ authType="sso"
+ />
+);
+
+const tshLoginProps = {
+ username: 'llama',
+ authType: 'local' as any,
+ clusterId: 'cluster-id-1234',
+ accessRequestId: 'request-id-1234',
+};
diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx
new file mode 100644
index 0000000000000..d88774c9a8690
--- /dev/null
+++ b/web/packages/teleport/src/components/PrivateKeyPolicy/PrivateKeyPolicy.tsx
@@ -0,0 +1,150 @@
+/**
+ * Copyright 2022 Gravitational, Inc.
+ *
+ * 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 React from 'react';
+import { Text, Link, ButtonText, ButtonSecondary, Box } from 'design';
+import Dialog, {
+ DialogHeader,
+ DialogTitle,
+ DialogContent,
+ DialogFooter,
+} from 'design/Dialog';
+import { Danger } from 'design/Alert';
+
+import { OnboardCard } from 'design/Onboard/OnboardCard';
+
+import { generateTshLoginCommand } from 'teleport/lib/util';
+
+import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
+
+import type { TshLoginCommand } from 'teleport/lib/util';
+
+const LINK_HARDWARE_KEY_SUPPORT =
+ 'https://goteleport.com/docs/access-controls/guides/hardware-key-support/';
+
+const LINK_TSH =
+ 'https://goteleport.com/docs/connect-your-client/tsh/#installing-tsh';
+
+const LINK_CONNECT =
+ 'https://goteleport.com/docs/connect-your-client/teleport-connect/';
+
+export const PrivateKeyLoginDisabledCard = ({
+ title,
+ onRecover,
+}: {
+ title: string;
+ // onRecover only applies to Teleport Cloud,
+ // and is called upon when user needs to recover
+ // lost password or two-factor device.
+ onRecover?: (isRecoverPassword: boolean) => void;
+}) => (
+
+
+ {title}
+
+ Web UI Login Disabled
+
+ This Teleport Cluster requires that user{' '}
+
+ private keys
+ {' '}
+ be stored on hardware authentication devices. Since these keys are not
+ accessible by web browsers, Web UI login has been disabled. Please use{' '}
+
+ Teleport Connect
+ {' '}
+ or{' '}
+
+ tsh
+ {' '}
+ to log in.
+
+ {onRecover && (
+
+ onRecover(true)}
+ style={{ padding: '0px', minHeight: 0 }}
+ mr={2}
+ >
+ Forgot Password?
+
+ or{' '}
+
+ Lost Two-Factor Device?
+
+
+ )}
+
+);
+
+export type PrivateKeyAccessRequest = TshLoginCommand & {
+ accessRequestId: string;
+};
+
+export function PrivateKeyAccessRequestDialogue({
+ onClose,
+ btnText,
+ ...tshProps
+}: PrivateKeyAccessRequest & {
+ btnText?: string;
+ onClose(): void;
+}) {
+ return (
+
+ );
+}
diff --git a/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts b/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts
new file mode 100644
index 0000000000000..b0c63e65827d5
--- /dev/null
+++ b/web/packages/teleport/src/components/PrivateKeyPolicy/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2022 Gravitational, Inc.
+ *
+ * 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.
+ */
+
+export {
+ PrivateKeyLoginDisabledCard,
+ PrivateKeyAccessRequestDialogue,
+} from './PrivateKeyPolicy';
+
+export type { PrivateKeyAccessRequest } from './PrivateKeyPolicy';
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index e3201f230fb41..b4623812a8ffa 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -25,6 +25,7 @@ import type {
AuthType,
PreferredMfaType,
PrimaryAuthType,
+ PrivateKeyPolicy,
} from 'shared/services';
import type { SortType } from 'teleport/services/agents';
@@ -66,6 +67,7 @@ const cfg = {
second_factor: 'off' as Auth2faType,
authType: 'local' as AuthType,
preferredLocalMfa: '' as PreferredMfaType,
+ privateKeyPolicy: 'none' as PrivateKeyPolicy,
// motd is message of the day, displayed to users before login.
motd: '',
},
@@ -318,6 +320,10 @@ const cfg = {
return cfg.auth.localAuthEnabled;
},
+ getPrivateKeyPolicy() {
+ return cfg.auth.privateKeyPolicy;
+ },
+
isPasswordlessEnabled() {
return cfg.auth.allowPasswordless;
},
diff --git a/web/packages/teleport/src/services/auth/make.test.ts b/web/packages/teleport/src/services/auth/make.test.ts
index 811d7de739333..76169bac61b14 100644
--- a/web/packages/teleport/src/services/auth/make.test.ts
+++ b/web/packages/teleport/src/services/auth/make.test.ts
@@ -19,6 +19,7 @@ import { makeChangedUserAuthn } from './make';
test('makeChangedUserAuthn with null', async () => {
expect(makeChangedUserAuthn(null)).toStrictEqual({
recovery: { codes: [], createdDate: null },
+ privateKeyPolicyEnabled: false,
});
});
@@ -30,11 +31,13 @@ test('makeChangedUserAuthn with recovery codes', async () => {
codes: ['llama', 'alpca'],
created: date,
},
+ privateKeyPolicyEnabled: true,
})
).toStrictEqual({
recovery: {
codes: ['llama', 'alpca'],
createdDate: new Date('2022-10-25T00:30:18.162Z'),
},
+ privateKeyPolicyEnabled: true,
});
});
diff --git a/web/packages/teleport/src/services/auth/make.ts b/web/packages/teleport/src/services/auth/make.ts
index 91626a0682f68..020fee64e294c 100644
--- a/web/packages/teleport/src/services/auth/make.ts
+++ b/web/packages/teleport/src/services/auth/make.ts
@@ -24,6 +24,7 @@ export function makeChangedUserAuthn(json: any): ChangedUserAuthn {
return {
recovery: makeRecoveryCodes(json.recovery),
+ privateKeyPolicyEnabled: !!json.privateKeyPolicyEnabled,
};
}
diff --git a/web/packages/teleport/src/services/auth/types.ts b/web/packages/teleport/src/services/auth/types.ts
index 679966848ca5d..0d246f76fcaa2 100644
--- a/web/packages/teleport/src/services/auth/types.ts
+++ b/web/packages/teleport/src/services/auth/types.ts
@@ -43,6 +43,7 @@ export type RecoveryCodes = {
export type ChangedUserAuthn = {
recovery: RecoveryCodes;
+ privateKeyPolicyEnabled?: boolean;
};
export type NewCredentialRequest = {