diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 176c2c3c90..48f2be900d 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 343 +INVENTREE_API_VERSION = 344 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v344 -> 2025-06-02 : https://github.com/inventree/InvenTree/pull/9714 + - Updates alauth version and adds device trust as a factor + v343 -> 2025-06-02 : https://github.com/inventree/InvenTree/pull/9717 - Add ISO currency codes to the description text for currency options diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 95a3388330..7d4738c2cf 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1327,6 +1327,7 @@ ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True HEADLESS_ONLY = True HEADLESS_TOKEN_STRATEGY = 'InvenTree.auth_overrides.DRFTokenStrategy' +HEADLESS_CLIENTS = 'browser' MFA_ENABLED = get_boolean_setting( 'INVENTREE_MFA_ENABLED', 'mfa_enabled', True ) # TODO re-implement @@ -1336,6 +1337,7 @@ MFA_SUPPORTED_TYPES = get_setting( ['totp', 'recovery_codes'], typecast=list, ) +MFA_TRUST_ENABLED = True LOGOUT_REDIRECT_URL = get_setting( 'INVENTREE_LOGOUT_REDIRECT_URL', 'logout_redirect_url', 'index' diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 41d651680f..cec6a1614d 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -423,8 +423,8 @@ django==4.2.21 \ # djangorestframework # djangorestframework-simplejwt # drf-spectacular -django-allauth[mfa, openid, saml, socialaccount]==65.4.1 \ - --hash=sha256:60b32aef7dbbcc213319aa4fd8f570e985266ea1162ae6ef7a26a24efca85c8c +django-allauth[mfa, openid, saml, socialaccount]==65.9.0 \ + --hash=sha256:a06bca9974df44321e94c33bcf770bb6f924d1a44b57defbce4d7ec54a55483e # via -r src/backend/requirements.in django-cleanup==9.0.0 \ --hash=sha256:19f8b0e830233f9f0f683b17181f414672a0f48afe3ea3cc80ba47ae40ad880c \ diff --git a/src/frontend/lib/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx index d247e69ba2..96f08d4224 100644 --- a/src/frontend/lib/enums/ApiEndpoints.tsx +++ b/src/frontend/lib/enums/ApiEndpoints.tsx @@ -30,6 +30,7 @@ export enum ApiEndpoints { auth_recovery = 'auth/v1/account/authenticators/recovery-codes', auth_mfa_reauthenticate = 'auth/v1/auth/2fa/reauthenticate', auth_totp = 'auth/v1/account/authenticators/totp', + auth_trust = 'auth/v1/auth/2fa/trust', auth_reauthenticate = 'auth/v1/auth/reauthenticate', auth_email = 'auth/v1/account/email', auth_email_verify = 'auth/v1/auth/email/verify', diff --git a/src/frontend/lib/types/Auth.tsx b/src/frontend/lib/types/Auth.tsx index a9dfa993f5..5f6b0670cf 100644 --- a/src/frontend/lib/types/Auth.tsx +++ b/src/frontend/lib/types/Auth.tsx @@ -13,7 +13,8 @@ export enum FlowEnum { ProviderToken = 'provider_token', MfaAuthenticate = 'mfa_authenticate', Reauthenticate = 'reauthenticate', - MfaReauthenticate = 'mfa_reauthenticate' + MfaReauthenticate = 'mfa_reauthenticate', + MfaTrust = 'mfa_trust' } export interface Flow { diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 6e4b376ab4..943d61b7bf 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -262,26 +262,19 @@ export function handleReset( export function handleMfaLogin( navigate: NavigateFunction, location: Location, - values: { code: string }, + values: { code: string; remember?: boolean }, setError: (message: string | undefined) => void ) { - const { setAuthenticated, fetchUserState } = useUserState.getState(); const { setAuthContext } = useServerApiState.getState(); authApi(apiUrl(ApiEndpoints.auth_login_2fa), undefined, 'post', { code: values.code }) .then((response) => { - setError(undefined); - setAuthContext(response.data?.data); - setAuthenticated(); - - fetchUserState().finally(() => { - observeProfile(); - followRedirect(navigate, location?.state); - }); + handleSuccessFullAuth(response, navigate, location, setError); }) .catch((err) => { + // Already logged in, but with a different session if (err?.response?.status == 409) { notifications.show({ title: t`Already logged in`, @@ -289,6 +282,19 @@ export function handleMfaLogin( color: 'red', autoClose: false }); + // MFA trust flow pending + } else if (err?.response?.status == 401) { + const mfa_trust = err.response.data.data.flows.find( + (flow: any) => flow.id == FlowEnum.MfaTrust + ); + if (mfa_trust?.is_pending) { + setAuthContext(err.response.data.data); + authApi(apiUrl(ApiEndpoints.auth_trust), undefined, 'post', { + trust: values.remember ?? false + }).then((response) => { + handleSuccessFullAuth(response, navigate, location, setError); + }); + } } else { const errors = err.response?.data?.errors; let msg = t`An error occurred`; @@ -351,6 +357,35 @@ export const checkLoginState = async ( } }; +function handleSuccessFullAuth( + response?: any, + navigate?: NavigateFunction, + location?: Location, + setError?: (message: string | undefined) => void +) { + const { setAuthenticated, fetchUserState } = useUserState.getState(); + const { setAuthContext } = useServerApiState.getState(); + + if (setError) { + // If an error function is provided, clear any previous errors + setError(undefined); + } + + if (response.data?.data) { + setAuthContext(response.data?.data); + } + setAuthenticated(); + + fetchUserState().finally(() => { + observeProfile(); + fetchGlobalStates(navigate); + + if (navigate) { + followRedirect(navigate, location?.state); + } + }); +} + /* * Return the value of the CSRF cookie, if available */ diff --git a/src/frontend/src/pages/Auth/MFA.tsx b/src/frontend/src/pages/Auth/MFA.tsx index 50e48c9b2d..30b59add86 100644 --- a/src/frontend/src/pages/Auth/MFA.tsx +++ b/src/frontend/src/pages/Auth/MFA.tsx @@ -1,6 +1,6 @@ import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { Button, TextInput } from '@mantine/core'; +import { Button, Checkbox, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -8,7 +8,7 @@ import { handleMfaLogin } from '../../functions/auth'; import { Wrapper } from './Layout'; export default function Mfa() { - const simpleForm = useForm({ initialValues: { code: '' } }); + const simpleForm = useForm({ initialValues: { code: '', remember: false } }); const navigate = useNavigate(); const location = useLocation(); const [loginError, setLoginError] = useState(undefined); @@ -23,6 +23,12 @@ export default function Mfa() { {...simpleForm.getInputProps('code')} error={loginError} /> +