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 + )} + ); }