Merge orgin/master into feat-settings-ui
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>feat-otp-verification
commit
6c89ee1f9c
|
@ -70,7 +70,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "b-client",
|
ID: "b-client",
|
||||||
Description: "Normal DisplayName",
|
Description: "Normal Description",
|
||||||
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
Secret: MustDecodeSecret("$plaintext$b-client-secret"),
|
||||||
Policy: twofactor,
|
Policy: twofactor,
|
||||||
RedirectURIs: []string{
|
RedirectURIs: []string{
|
||||||
|
|
|
@ -3,50 +3,50 @@
|
||||||
"Add": "Add",
|
"Add": "Add",
|
||||||
"Add Credential": "Add Credential",
|
"Add Credential": "Add Credential",
|
||||||
"Added": "Added {{when, datetime}}",
|
"Added": "Added {{when, datetime}}",
|
||||||
"Are you sure you want to remove the Webauthn credential from from your account": "Are you sure you want to remove the Webauthn credential {{description}} from your account?",
|
"Are you sure you want to remove the WebAuthn credential from from your account": "Are you sure you want to remove the WebAuthn credential {{description}} from your account?",
|
||||||
"Attestation Type": "Attestation Type",
|
"Attestation Type": "Attestation Type",
|
||||||
"Authenticator GUID": "Authenticator GUID",
|
"Authenticator GUID": "Authenticator GUID",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Click to add a Webauthn credential to your account": "Click to add a Webauthn credential to your account",
|
"Click to add a WebAuthn credential to your account": "Click to add a WebAuthn credential to your account",
|
||||||
"Click to copy the": "Click to copy the",
|
"Click to copy the": "Click to copy the",
|
||||||
"Clone Warning": "Clone Warning",
|
"Clone Warning": "Clone Warning",
|
||||||
"Created": "Created",
|
"Created": "Created",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Display extended information for this Webauthn credential": "Display extended information for this Webauthn credential",
|
"Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Edit information for this Webauthn credential": "Edit information for this Webauthn credential",
|
"Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential",
|
||||||
"Edit Webauthn Credential": "Edit Webauthn Credential",
|
"Edit WebAuthn Credential": "Edit WebAuthn Credential",
|
||||||
"Enabled": "Enabled",
|
"Enabled": "Enabled",
|
||||||
"Enter a new name for this Webauthn credential": "Enter a new name for this Webauthn credential:",
|
"Enter a new name for this WebAuthn credential": "Enter a new name for this WebAuthn credential:",
|
||||||
"Enter a description for this credential": "Enter a description for this credential",
|
"Enter a description for this credential": "Enter a description for this credential",
|
||||||
"Extended Webauthn credential information for security key": "Extended Webauthn credential information for security key {{description}}",
|
"Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
|
||||||
"Identifier": "Identifier",
|
"Identifier": "Identifier",
|
||||||
"Last Used": "Last Used {{when, datetime}}",
|
"Last Used": "Last Used {{when, datetime}}",
|
||||||
"Manage your security keys": "Manage your security keys",
|
"Manage your security keys": "Manage your security keys",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"No Registered Webauthn Credentials": "No Registered Webauthn Credentials",
|
"No Registered WebAuthn Credentials": "No Registered WebAuthn Credentials",
|
||||||
"Overview": "Overview",
|
"Overview": "Overview",
|
||||||
"Provide the details for the new security key": "Provide the details for the new security key",
|
"Provide the details for the new security key": "Provide the details for the new security key",
|
||||||
"Register Webauthn Credential": "Register Webauthn Credential",
|
"Register WebAuthn Credential": "Register WebAuthn Credential",
|
||||||
"Relying Party ID": "Relying Party ID",
|
"Relying Party ID": "Relying Party ID",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"Remove this Webauthn credential": "Remove this Webauthn credential",
|
"Remove this WebAuthn credential": "Remove this WebAuthn credential",
|
||||||
"Remove Webauthn Credential": "Remove Webauthn Credential",
|
"Remove WebAuthn Credential": "Remove WebAuthn Credential",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Successfully deleted the Webauthn credential": "Successfully deleted the Webauthn credential",
|
"Successfully deleted the WebAuthn credential": "Successfully deleted the WebAuthn credential",
|
||||||
"Successfully updated the Webauthn credential": "Successfully updated the Webauthn credential",
|
"Successfully updated the WebAuthn credential": "Successfully updated the WebAuthn credential",
|
||||||
"There was a problem deleting the Webauthn credential": "There was a problem deleting the Webauthn credential",
|
"There was a problem deleting the WebAuthn credential": "There was a problem deleting the WebAuthn credential",
|
||||||
"There was a problem updating the Webauthn credential": "There was a problem updating the Webauthn credential",
|
"There was a problem updating the WebAuthn credential": "There was a problem updating the WebAuthn credential",
|
||||||
"Transports": "Transports",
|
"Transports": "Transports",
|
||||||
"Two-Factor Authentication": "Two-Factor Authentication",
|
"Two-Factor Authentication": "Two-Factor Authentication",
|
||||||
"Usage Count": "Usage Count",
|
"Usage Count": "Usage Count",
|
||||||
"Webauthn Credential Details": "Webauthn Credential Details",
|
"WebAuthn Credential Details": "WebAuthn Credential Details",
|
||||||
"Webauthn Credentials": "Webauthn Credentials",
|
"WebAuthn Credentials": "WebAuthn Credentials",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"You must have a higher authentication level to delete Webauthn credentials": "You must have a higher authentication level to delete Webauthn credentials",
|
"You must have a higher authentication level to delete WebAuthn credentials": "You must have a higher authentication level to delete WebAuthn credentials",
|
||||||
"You must be elevated to delete Webauthn credentials": "You must be elevated to delete Webauthn credentials",
|
"You must be elevated to delete WebAuthn credentials": "You must be elevated to delete WebAuthn credentials",
|
||||||
"You must have a higher authentication level to update Webauthn credentials": "You must have a higher authentication level to update Webauthn credentials",
|
"You must have a higher authentication level to update WebAuthn credentials": "You must have a higher authentication level to update WebAuthn credentials",
|
||||||
"You must be elevated to update Webauthn credentials": "You must be elevated to update Webauthn credentials"
|
"You must be elevated to update WebAuthn credentials": "You must be elevated to update WebAuthn credentials"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnRegisterIcon(props: Props) {
|
export default function WebAuthnRegisterIcon(props: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [timerPercent, triggerTimer] = useTimer(props.timeout);
|
const [timerPercent, triggerTimer] = useTimer(props.timeout);
|
||||||
|
|
|
@ -7,15 +7,15 @@ import FailureIcon from "@components/FailureIcon";
|
||||||
import FingerTouchIcon from "@components/FingerTouchIcon";
|
import FingerTouchIcon from "@components/FingerTouchIcon";
|
||||||
import LinearProgressBar from "@components/LinearProgressBar";
|
import LinearProgressBar from "@components/LinearProgressBar";
|
||||||
import { useTimer } from "@hooks/Timer";
|
import { useTimer } from "@hooks/Timer";
|
||||||
import { WebauthnTouchState } from "@models/Webauthn";
|
import { WebAuthnTouchState } from "@models/WebAuthn";
|
||||||
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
|
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRetryClick: () => void;
|
onRetryClick: () => void;
|
||||||
webauthnTouchState: WebauthnTouchState;
|
webauthnTouchState: WebAuthnTouchState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnTryIcon(props: Props) {
|
export default function WebAuthnTryIcon(props: Props) {
|
||||||
const touchTimeout = 30;
|
const touchTimeout = 30;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
|
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
|
||||||
|
@ -42,7 +42,7 @@ export default function WebauthnTryIcon(props: Props) {
|
||||||
const touch = (
|
const touch = (
|
||||||
<IconWithContext
|
<IconWithContext
|
||||||
icon={<FingerTouchIcon size={64} animated strong />}
|
icon={<FingerTouchIcon size={64} animated strong />}
|
||||||
className={props.webauthnTouchState === WebauthnTouchState.WaitTouch ? undefined : "hidden"}
|
className={props.webauthnTouchState === WebAuthnTouchState.WaitTouch ? undefined : "hidden"}
|
||||||
>
|
>
|
||||||
<LinearProgressBar value={timerPercent} className={styles.progressBar} height={theme.spacing(2)} />
|
<LinearProgressBar value={timerPercent} className={styles.progressBar} height={theme.spacing(2)} />
|
||||||
</IconWithContext>
|
</IconWithContext>
|
||||||
|
@ -51,7 +51,7 @@ export default function WebauthnTryIcon(props: Props) {
|
||||||
const failure = (
|
const failure = (
|
||||||
<IconWithContext
|
<IconWithContext
|
||||||
icon={<FailureIcon />}
|
icon={<FailureIcon />}
|
||||||
className={props.webauthnTouchState === WebauthnTouchState.Failure ? undefined : "hidden"}
|
className={props.webauthnTouchState === WebAuthnTouchState.Failure ? undefined : "hidden"}
|
||||||
>
|
>
|
||||||
<Button color="secondary" onClick={handleRetryClick}>
|
<Button color="secondary" onClick={handleRetryClick}>
|
||||||
Retry
|
Retry
|
|
@ -3,7 +3,7 @@ export const AuthenticatedRoute: string = "/authenticated";
|
||||||
export const ConsentRoute: string = "/consent";
|
export const ConsentRoute: string = "/consent";
|
||||||
|
|
||||||
export const SecondFactorRoute: string = "/2fa";
|
export const SecondFactorRoute: string = "/2fa";
|
||||||
export const SecondFactorWebauthnSubRoute: string = "/webauthn";
|
export const SecondFactorWebAuthnSubRoute: string = "/webauthn";
|
||||||
export const SecondFactorTOTPSubRoute: string = "/one-time-password";
|
export const SecondFactorTOTPSubRoute: string = "/one-time-password";
|
||||||
export const SecondFactorPushSubRoute: string = "/push-notification";
|
export const SecondFactorPushSubRoute: string = "/push-notification";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export enum SecondFactorMethod {
|
export enum SecondFactorMethod {
|
||||||
TOTP = 1,
|
TOTP = 1,
|
||||||
Webauthn,
|
WebAuthn,
|
||||||
MobilePush,
|
MobilePush,
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export enum AttestationResult {
|
||||||
FailureSyntax,
|
FailureSyntax,
|
||||||
FailureSupport,
|
FailureSupport,
|
||||||
FailureUnknown,
|
FailureUnknown,
|
||||||
FailureWebauthnNotSupported,
|
FailureWebAuthnNotSupported,
|
||||||
FailureToken,
|
FailureToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export enum AssertionResult {
|
||||||
FailureSyntax,
|
FailureSyntax,
|
||||||
FailureUnknown,
|
FailureUnknown,
|
||||||
FailureUnknownSecurity,
|
FailureUnknownSecurity,
|
||||||
FailureWebauthnNotSupported,
|
FailureWebAuthnNotSupported,
|
||||||
FailureChallenge,
|
FailureChallenge,
|
||||||
FailureUnrecognized,
|
FailureUnrecognized,
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export function AssertionResultFailureString(result: AssertionResult) {
|
||||||
return "The server responded with an invalid Facet ID for the URL.";
|
return "The server responded with an invalid Facet ID for the URL.";
|
||||||
case AssertionResult.FailureSyntax:
|
case AssertionResult.FailureSyntax:
|
||||||
return "The assertion challenge was rejected as malformed or incompatible by your browser.";
|
return "The assertion challenge was rejected as malformed or incompatible by your browser.";
|
||||||
case AssertionResult.FailureWebauthnNotSupported:
|
case AssertionResult.FailureWebAuthnNotSupported:
|
||||||
return "Your browser does not support the WebAuthN protocol.";
|
return "Your browser does not support the WebAuthN protocol.";
|
||||||
case AssertionResult.FailureUnrecognized:
|
case AssertionResult.FailureUnrecognized:
|
||||||
return "This device is not registered.";
|
return "This device is not registered.";
|
||||||
|
@ -85,7 +85,7 @@ export function AttestationResultFailureString(result: AttestationResult) {
|
||||||
return "Your browser does not appear to support the configuration.";
|
return "Your browser does not appear to support the configuration.";
|
||||||
case AttestationResult.FailureSyntax:
|
case AttestationResult.FailureSyntax:
|
||||||
return "The attestation challenge was rejected as malformed or incompatible by your browser.";
|
return "The attestation challenge was rejected as malformed or incompatible by your browser.";
|
||||||
case AttestationResult.FailureWebauthnNotSupported:
|
case AttestationResult.FailureWebAuthnNotSupported:
|
||||||
return "Your browser does not support the WebAuthN protocol.";
|
return "Your browser does not support the WebAuthN protocol.";
|
||||||
case AttestationResult.FailureUserConsent:
|
case AttestationResult.FailureUserConsent:
|
||||||
return "You cancelled the attestation request.";
|
return "You cancelled the attestation request.";
|
||||||
|
@ -105,7 +105,7 @@ export interface AuthenticationResult {
|
||||||
result: AssertionResult;
|
result: AssertionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebauthnDevice {
|
export interface WebAuthnDevice {
|
||||||
id: string;
|
id: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
|
@ -140,7 +140,7 @@ export function toTransportName(transport: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WebauthnTouchState {
|
export enum WebAuthnTouchState {
|
||||||
WaitTouch = 1,
|
WaitTouch = 1,
|
||||||
InProgress = 2,
|
InProgress = 2,
|
||||||
Failure = 3,
|
Failure = 3,
|
|
@ -22,7 +22,7 @@ export function toEnum(method: Method2FA): SecondFactorMethod {
|
||||||
case "totp":
|
case "totp":
|
||||||
return SecondFactorMethod.TOTP;
|
return SecondFactorMethod.TOTP;
|
||||||
case "webauthn":
|
case "webauthn":
|
||||||
return SecondFactorMethod.Webauthn;
|
return SecondFactorMethod.WebAuthn;
|
||||||
case "mobile_push":
|
case "mobile_push":
|
||||||
return SecondFactorMethod.MobilePush;
|
return SecondFactorMethod.MobilePush;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export function toString(method: SecondFactorMethod): Method2FA {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case SecondFactorMethod.TOTP:
|
case SecondFactorMethod.TOTP:
|
||||||
return "totp";
|
return "totp";
|
||||||
case SecondFactorMethod.Webauthn:
|
case SecondFactorMethod.WebAuthn:
|
||||||
return "webauthn";
|
return "webauthn";
|
||||||
case SecondFactorMethod.MobilePush:
|
case SecondFactorMethod.MobilePush:
|
||||||
return "mobile_push";
|
return "mobile_push";
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||||
|
import { WebAuthnDevicesPath } from "@services/Api";
|
||||||
|
import { GetWithOptionalData } from "@services/Client";
|
||||||
|
|
||||||
|
// getWebAuthnDevices returns the list of webauthn devices for the authenticated user.
|
||||||
|
export async function getWebAuthnDevices(): Promise<WebAuthnDevice[] | null> {
|
||||||
|
return GetWithOptionalData<WebAuthnDevice[] | null>(WebAuthnDevicesPath);
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
|
||||||
import { WebAuthnDevicesPath } from "@services/Api";
|
|
||||||
import { GetWithOptionalData } from "@services/Client";
|
|
||||||
|
|
||||||
// getWebauthnDevices returns the list of webauthn devices for the authenticated user.
|
|
||||||
export async function getWebauthnDevices(): Promise<WebauthnDevice[] | null> {
|
|
||||||
return GetWithOptionalData<WebauthnDevice[] | null>(WebAuthnDevicesPath);
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
PublicKeyCredentialCreationOptionsStatus,
|
PublicKeyCredentialCreationOptionsStatus,
|
||||||
PublicKeyCredentialRequestOptionsStatus,
|
PublicKeyCredentialRequestOptionsStatus,
|
||||||
RegistrationResult,
|
RegistrationResult,
|
||||||
} from "@models/Webauthn";
|
} from "@models/WebAuthn";
|
||||||
import {
|
import {
|
||||||
AuthenticationOKResponse,
|
AuthenticationOKResponse,
|
||||||
OptionalDataServiceResponse,
|
OptionalDataServiceResponse,
|
||||||
|
@ -28,7 +28,7 @@ import {
|
||||||
} from "@services/Api";
|
} from "@services/Api";
|
||||||
import { SignInResponse } from "@services/SignIn";
|
import { SignInResponse } from "@services/SignIn";
|
||||||
|
|
||||||
export function isWebauthnSecure(): boolean {
|
export function isWebAuthnSecure(): boolean {
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -36,12 +36,12 @@ export function isWebauthnSecure(): boolean {
|
||||||
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
|
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isWebauthnSupported(): boolean {
|
export function isWebAuthnSupported(): boolean {
|
||||||
return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function";
|
return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolean> {
|
export async function isWebAuthnPlatformAuthenticatorAvailable(): Promise<boolean> {
|
||||||
if (!isWebauthnSupported()) {
|
if (!isWebAuthnSupported()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ export async function getAuthenticationOptions(): Promise<PublicKeyCredentialReq
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startWebauthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
|
export async function startWebAuthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
|
||||||
const result: RegistrationResult = {
|
const result: RegistrationResult = {
|
||||||
result: AttestationResult.Failure,
|
result: AttestationResult.Failure,
|
||||||
};
|
};
|
|
@ -8,7 +8,7 @@ import {
|
||||||
SecondFactorPushSubRoute,
|
SecondFactorPushSubRoute,
|
||||||
SecondFactorRoute,
|
SecondFactorRoute,
|
||||||
SecondFactorTOTPSubRoute,
|
SecondFactorTOTPSubRoute,
|
||||||
SecondFactorWebauthnSubRoute,
|
SecondFactorWebAuthnSubRoute,
|
||||||
} from "@constants/Routes";
|
} from "@constants/Routes";
|
||||||
import { RedirectionURL } from "@constants/SearchParams";
|
import { RedirectionURL } from "@constants/SearchParams";
|
||||||
import { useConfiguration } from "@hooks/Configuration";
|
import { useConfiguration } from "@hooks/Configuration";
|
||||||
|
@ -127,8 +127,8 @@ const LoginPortal = function (props: Props) {
|
||||||
if (configuration.available_methods.size === 0) {
|
if (configuration.available_methods.size === 0) {
|
||||||
navigate(AuthenticatedRoute, false);
|
navigate(AuthenticatedRoute, false);
|
||||||
} else {
|
} else {
|
||||||
if (userInfo.method === SecondFactorMethod.Webauthn) {
|
if (userInfo.method === SecondFactorMethod.WebAuthn) {
|
||||||
navigate(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);
|
navigate(`${SecondFactorRoute}${SecondFactorWebAuthnSubRoute}`);
|
||||||
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||||
navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
|
navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -39,12 +39,12 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
|
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{props.methods.has(SecondFactorMethod.Webauthn) && props.webauthnSupported ? (
|
{props.methods.has(SecondFactorMethod.WebAuthn) && props.webauthnSupported ? (
|
||||||
<MethodItem
|
<MethodItem
|
||||||
id="webauthn-option"
|
id="webauthn-option"
|
||||||
method={translate("Security Key - WebAuthN")}
|
method={translate("Security Key - WebAuthN")}
|
||||||
icon={<FingerTouchIcon size={32} />}
|
icon={<FingerTouchIcon size={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.Webauthn)}
|
onClick={() => props.onClick(SecondFactorMethod.WebAuthn)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{props.methods.has(SecondFactorMethod.MobilePush) ? (
|
{props.methods.has(SecondFactorMethod.MobilePush) ? (
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
RegisterOneTimePasswordRoute,
|
RegisterOneTimePasswordRoute,
|
||||||
SecondFactorPushSubRoute,
|
SecondFactorPushSubRoute,
|
||||||
SecondFactorTOTPSubRoute,
|
SecondFactorTOTPSubRoute,
|
||||||
SecondFactorWebauthnSubRoute,
|
SecondFactorWebAuthnSubRoute,
|
||||||
SettingsRoute,
|
SettingsRoute,
|
||||||
SettingsTwoFactorAuthenticationSubRoute,
|
SettingsTwoFactorAuthenticationSubRoute,
|
||||||
LogoutRoute as SignOutRoute,
|
LogoutRoute as SignOutRoute,
|
||||||
|
@ -22,11 +22,11 @@ import { UserInfo } from "@models/UserInfo";
|
||||||
import { initiateTOTPRegistrationProcess } from "@services/RegisterDevice";
|
import { initiateTOTPRegistrationProcess } from "@services/RegisterDevice";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
import { setPreferred2FAMethod } from "@services/UserInfo";
|
import { setPreferred2FAMethod } from "@services/UserInfo";
|
||||||
import { isWebauthnSupported } from "@services/Webauthn";
|
import { isWebAuthnSupported } from "@services/WebAuthn";
|
||||||
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
|
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
|
||||||
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
|
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
|
||||||
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
|
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
|
||||||
import WebauthnMethod from "@views/LoginPortal/SecondFactor/WebauthnMethod";
|
import WebAuthnMethod from "@views/LoginPortal/SecondFactor/WebAuthnMethod";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
@ -44,12 +44,12 @@ const SecondFactorForm = function (props: Props) {
|
||||||
const [methodSelectionOpen, setMethodSelectionOpen] = useState(false);
|
const [methodSelectionOpen, setMethodSelectionOpen] = useState(false);
|
||||||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||||
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
||||||
const [webauthnSupported, setWebauthnSupported] = useState(false);
|
const [stateWebAuthnSupported, setStateWebAuthnSupported] = useState(false);
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWebauthnSupported(isWebauthnSupported());
|
setStateWebAuthnSupported(isWebAuthnSupported());
|
||||||
}, [setWebauthnSupported]);
|
}, [setStateWebAuthnSupported]);
|
||||||
|
|
||||||
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => {
|
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => {
|
||||||
return async () => {
|
return async () => {
|
||||||
|
@ -102,7 +102,7 @@ const SecondFactorForm = function (props: Props) {
|
||||||
<MethodSelectionDialog
|
<MethodSelectionDialog
|
||||||
open={methodSelectionOpen}
|
open={methodSelectionOpen}
|
||||||
methods={props.configuration.available_methods}
|
methods={props.configuration.available_methods}
|
||||||
webauthnSupported={webauthnSupported}
|
webauthnSupported={stateWebAuthnSupported}
|
||||||
onClose={() => setMethodSelectionOpen(false)}
|
onClose={() => setMethodSelectionOpen(false)}
|
||||||
onClick={handleMethodSelected}
|
onClick={handleMethodSelected}
|
||||||
/>
|
/>
|
||||||
|
@ -139,12 +139,12 @@ const SecondFactorForm = function (props: Props) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SecondFactorWebauthnSubRoute}
|
path={SecondFactorWebAuthnSubRoute}
|
||||||
element={
|
element={
|
||||||
<WebauthnMethod
|
<WebAuthnMethod
|
||||||
id="webauthn-method"
|
id="webauthn-method"
|
||||||
authenticationLevel={props.authenticationLevel}
|
authenticationLevel={props.authenticationLevel}
|
||||||
// Whether the user has a Webauthn device registered already
|
// Whether the user has a WebAuthn device registered already
|
||||||
registered={props.userInfo.has_webauthn}
|
registered={props.userInfo.has_webauthn}
|
||||||
onRegisterClick={() => {
|
onRegisterClick={() => {
|
||||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import WebauthnTryIcon from "@components/WebauthnTryIcon";
|
import WebAuthnTryIcon from "@components/WebAuthnTryIcon";
|
||||||
import { RedirectionURL } from "@constants/SearchParams";
|
import { RedirectionURL } from "@constants/SearchParams";
|
||||||
import { useIsMountedRef } from "@hooks/Mounted";
|
import { useIsMountedRef } from "@hooks/Mounted";
|
||||||
import { useQueryParam } from "@hooks/QueryParam";
|
import { useQueryParam } from "@hooks/QueryParam";
|
||||||
import { useWorkflow } from "@hooks/Workflow";
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import { AssertionResult, AssertionResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
import { AssertionResult, AssertionResultFailureString, WebAuthnTouchState } from "@models/WebAuthn";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/Webauthn";
|
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/WebAuthn";
|
||||||
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -20,8 +20,8 @@ export interface Props {
|
||||||
onSignInSuccess: (redirectURL: string | undefined) => void;
|
onSignInSuccess: (redirectURL: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebauthnMethod = function (props: Props) {
|
const WebAuthnMethod = function (props: Props) {
|
||||||
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
const [state, setState] = useState(WebAuthnTouchState.WaitTouch);
|
||||||
const redirectionURL = useQueryParam(RedirectionURL);
|
const redirectionURL = useQueryParam(RedirectionURL);
|
||||||
const [workflow, workflowID] = useWorkflow();
|
const [workflow, workflowID] = useWorkflow();
|
||||||
const mounted = useIsMountedRef();
|
const mounted = useIsMountedRef();
|
||||||
|
@ -37,11 +37,11 @@ const WebauthnMethod = function (props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebAuthnTouchState.WaitTouch);
|
||||||
const optionsStatus = await getAuthenticationOptions();
|
const optionsStatus = await getAuthenticationOptions();
|
||||||
|
|
||||||
if (optionsStatus.status !== 200 || optionsStatus.options == null) {
|
if (optionsStatus.status !== 200 || optionsStatus.options == null) {
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -52,7 +52,7 @@ const WebauthnMethod = function (props: Props) {
|
||||||
if (result.result !== AssertionResult.Success) {
|
if (result.result !== AssertionResult.Success) {
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
|
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
|
|
||||||
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
|
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
|
||||||
|
|
||||||
|
@ -61,14 +61,14 @@ const WebauthnMethod = function (props: Props) {
|
||||||
|
|
||||||
if (result.response == null) {
|
if (result.response == null) {
|
||||||
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
|
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
|
|
||||||
setState(WebauthnTouchState.InProgress);
|
setState(WebAuthnTouchState.InProgress);
|
||||||
|
|
||||||
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
|
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
|
||||||
|
|
||||||
|
@ -80,14 +80,14 @@ const WebauthnMethod = function (props: Props) {
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
|
|
||||||
onSignInErrorCallback(new Error("The server rejected the security key."));
|
onSignInErrorCallback(new Error("The server rejected the security key."));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the request was initiated and the user changed 2FA method in the meantime,
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
||||||
// the process is interrupted to avoid updating state of unmounted component.
|
// the process is interrupted to avoid updating state of unmounted component.
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onSignInErrorCallback,
|
onSignInErrorCallback,
|
||||||
|
@ -121,9 +121,9 @@ const WebauthnMethod = function (props: Props) {
|
||||||
state={methodState}
|
state={methodState}
|
||||||
onRegisterClick={props.onRegisterClick}
|
onRegisterClick={props.onRegisterClick}
|
||||||
>
|
>
|
||||||
<WebauthnTryIcon onRetryClick={doInitiateSignIn} webauthnTouchState={state} />
|
<WebAuthnTryIcon onRetryClick={doInitiateSignIn} webauthnTouchState={state} />
|
||||||
</MethodContainer>
|
</MethodContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WebauthnMethod;
|
export default WebAuthnMethod;
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
|
|
||||||
import { AutheliaState } from "@services/State";
|
import { AutheliaState } from "@services/State";
|
||||||
import WebauthnDevices from "@views/Settings/TwoFactorAuthentication/WebauthnDevices";
|
import WebAuthnDevices from "@views/Settings/TwoFactorAuthentication/WebAuthnDevices";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: AutheliaState;
|
state: AutheliaState;
|
||||||
|
@ -13,7 +13,7 @@ export default function TwoFactorAuthSettings(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<WebauthnDevices state={props.state} />
|
<WebAuthnDevices state={props.state} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,15 +3,15 @@ import React from "react";
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice;
|
device: WebAuthnDevice;
|
||||||
handleClose: (ok: boolean) => void;
|
handleClose: (ok: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceDeleteDialog(props: Props) {
|
export default function WebAuthnDeviceDeleteDialog(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
@ -20,10 +20,10 @@ export default function WebauthnDeviceDeleteDialog(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleCancel}>
|
<Dialog open={props.open} onClose={handleCancel}>
|
||||||
<DialogTitle>{translate("Remove Webauthn Credential")}</DialogTitle>
|
<DialogTitle>{translate("Remove WebAuthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{translate("Are you sure you want to remove the Webauthn credential from from your account", {
|
{translate("Are you sure you want to remove the WebAuthn credential from from your account", {
|
||||||
description: props.device.description,
|
description: props.device.description,
|
||||||
})}
|
})}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
|
@ -16,23 +16,23 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { WebauthnDevice, toTransportName } from "@models/Webauthn";
|
import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice;
|
device: WebAuthnDevice;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDetailsDeleteDialog(props: Props) {
|
export default function WebAuthnDetailsDeleteDialog(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.handleClose}>
|
<Dialog open={props.open} onClose={props.handleClose}>
|
||||||
<DialogTitle>{translate("Webauthn Credential Details")}</DialogTitle>
|
<DialogTitle>{translate("WebAuthn Credential Details")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ mb: 3 }}>
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
{translate("Extended Webauthn credential information for security key", {
|
{translate("Extended WebAuthn credential information for security key", {
|
||||||
description: props.device.description,
|
description: props.device.description,
|
||||||
})}
|
})}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
|
@ -3,15 +3,15 @@ import React, { MutableRefObject, useRef, useState } from "react";
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice;
|
device: WebAuthnDevice;
|
||||||
handleClose: (ok: boolean, name: string) => void;
|
handleClose: (ok: boolean, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceEditDialog(props: Props) {
|
export default function WebAuthnDeviceEditDialog(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [deviceName, setName] = useState("");
|
const [deviceName, setName] = useState("");
|
||||||
|
@ -34,9 +34,9 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleCancel}>
|
<Dialog open={props.open} onClose={handleCancel}>
|
||||||
<DialogTitle>{translate("Edit Webauthn Credential")}</DialogTitle>
|
<DialogTitle>{translate("Edit WebAuthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>{translate("Enter a new name for this Webauthn credential")}</DialogContentText>
|
<DialogContentText>{translate("Enter a new name for this WebAuthn credential")}</DialogContentText>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
inputRef={nameRef}
|
inputRef={nameRef}
|
|
@ -8,19 +8,19 @@ import { Box, Button, CircularProgress, Paper, Stack, Tooltip, Typography } from
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||||
import { deleteDevice, updateDevice } from "@services/Webauthn";
|
import { deleteDevice, updateDevice } from "@services/WebAuthn";
|
||||||
import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog";
|
import WebAuthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDeleteDialog";
|
||||||
import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog";
|
import WebAuthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog";
|
||||||
import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog";
|
import WebAuthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number;
|
index: number;
|
||||||
device: WebauthnDevice;
|
device: WebAuthnDevice;
|
||||||
handleEdit: () => void;
|
handleEdit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceItem(props: Props) {
|
export default function WebAuthnDeviceItem(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||||
|
@ -47,19 +47,19 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
|
|
||||||
if (response.data.status === "KO") {
|
if (response.data.status === "KO") {
|
||||||
if (response.data.elevation) {
|
if (response.data.elevation) {
|
||||||
createErrorNotification(translate("You must be elevated to update Webauthn credentials"));
|
createErrorNotification(translate("You must be elevated to update WebAuthn credentials"));
|
||||||
} else if (response.data.authentication) {
|
} else if (response.data.authentication) {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
translate("You must have a higher authentication level to update Webauthn credentials"),
|
translate("You must have a higher authentication level to update WebAuthn credentials"),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createErrorNotification(translate("There was a problem updating the Webauthn credential"));
|
createErrorNotification(translate("There was a problem updating the WebAuthn credential"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSuccessNotification(translate("Successfully updated the Webauthn credential"));
|
createSuccessNotification(translate("Successfully updated the WebAuthn credential"));
|
||||||
|
|
||||||
props.handleEdit();
|
props.handleEdit();
|
||||||
};
|
};
|
||||||
|
@ -79,19 +79,19 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
|
|
||||||
if (response.data.status === "KO") {
|
if (response.data.status === "KO") {
|
||||||
if (response.data.elevation) {
|
if (response.data.elevation) {
|
||||||
createErrorNotification(translate("You must be elevated to delete Webauthn credentials"));
|
createErrorNotification(translate("You must be elevated to delete WebAuthn credentials"));
|
||||||
} else if (response.data.authentication) {
|
} else if (response.data.authentication) {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
translate("You must have a higher authentication level to delete Webauthn credentials"),
|
translate("You must have a higher authentication level to delete WebAuthn credentials"),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createErrorNotification(translate("There was a problem deleting the Webauthn credential"));
|
createErrorNotification(translate("There was a problem deleting the WebAuthn credential"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSuccessNotification(translate("Successfully deleted the Webauthn credential"));
|
createSuccessNotification(translate("Successfully deleted the WebAuthn credential"));
|
||||||
|
|
||||||
props.handleEdit();
|
props.handleEdit();
|
||||||
};
|
};
|
||||||
|
@ -100,15 +100,15 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Paper variant="outlined">
|
<Paper variant="outlined">
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<WebauthnDeviceDetailsDialog
|
<WebAuthnDeviceDetailsDialog
|
||||||
device={props.device}
|
device={props.device}
|
||||||
open={showDialogDetails}
|
open={showDialogDetails}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
setShowDialogDetails(false);
|
setShowDialogDetails(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
<WebAuthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
||||||
<WebauthnDeviceDeleteDialog
|
<WebAuthnDeviceDeleteDialog
|
||||||
device={props.device}
|
device={props.device}
|
||||||
open={showDialogDelete}
|
open={showDialogDelete}
|
||||||
handleClose={handleDelete}
|
handleClose={handleDelete}
|
||||||
|
@ -157,7 +157,7 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Tooltip title={translate("Display extended information for this Webauthn credential")}>
|
<Tooltip title={translate("Display extended information for this WebAuthn credential")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -167,7 +167,7 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
{translate("Info")}
|
{translate("Info")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={translate("Edit information for this Webauthn credential")}>
|
<Tooltip title={translate("Edit information for this WebAuthn credential")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -177,7 +177,7 @@ export default function WebauthnDeviceItem(props: Props) {
|
||||||
{translate("Edit")}
|
{translate("Edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={translate("Remove this Webauthn credential")}>
|
<Tooltip title={translate("Remove this WebAuthn credential")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
|
@ -21,10 +21,10 @@ import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescri
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import InformationIcon from "@components/InformationIcon";
|
import InformationIcon from "@components/InformationIcon";
|
||||||
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon";
|
import WebAuthnRegisterIcon from "@components/WebAuthnRegisterIcon";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { AttestationResult, AttestationResultFailureString, WebauthnTouchState } from "@models/Webauthn";
|
import { AttestationResult, AttestationResultFailureString, WebAuthnTouchState } from "@models/WebAuthn";
|
||||||
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn";
|
import { finishRegistration, getAttestationCreationOptions, startWebAuthnRegistration } from "@services/WebAuthn";
|
||||||
|
|
||||||
const steps = ["Description", "Verification"];
|
const steps = ["Description", "Verification"];
|
||||||
|
|
||||||
|
@ -34,13 +34,13 @@ interface Props {
|
||||||
setCancelled: () => void;
|
setCancelled: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebauthnDeviceRegisterDialog = function (props: Props) {
|
const WebAuthnDeviceRegisterDialog = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
const [state, setState] = useState(WebAuthnTouchState.WaitTouch);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
|
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
|
||||||
const [timeout, setTimeout] = useState<number | null>(null);
|
const [timeout, setTimeout] = useState<number | null>(null);
|
||||||
|
@ -50,7 +50,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebAuthnTouchState.WaitTouch);
|
||||||
setOptions(null);
|
setOptions(null);
|
||||||
setActiveStep(0);
|
setActiveStep(0);
|
||||||
setTimeout(null);
|
setTimeout(null);
|
||||||
|
@ -73,9 +73,9 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
setActiveStep(1);
|
setActiveStep(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebAuthnTouchState.WaitTouch);
|
||||||
|
|
||||||
const resultCredentialCreation = await startWebauthnRegistration(options);
|
const resultCredentialCreation = await startWebAuthnRegistration(options);
|
||||||
|
|
||||||
setTimeout(null);
|
setTimeout(null);
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result));
|
createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result));
|
||||||
setState(WebauthnTouchState.Failure);
|
setState(WebAuthnTouchState.Failure);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
|
@ -109,7 +109,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
}, [options, createErrorNotification, handleClose]);
|
}, [options, createErrorNotification, handleClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
if (state !== WebAuthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,12 +151,12 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
break;
|
break;
|
||||||
case 409:
|
case 409:
|
||||||
setErrorDescription(true);
|
setErrorDescription(true);
|
||||||
createErrorNotification(translate("A Webauthn Credential with that Description already exists."));
|
createErrorNotification(translate("A WebAuthn Credential with that Description already exists."));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
translate("Error occurred obtaining the Webauthn Credential creation options."),
|
translate("Error occurred obtaining the WebAuthn Credential creation options."),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [createErrorNotification, credentialDescription, translate]);
|
}, [createErrorNotification, credentialDescription, translate]);
|
||||||
|
@ -214,7 +214,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box className={styles.icon}>
|
<Box className={styles.icon}>
|
||||||
{timeout !== null ? <WebauthnRegisterIcon timeout={timeout} /> : null}
|
{timeout !== null ? <WebAuthnRegisterIcon timeout={timeout} /> : null}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography className={styles.instruction}>
|
<Typography className={styles.instruction}>
|
||||||
{translate("Touch the token on your security key")}
|
{translate("Touch the token on your security key")}
|
||||||
|
@ -234,11 +234,11 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
||||||
<DialogTitle>{translate("Register Webauthn Credential")}</DialogTitle>
|
<DialogTitle>{translate("Register WebAuthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ mb: 3 }}>
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
{translate(
|
{translate(
|
||||||
"This page allows registration of a new Security Key backed by modern Webauthn Credential technology.",
|
"This page allows registration of a new Security Key backed by modern WebAuthn Credential technology.",
|
||||||
)}
|
)}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||||
|
@ -264,8 +264,8 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
color={activeStep === 1 && state !== WebauthnTouchState.Failure ? "primary" : "error"}
|
color={activeStep === 1 && state !== WebAuthnTouchState.Failure ? "primary" : "error"}
|
||||||
disabled={activeStep === 1 && state !== WebauthnTouchState.Failure}
|
disabled={activeStep === 1 && state !== WebAuthnTouchState.Failure}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
{translate("Cancel")}
|
{translate("Cancel")}
|
||||||
|
@ -286,7 +286,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WebauthnDeviceRegisterDialog;
|
export default WebAuthnDeviceRegisterDialog;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
icon: {
|
icon: {
|
|
@ -5,17 +5,17 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { AutheliaState } from "@services/State";
|
import { AutheliaState } from "@services/State";
|
||||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
import WebauthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog";
|
import WebAuthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog";
|
||||||
import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack";
|
import WebAuthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state: AutheliaState;
|
state: AutheliaState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDevices(props: Props) {
|
export default function WebAuthnDevices(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [showWebauthnDeviceRegisterDialog, setShowWebauthnDeviceRegisterDialog] = useState<boolean>(false);
|
const [showWebAuthnDeviceRegisterDialog, setShowWebAuthnDeviceRegisterDialog] = useState<boolean>(false);
|
||||||
const [refreshState, setRefreshState] = useState<number>(0);
|
const [refreshState, setRefreshState] = useState<number>(0);
|
||||||
|
|
||||||
const handleIncrementRefreshState = () => {
|
const handleIncrementRefreshState = () => {
|
||||||
|
@ -24,13 +24,13 @@ export default function WebauthnDevices(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<WebauthnDeviceRegisterDialog
|
<WebAuthnDeviceRegisterDialog
|
||||||
open={showWebauthnDeviceRegisterDialog}
|
open={showWebAuthnDeviceRegisterDialog}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
handleIncrementRefreshState();
|
handleIncrementRefreshState();
|
||||||
}}
|
}}
|
||||||
setCancelled={() => {
|
setCancelled={() => {
|
||||||
setShowWebauthnDeviceRegisterDialog(false);
|
setShowWebAuthnDeviceRegisterDialog(false);
|
||||||
handleIncrementRefreshState();
|
handleIncrementRefreshState();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -38,15 +38,15 @@ export default function WebauthnDevices(props: Props) {
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5">{translate("Webauthn Credentials")}</Typography>
|
<Typography variant="h5">{translate("WebAuthn Credentials")}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Tooltip title={translate("Click to add a Webauthn credential to your account")}>
|
<Tooltip title={translate("Click to add a WebAuthn credential to your account")}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowWebauthnDeviceRegisterDialog(true);
|
setShowWebAuthnDeviceRegisterDialog(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{translate("Add Credential")}
|
{translate("Add Credential")}
|
||||||
|
@ -54,7 +54,7 @@ export default function WebauthnDevices(props: Props) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Suspense fallback={<LoadingPage />}>
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<WebauthnDevicesStack
|
<WebAuthnDevicesStack
|
||||||
refreshState={refreshState}
|
refreshState={refreshState}
|
||||||
incrementRefreshState={handleIncrementRefreshState}
|
incrementRefreshState={handleIncrementRefreshState}
|
||||||
/>
|
/>
|
|
@ -3,24 +3,24 @@ import React, { Fragment, useEffect, useState } from "react";
|
||||||
import { Stack, Typography } from "@mui/material";
|
import { Stack, Typography } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebAuthnDevice } from "@models/WebAuthn";
|
||||||
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
|
import { getWebAuthnDevices } from "@services/UserWebAuthnDevices";
|
||||||
import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem";
|
import WebAuthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
refreshState: number;
|
refreshState: number;
|
||||||
incrementRefreshState: () => void;
|
incrementRefreshState: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDevicesStack(props: Props) {
|
export default function WebAuthnDevicesStack(props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [devices, setDevices] = useState<WebauthnDevice[] | null>(null);
|
const [devices, setDevices] = useState<WebAuthnDevice[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
setDevices(null);
|
setDevices(null);
|
||||||
const devices = await getWebauthnDevices();
|
const devices = await getWebAuthnDevices();
|
||||||
setDevices(devices);
|
setDevices(devices);
|
||||||
})();
|
})();
|
||||||
}, [props.refreshState]);
|
}, [props.refreshState]);
|
||||||
|
@ -30,11 +30,11 @@ export default function WebauthnDevicesStack(props: Props) {
|
||||||
{devices !== null && devices.length !== 0 ? (
|
{devices !== null && devices.length !== 0 ? (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
{devices.map((x, idx) => (
|
{devices.map((x, idx) => (
|
||||||
<WebauthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
|
<WebAuthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Typography>{translate("No Registered Webauthn Credentials")}</Typography>
|
<Typography>{translate("No Registered WebAuthn Credentials")}</Typography>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
Loading…
Reference in New Issue