diff --git a/internal/middlewares/authelia_context.go b/internal/middlewares/authelia_context.go
index e30e58399..86a7d30ae 100644
--- a/internal/middlewares/authelia_context.go
+++ b/internal/middlewares/authelia_context.go
@@ -67,8 +67,19 @@ func (ctx *AutheliaCtx) Error(err error, message string) {
// SetJSONError sets the body of the response to an JSON error KO message.
func (ctx *AutheliaCtx) SetJSONError(message string) {
- if replyErr := ctx.ReplyJSON(ErrorResponse{Status: "KO", Message: message}, 0); replyErr != nil {
- ctx.Logger.Error(replyErr)
+ if err := ctx.ReplyJSON(ErrorResponse{Status: "KO", Message: message}, 0); err != nil {
+ ctx.Logger.Error(err)
+ }
+}
+
+// SetAuthenticationErrorJSON sets the body of the response to an JSON error KO message.
+func (ctx *AutheliaCtx) SetAuthenticationErrorJSON(status int, message string, authentication, elevation bool) {
+ if status > fasthttp.StatusOK {
+ ctx.SetStatusCode(status)
+ }
+
+ if err := ctx.ReplyJSON(AuthenticationErrorResponse{Status: "KO", Message: message, Authentication: authentication, Elevation: elevation}, 0); err != nil {
+ ctx.Logger.Error(err)
}
}
diff --git a/internal/middlewares/require_authentication_level.go b/internal/middlewares/require_authentication_level.go
index 9b313f1b6..7384685a0 100644
--- a/internal/middlewares/require_authentication_level.go
+++ b/internal/middlewares/require_authentication_level.go
@@ -1,6 +1,8 @@
package middlewares
import (
+ "github.com/valyala/fasthttp"
+
"github.com/authelia/authelia/v4/internal/authentication"
)
@@ -27,3 +29,15 @@ func Require2FA(next RequestHandler) RequestHandler {
next(ctx)
}
}
+
+// Require2FAWithAPIResponse requires the user to have authenticated with two-factor authentication.
+func Require2FAWithAPIResponse(next RequestHandler) RequestHandler {
+ return func(ctx *AutheliaCtx) {
+ if ctx.GetSession().AuthenticationLevel < authentication.TwoFactor {
+ ctx.SetAuthenticationErrorJSON(fasthttp.StatusForbidden, "Authentication Required.", true, false)
+ return
+ }
+
+ next(ctx)
+ }
+}
diff --git a/internal/middlewares/types.go b/internal/middlewares/types.go
index f26e57fc7..2e7d8542a 100644
--- a/internal/middlewares/types.go
+++ b/internal/middlewares/types.go
@@ -117,3 +117,11 @@ type ErrorResponse struct {
Status string `json:"status"`
Message string `json:"message"`
}
+
+// AuthenticationErrorResponse model of an error response.
+type AuthenticationErrorResponse struct {
+ Status string `json:"status"`
+ Message string `json:"message"`
+ Authentication bool `json:"authentication"`
+ Elevation bool `json:"elevation"`
+}
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index c6fa06a59..0620f44fa 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -146,7 +146,7 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
middleware2FA := middlewares.NewBridgeBuilder(config, providers).
WithPreMiddlewares(middlewares.SecurityHeaders, middlewares.SecurityHeadersNoStore, middlewares.SecurityHeadersCSPNone).
- WithPostMiddlewares(middlewares.Require2FA).
+ WithPostMiddlewares(middlewares.Require2FAWithAPIResponse).
Build()
r.GET("/api/health", middlewareAPI(handlers.HealthGET))
diff --git a/web/src/components/LoadingButton.tsx b/web/src/components/LoadingButton.tsx
new file mode 100644
index 000000000..353b5782d
--- /dev/null
+++ b/web/src/components/LoadingButton.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+
+import { Button, CircularProgress } from "@mui/material";
+import { ButtonProps } from "@mui/material/Button";
+
+export interface Props extends ButtonProps {
+ loading: boolean;
+}
+
+function LoadingButton(props: Props) {
+ let { loading, ...childProps } = props;
+ if (loading) {
+ childProps = {
+ ...childProps,
+ startIcon: ,
+ color: "inherit",
+ onClick: undefined,
+ };
+ }
+ return ;
+}
+
+export default LoadingButton;
diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts
index 56c853c5d..f2a0e1eb6 100644
--- a/web/src/services/Api.ts
+++ b/web/src/services/Api.ts
@@ -42,21 +42,30 @@ export const UserInfoTOTPConfigurationPath = basePath + "/api/user/info/totp";
export const ConfigurationPath = basePath + "/api/configuration";
export const PasswordPolicyConfigurationPath = basePath + "/api/configuration/password-policy";
+export interface AuthenticationErrorResponse extends ErrorResponse {
+ authentication: boolean;
+ elevation: boolean;
+}
+
export interface ErrorResponse {
status: "KO";
message: string;
}
-export interface Response {
- status: "OK";
+export interface Response extends OKResponse {
data: T;
}
-export interface OptionalDataResponse {
- status: "OK";
+export interface OptionalDataResponse extends OKResponse {
data?: T;
}
+export interface OKResponse {
+ status: "OK";
+}
+
+export type AuthenticationResponse = Response | AuthenticationErrorResponse;
+export type AuthenticationOKResponse = OKResponse | AuthenticationErrorResponse;
export type OptionalDataServiceResponse = OptionalDataResponse | ErrorResponse;
export type ServiceResponse = Response | ErrorResponse;
@@ -81,3 +90,7 @@ export function hasServiceError(resp: AxiosResponse>) {
}
return { errored: false, message: null };
}
+
+export function validateStatusAuthentication(status: number): boolean {
+ return (status >= 200 && status < 300) || status === 401 || status === 403;
+}
diff --git a/web/src/services/Webauthn.ts b/web/src/services/Webauthn.ts
index b224e8cf6..ea354123a 100644
--- a/web/src/services/Webauthn.ts
+++ b/web/src/services/Webauthn.ts
@@ -17,15 +17,16 @@ import {
PublicKeyCredentialJSON,
PublicKeyCredentialRequestOptionsJSON,
PublicKeyCredentialRequestOptionsStatus,
- WebauthnDeviceUpdateRequest,
} from "@models/Webauthn";
import {
+ AuthenticationOKResponse,
OptionalDataServiceResponse,
ServiceResponse,
WebauthnAssertionPath,
WebauthnAttestationPath,
WebauthnDevicePath,
WebauthnIdentityFinishPath,
+ validateStatusAuthentication,
} from "@services/Api";
import { SignInResponse } from "@services/SignIn";
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64";
@@ -398,14 +399,19 @@ export async function performAssertionCeremony(
return AssertionResult.Failure;
}
-export async function deleteDevice(deviceID: string): Promise {
- let response = await axios.delete(`${WebauthnDevicePath}/${deviceID}`);
- return response.status;
+export async function deleteDevice(deviceID: string) {
+ return await axios({
+ method: "DELETE",
+ url: `${WebauthnDevicePath}/${deviceID}`,
+ validateStatus: validateStatusAuthentication,
+ });
}
-export async function updateDevice(deviceID: string, description: string): Promise {
- let response = await axios.put>(`${WebauthnDevicePath}/${deviceID}`, {
- description: description,
+export async function updateDevice(deviceID: string, description: string) {
+ return await axios({
+ method: "PUT",
+ url: `${WebauthnDevicePath}/${deviceID}`,
+ data: { description: description },
+ validateStatus: validateStatusAuthentication,
});
- return response.status;
}
diff --git a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx b/web/src/views/DeviceRegistration/RegisterWebauthn.tsx
index fcafc46dd..b4ebcb25c 100644
--- a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx
+++ b/web/src/views/DeviceRegistration/RegisterWebauthn.tsx
@@ -1,4 +1,4 @@
-import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
+import React, { Fragment, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
import { Box, Button, Grid, Stack, Step, StepLabel, Stepper, Theme, Typography } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
@@ -142,7 +142,7 @@ const RegisterWebauthn = function (props: Props) {
switch (step) {
case 0:
return (
- <>
+
@@ -156,7 +156,7 @@ const RegisterWebauthn = function (props: Props) {
- >
+
);
case 1:
return (
diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx
index 3028dfa69..e8041cee9 100644
--- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx
+++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx
@@ -4,10 +4,10 @@ import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import KeyRoundedIcon from "@mui/icons-material/KeyRounded";
-import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
-import { ButtonProps } from "@mui/material/Button";
+import { Box, Button, Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
+import LoadingButton from "@components/LoadingButton";
import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@models/Webauthn";
import { deleteDevice, updateDevice } from "@services/Webauthn";
@@ -25,11 +25,12 @@ interface Props {
export default function WebauthnDeviceItem(props: Props) {
const { t: translate } = useTranslation("settings");
- const { createErrorNotification } = useNotifications();
+ const { createSuccessNotification, createErrorNotification } = useNotifications();
const [showDialogDetails, setShowDialogDetails] = useState(false);
const [showDialogEdit, setShowDialogEdit] = useState(false);
const [showDialogDelete, setShowDialogDelete] = useState(false);
+
const [loadingEdit, setLoadingEdit] = useState(false);
const [loadingDelete, setLoadingDelete] = useState(false);
@@ -42,17 +43,24 @@ export default function WebauthnDeviceItem(props: Props) {
setLoadingEdit(true);
- const status = await updateDevice(props.device.id, name);
-
- console.log("Status was: ", status);
+ const response = await updateDevice(props.device.id, name);
setLoadingEdit(false);
- if (status !== 200) {
- createErrorNotification(translate("There was a problem updating the device"));
+ if (response.data.status === "KO") {
+ if (response.data.elevation) {
+ createErrorNotification(translate("You must be elevated to update the device"));
+ } else if (response.data.authentication) {
+ createErrorNotification(translate("You must have a higher authentication level to update the device"));
+ } else {
+ createErrorNotification(translate("There was a problem updating the device"));
+ }
+
return;
}
+ createSuccessNotification(translate("Successfully updated the device"));
+
props.handleDeviceEdit(props.index, { ...props.device, description: name });
};
@@ -65,17 +73,24 @@ export default function WebauthnDeviceItem(props: Props) {
setLoadingDelete(true);
- const status = await deleteDevice(props.device.id);
-
- console.log("Status was: ", status);
+ const response = await deleteDevice(props.device.id);
setLoadingDelete(false);
- if (status !== 200) {
- createErrorNotification(translate("There was a problem deleting the device"));
+ if (response.data.status === "KO") {
+ if (response.data.elevation) {
+ createErrorNotification(translate("You must be elevated to delete the device"));
+ } else if (response.data.authentication) {
+ createErrorNotification(translate("You must have a higher authentication level to delete the device"));
+ } else {
+ createErrorNotification(translate("There was a problem deleting the device"));
+ }
+
return;
}
+ createSuccessNotification(translate("Successfully deleted the device"));
+
props.handleDeviceDelete(props.device);
};
@@ -139,20 +154,3 @@ export default function WebauthnDeviceItem(props: Props) {
);
}
-
-interface LoadingButtonProps extends ButtonProps {
- loading: boolean;
-}
-
-function LoadingButton(props: LoadingButtonProps) {
- let { loading, ...childProps } = props;
- if (loading) {
- childProps = {
- ...childProps,
- startIcon: ,
- color: "inherit",
- onClick: undefined,
- };
- }
- return ;
-}
diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx
index 90ed0e8e0..7c73c7b8f 100644
--- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx
+++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx
@@ -1,6 +1,6 @@
-import React, { useEffect, useState } from "react";
+import React, { Fragment, useEffect, useState } from "react";
-import { Stack } from "@mui/material";
+import { Stack, Typography } from "@mui/material";
import { WebauthnDevice } from "@models/Webauthn";
import { getWebauthnDevices } from "@services/UserWebauthnDevices";
@@ -35,17 +35,21 @@ export default function WebauthnDevicesStack(props: Props) {
};
return (
-
- {devices
- ? devices.map((x, idx) => (
-
- ))
- : null}
-
+
+ {devices ? (
+
+ {devices.map((x, idx) => (
+
+ ))}
+
+ ) : (
+ No Registered Webauthn Devices
+ )}
+
);
}