diff --git a/content/collections/pages/users.md b/content/collections/pages/users.md index d470f9039..9f675e6d1 100644 --- a/content/collections/pages/users.md +++ b/content/collections/pages/users.md @@ -284,6 +284,28 @@ Statamic uses your `APP_KEY` to encrypt the two-factor authentication secret and You may run into issues with two-factor authentication if you have different `APP_KEY` values between environments *and* they share the same users (eg. you're tracking users in Git). You may want to disable 2FA locally in this case. ::: +### Frontend Two-Factor Authentication + +Users who authenticate through your site's frontend (via [`{{ user:login_form }}`](/tags/user-login_form)) can also set up and challenge 2FA without ever touching the Control Panel. Statamic ships a set of tags for building those pages yourself: + +- [`{{ user:two_factor_challenge_form }}`](/tags/user-two_factor_challenge_form) — the code verification form shown during login +- [`{{ user:two_factor_enable_form }}`](/tags/user-two_factor_enable_form) — step 1 of setup, generates the secret +- [`{{ user:two_factor_setup_form }}`](/tags/user-two_factor_setup_form) — step 2 of setup, displays the QR code and confirms the code +- [`{{ user:disable_two_factor_form }}`](/tags/user-disable_two_factor_form) — lets users turn 2FA off +- [`{{ user:two_factor_recovery_codes }}`](/tags/user-two_factor_recovery_codes) and [`{{ user:reset_two_factor_recovery_codes_form }}`](/tags/user-reset_two_factor_recovery_codes_form) — show and regenerate recovery codes +- [`{{ user:two_factor_enabled }}`](/tags/user-two_factor_enabled) — a boolean for conditionally rendering the above + +When a user with 2FA enabled signs in on the frontend, Statamic redirects them to a challenge page. When 2FA is enforced for the user's role and they haven't set it up, Statamic redirects them to a setup page. Point these redirects at your own pages with the following config keys: + +```php +// config/statamic/users.php + +'two_factor_challenge_url' => '/account/2fa/challenge', +'two_factor_setup_url' => '/account/2fa/setup', +``` + +Leave either value `null` to use Statamic's built-in page for that step. Control Panel flows are unaffected — they always use their own pages. + ## Passkeys Statamic supports **passkeys** as a secure alternative to email-and-password logins. Passkeys are a passwordless authentication method built on WebAuthn and are supported by most modern operating systems and password managers. On macOS, iOS, and iPadOS, for example, you can sign in using Touch ID or Face ID. diff --git a/content/collections/tags/user-disable_two_factor_form.md b/content/collections/tags/user-disable_two_factor_form.md new file mode 100644 index 000000000..23c94c8c6 --- /dev/null +++ b/content/collections/tags/user-disable_two_factor_form.md @@ -0,0 +1,72 @@ +--- +title: User:Disable_Two_Factor_Form +description: Renders a form to disable 2FA on the user's account +intro: Allow users to turn off two-factor authentication. If their role requires 2FA, they'll be prompted to set it up again. +parameters: + - + name: redirect + type: string + description: Where the user should be taken after disabling 2FA. + - + name: allow_request_redirect + type: boolean + description: When set to true, the `redirect` parameter will get overridden by a `redirect` query parameter in the URL. + - + name: HTML Attributes + type: + description: > + Set HTML attributes as if you were in an HTML element. For example, `class="disable-form"`. +variables: + - + name: success + type: string + description: A success message. +id: 8f3d4a9b-0c2e-4f7a-3b6d-1e4f5a8b0c2d +--- +## Overview + +The `user:disable_two_factor_form` tag renders a form that allows authenticated users to disable two-factor authentication on their account. This removes the 2FA requirement and deletes their recovery codes. + +The tag will render the opening and closing `
` HTML elements for you. No input fields are required—just a submit button. + +:::tip +This form requires the user to be authenticated with 2FA enabled and an [elevated session](/tags/user-elevated_session_form). If the session isn't elevated, the user will be redirected to confirm their identity first. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ user:disable_two_factor_form redirect="/account" }} + + {{ if success }} +
+ {{ success }} +
+ {{ /if }} + +

Are you sure you want to disable two-factor authentication?

+ + +{{ /user:disable_two_factor_form }} +``` +::tab blade +```blade + + @if ($success) +
+ {{ $success }} +
+ @endif + +

Are you sure you want to disable two-factor authentication?

+ +
+``` +:: + +## Enforced 2FA + +If the user belongs to a role that has 2FA enforced (configured via `two_factor_enforced_roles` in your config), they can't really stay signed in with 2FA off — so after the form is submitted, Statamic ignores the `redirect` parameter and sends them to the setup page instead. That destination is pulled from the `statamic.users.two_factor_setup_url` config key in `config/statamic/users.php`, falling back to Statamic's built-in setup route if that's left `null`. diff --git a/content/collections/tags/user-login_form.md b/content/collections/tags/user-login_form.md index 5a152dba6..4f61a2106 100644 --- a/content/collections/tags/user-login_form.md +++ b/content/collections/tags/user-login_form.md @@ -151,3 +151,22 @@ For more information on managing passkeys on the frontend, see the following doc - [`{{ user:passkeys }}`](/tags/user-passkeys) - [`{{ user:passkey_form }}`](/tags/user-passkey_form) - [`{{ user:delete_passkey_form }}`](/tags/user-delete_passkey_form) + +## Two-Factor Authentication + +When a user with two-factor authentication (2FA) enabled submits the login form, Statamic will redirect them to a challenge page so they can enter a code from their authenticator app. If the user belongs to a role that requires 2FA but hasn't set it up yet, they'll be redirected to the setup page instead. + +You can customize where each of these redirects goes using the `two_factor_challenge_url` and `two_factor_setup_url` config keys in `config/statamic/users.php`. Leave them `null` to use Statamic's built-in pages. + +```php +// config/statamic/users.php + +'two_factor_challenge_url' => '/account/2fa/challenge', +'two_factor_setup_url' => '/account/2fa/setup', +``` + +When rolling your own frontend pages, use the following tags to render the forms: + +- [`{{ user:two_factor_challenge_form }}`](/tags/user-two_factor_challenge_form) — the code verification form during login +- [`{{ user:two_factor_enable_form }}`](/tags/user-two_factor_enable_form) — step 1 of setup, generates the secret +- [`{{ user:two_factor_setup_form }}`](/tags/user-two_factor_setup_form) — step 2 of setup, displays the QR code and confirms the code diff --git a/content/collections/tags/user-reset_two_factor_recovery_codes_form.md b/content/collections/tags/user-reset_two_factor_recovery_codes_form.md new file mode 100644 index 000000000..1f0fd0b74 --- /dev/null +++ b/content/collections/tags/user-reset_two_factor_recovery_codes_form.md @@ -0,0 +1,64 @@ +--- +title: User:Reset_Two_Factor_Recovery_Codes_Form +description: Renders a form to generate new recovery codes +intro: If a user has used some of their recovery codes or suspects they've been compromised, they can generate a fresh set. This invalidates all existing codes. +parameters: + - + name: redirect + type: string + description: Where the user should be taken after generating new codes. + - + name: HTML Attributes + type: + description: > + Set HTML attributes as if you were in an HTML element. For example, `class="reset-form"`. +variables: + - + name: success + type: string + description: A success message. +id: 7e2c3f8a-9b1d-4e6f-2a5c-0d3e4f7a9b1c +--- +## Overview + +The `user:reset_two_factor_recovery_codes_form` tag renders a form that allows authenticated users to generate a new set of recovery codes. When submitted, all existing recovery codes are invalidated and replaced with new ones. + +The tag will render the opening and closing `` HTML elements for you. No input fields are required—just a submit button. + +:::tip +This form requires the user to be authenticated with 2FA enabled and an [elevated session](/tags/user-elevated_session_form). If the session isn't elevated, the user will be redirected to confirm their identity first. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ user:reset_two_factor_recovery_codes_form redirect="/account/recovery-codes" }} + + {{ if success }} +
+ {{ success }} +
+ {{ /if }} + +

Generate new recovery codes? Your current codes will be invalidated.

+ + +{{ /user:reset_two_factor_recovery_codes_form }} +``` +::tab blade +```blade + + @if ($success) +
+ {{ $success }} +
+ @endif + +

Generate new recovery codes? Your current codes will be invalidated.

+ +
+``` +:: diff --git a/content/collections/tags/user-two_factor_challenge_form.md b/content/collections/tags/user-two_factor_challenge_form.md new file mode 100644 index 000000000..861680436 --- /dev/null +++ b/content/collections/tags/user-two_factor_challenge_form.md @@ -0,0 +1,98 @@ +--- +title: User:Two_Factor_Challenge_Form +description: Renders a form for users to enter their 2FA code during login +intro: When a user with two-factor authentication enabled logs in, they need to verify their identity with a code from their authenticator app. This tag renders that verification form. +parameters: + - + name: redirect + type: string + description: Where the user should be taken after successful verification. + - + name: error_redirect + type: string + description: Where the user should be redirected on validation errors. + - + name: allow_request_redirect + type: boolean + description: When set to true, the `redirect` and `error_redirect` parameters will get overridden by `redirect` and `error_redirect` query parameters in the URL. + - + name: HTML Attributes + type: + description: > + Set HTML attributes as if you were in an HTML element. For example, `class="required" id="2fa-form"`. +variables: + - + name: errors + type: array + description: An array of validation errors. + - + name: error + type: array + description: An array of validation errors indexed by field names. Suitable for targeting fields. eg. `{{ error:code }}` + - + name: success + type: string + description: A success message. +id: 3a8e9b4c-5d7f-4a2b-8c1e-6f9d0a3b5c7e +--- +## Overview + +The `user:two_factor_challenge_form` tag renders a form for users to complete the second step of two-factor authentication. After submitting valid credentials on the login form, users with 2FA enabled are redirected to this challenge form. + +The tag will render the opening and closing `` HTML elements for you. Users can verify their identity by entering either a `code` from their authenticator app or a `recovery_code`. + +:::tip +This form will only render content if there's a pending 2FA challenge in the session. If accessed without a pending challenge, the form contents won't be displayed. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ user:two_factor_challenge_form redirect="/dashboard" }} + + {{ if errors }} +
+ {{ errors }} + {{ value }}
+ {{ /errors }} +
+ {{ /if }} + +

Enter the 6-digit code from your authenticator app:

+ + +

Or use a recovery code:

+ + + + +{{ /user:two_factor_challenge_form }} +``` +::tab blade +```blade + + @if ($errors) +
+ @foreach ($errors as $error) + {{ $error }}
+ @endforeach +
+ @endif + +

Enter the 6-digit code from your authenticator app:

+ + +

Or use a recovery code:

+ + + +
+``` +:: + +## Redirect Behavior + +If you don't specify a `redirect` parameter, the form will use the `redirect` value from the original login form. This allows you to specify the redirect destination once on the login form and have it carry through the entire 2FA flow. diff --git a/content/collections/tags/user-two_factor_enable_form.md b/content/collections/tags/user-two_factor_enable_form.md new file mode 100644 index 000000000..2b2eec964 --- /dev/null +++ b/content/collections/tags/user-two_factor_enable_form.md @@ -0,0 +1,92 @@ +--- +title: User:Two_Factor_Enable_Form +description: Renders the first step of 2FA setup — generates the user's 2FA secret +intro: Step one of the two-factor authentication setup flow. Submitting this form generates a 2FA secret for the user and sends them on to the setup page where they confirm the code. +parameters: + - + name: redirect + type: string + description: Where the user should be taken after their 2FA secret has been generated. Typically this is the page that renders `{{ user:two_factor_setup_form }}`. + - + name: allow_request_redirect + type: boolean + description: When set to true, the `redirect` parameter will get overridden by a `redirect` query parameter in the URL. + - + name: HTML Attributes + type: + description: > + Set HTML attributes as if you were in an HTML element. For example, `class="required" id="2fa-enable-form"`. +variables: + - + name: errors + type: array + description: An array of validation errors. + - + name: error + type: array + description: An array of validation errors indexed by field names. + - + name: success + type: string + description: A success message. +id: 8f4c6868-0aee-4814-a498-a4e118c2f400 +--- +## Overview + +The `user:two_factor_enable_form` tag renders **step one** of the two-factor authentication setup flow. Submitting it generates the user's 2FA secret (and eventually their recovery codes, once they confirm), then redirects them on to the setup page where the QR code and confirmation form are displayed. + +The tag will render the opening and closing `` HTML elements for you. No input fields are required — just a submit button. + +:::tip +This form only renders for an authenticated user who does **not** already have 2FA enabled. It also requires an [elevated session](/tags/user-elevated_session_form) — if the session isn't elevated, the user will be redirected to confirm their identity first. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ user:two_factor_enable_form redirect="/account/2fa/setup" }} + + {{ if errors }} +
+ {{ errors }} + {{ value }}
+ {{ /errors }} +
+ {{ /if }} + +

Click below to start setting up two-factor authentication. You'll be taken to a page where you can scan a QR code with your authenticator app.

+ + + +{{ /user:two_factor_enable_form }} +``` +::tab blade +```blade + + @if ($errors) +
+ @foreach ($errors as $error) + {{ $error }}
+ @endforeach +
+ @endif + +

Click below to start setting up two-factor authentication. You'll be taken to a page where you can scan a QR code with your authenticator app.

+ + +
+``` +:: + +## Redirect behavior + +After the secret is generated, Statamic decides where to send the user in the following order: + +1. The form's `redirect` parameter (submitted as `_redirect`). +2. The `statamic.users.two_factor_setup_url` config key in `config/statamic/users.php`. +3. The referring page (i.e. back to where the form was rendered). + +Whichever destination is used, that page should render [`{{ user:two_factor_setup_form }}`](/tags/user-two_factor_setup_form) so the user can complete setup. diff --git a/content/collections/tags/user-two_factor_enabled.md b/content/collections/tags/user-two_factor_enabled.md new file mode 100644 index 000000000..26dfb31e6 --- /dev/null +++ b/content/collections/tags/user-two_factor_enabled.md @@ -0,0 +1,51 @@ +--- +title: User:Two_Factor_Enabled +description: Returns whether the current user has 2FA enabled +intro: A boolean tag that returns `true` when the authenticated user has two-factor authentication enabled. Useful for conditionally showing recovery code or disable UI. +id: fa2eaa81-6a76-48d1-8b37-a0abda40b09e +--- +## Overview + +The `user:two_factor_enabled` tag returns `true` when the currently authenticated user has confirmed their two-factor authentication setup, and `false` otherwise (including when nobody is logged in). + +### Example + +Because this is a tag rather than a variable, use the extra brace syntax inside conditionals: + +::tabs + +::tab antlers +```antlers +{{ if {user:two_factor_enabled} }} +

Your Recovery Codes

+ {{ user:two_factor_recovery_codes }} +
  • {{ code }}
  • + {{ /user:two_factor_recovery_codes }} + + {{ user:disable_two_factor_form }} + + {{ /user:disable_two_factor_form }} +{{ else }} + {{ user:two_factor_enable_form redirect="/account/2fa/setup" }} + + {{ /user:two_factor_enable_form }} +{{ /if }} +``` +::tab blade +```blade +@if (Statamic::tag('user:two_factor_enabled')->fetch()) +

    Your Recovery Codes

    + +
  • {{ $code }}
  • +
    + + + + +@else + + + +@endif +``` +:: diff --git a/content/collections/tags/user-two_factor_recovery_codes.md b/content/collections/tags/user-two_factor_recovery_codes.md new file mode 100644 index 000000000..a08d41adf --- /dev/null +++ b/content/collections/tags/user-two_factor_recovery_codes.md @@ -0,0 +1,59 @@ +--- +title: User:Two_Factor_Recovery_Codes +description: Displays the user's 2FA recovery codes +intro: Recovery codes provide a backup way for users to access their account if they lose their authenticator device. This tag displays those codes. +variables: + - + name: code + type: string + description: A single recovery code. +id: 5c0a1d6e-7f9b-4c4d-0e3a-8b1f2c5d7e9a +--- +## Overview + +The `user:two_factor_recovery_codes` tag loops through and displays the authenticated user's recovery codes. Each recovery code can only be used once to log in if the user doesn't have access to their authenticator app. + +:::tip +This tag requires the user to be authenticated with 2FA enabled. If 2FA is not enabled, the tag won't render any content. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ if {user:two_factor_enabled} }} +

    Your Recovery Codes

    +

    Store these codes in a safe place. Each code can only be used once.

    + + +{{ else }} +

    You don't have two-factor authentication enabled.

    +{{ /if }} +``` +::tab blade +```blade +@if (Statamic::tag('user:two_factor_enabled')->fetch()) +

    Your Recovery Codes

    +

    Store these codes in a safe place. Each code can only be used once.

    + + +@else +

    You don't have two-factor authentication enabled.

    +@endif +``` +:: + +:::tip +[`{{ user:two_factor_enabled }}`](/tags/user-two_factor_enabled) is a tag, not a variable, so conditionals need the extra braces: `{{ if {user:two_factor_enabled} }}`. +::: + diff --git a/content/collections/tags/user-two_factor_recovery_codes_download_url.md b/content/collections/tags/user-two_factor_recovery_codes_download_url.md new file mode 100644 index 000000000..c7d55b89c --- /dev/null +++ b/content/collections/tags/user-two_factor_recovery_codes_download_url.md @@ -0,0 +1,38 @@ +--- +title: User:Two_Factor_Recovery_Codes_Download_URL +description: Outputs a URL to download recovery codes as a text file +id: 6d1b2e7f-8a0c-4d5e-1f4b-9c2a3d6e8f0b +--- +## Overview + +The `user:two_factor_recovery_codes_download_url` tag outputs a URL that allows the authenticated user to download their recovery codes as a plain text file. This provides a convenient way for users to save their codes offline. + +:::tip +This tag requires the user to be authenticated with 2FA enabled. If 2FA is not enabled, the tag returns an empty string. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ if {user:two_factor_enabled} }} + + Download Recovery Codes + +{{ /if }} +``` +::tab blade +```blade +@if (Statamic::tag('user:two_factor_enabled')->fetch()) + + Download Recovery Codes + +@endif +``` +:: + +:::tip +[`{{ user:two_factor_enabled }}`](/tags/user-two_factor_enabled) is a tag, not a variable, so conditionals need the extra braces: `{{ if {user:two_factor_enabled} }}`. +::: diff --git a/content/collections/tags/user-two_factor_setup_form.md b/content/collections/tags/user-two_factor_setup_form.md new file mode 100644 index 000000000..a6d0d80ec --- /dev/null +++ b/content/collections/tags/user-two_factor_setup_form.md @@ -0,0 +1,149 @@ +--- +title: User:Two_Factor_Setup_Form +description: Renders the second step of 2FA setup — the QR code and confirmation form +intro: Step two of the two-factor authentication setup flow. This tag displays the QR code and verifies the code the user enters to confirm their authenticator app is working. +parameters: + - + name: redirect + type: string + description: Where the user should be taken after successful setup (e.g., a recovery codes page). + - + name: error_redirect + type: string + description: Where the user should be redirected on validation errors. + - + name: allow_request_redirect + type: boolean + description: When set to true, the `redirect` and `error_redirect` parameters will get overridden by `redirect` and `error_redirect` query parameters in the URL. + - + name: HTML Attributes + type: + description: > + Set HTML attributes as if you were in an HTML element. For example, `class="required" id="2fa-setup-form"`. +variables: + - + name: qr_code + type: string + description: SVG markup for the QR code that can be rendered directly in the template. + - + name: qr_code_url + type: string + description: A data URL for the QR code, suitable for use with an `` tag's `src` attribute. + - + name: secret_key + type: string + description: The TOTP secret key for users who prefer to enter it manually into their authenticator app. + - + name: errors + type: array + description: An array of validation errors. + - + name: error + type: array + description: An array of validation errors indexed by field names. Suitable for targeting fields. eg. `{{ error:code }}` + - + name: success + type: string + description: A success message. +id: 4b9f0c5d-6e8a-4b3c-9d2f-7a0e1b4c6d8f +--- +## Overview + +The `user:two_factor_setup_form` tag renders **step two** of the two-factor authentication setup flow. At this point the user has already generated a 2FA secret (via [`{{ user:two_factor_enable_form }}`](/tags/user-two_factor_enable_form)) and just needs to scan the QR code with their authenticator app and enter a verification code to confirm the setup. + +The tag will render the opening and closing `` HTML elements for you. You'll need to provide a `code` input field for the verification code. + +:::tip +This form only renders for an authenticated user who has a 2FA secret but hasn't yet confirmed it. If the user doesn't have a secret yet, send them through [`{{ user:two_factor_enable_form }}`](/tags/user-two_factor_enable_form) first. If the user already has 2FA enabled, the form contents won't be rendered. +::: + +### Example + +::tabs + +::tab antlers +```antlers +{{ user:two_factor_setup_form redirect="/account/recovery-codes" }} + + {{ if errors }} +
    + {{ errors }} + {{ value }}
    + {{ /errors }} +
    + {{ /if }} + +

    Set Up Two-Factor Authentication

    + +

    Scan this QR code with your authenticator app:

    + + {{# Option 1: Render SVG directly #}} +
    + {{ qr_code }} +
    + + {{# Option 2: Use as image source #}} + QR Code + +

    Or enter this code manually: {{ secret_key }}

    + + + + + +{{ /user:two_factor_setup_form }} +``` +::tab blade +```blade + + @if ($errors) +
    + @foreach ($errors as $error) + {{ $error }}
    + @endforeach +
    + @endif + +

    Set Up Two-Factor Authentication

    + +

    Scan this QR code with your authenticator app:

    + + {{-- Option 1: Render SVG directly --}} +
    + {!! $qr_code !!} +
    + + {{-- Option 2: Use as image source --}} + QR Code + +

    Or enter this code manually: {{ $secret_key }}

    + + + + +
    +``` +:: + + + +## Displaying the QR code + +You have two options for displaying the QR code: + +1. **SVG Markup** (`qr_code`): Renders directly in the HTML. This is generally preferred as it scales well and doesn't require an additional request. + +2. **Data URL** (`qr_code_url`): Use with an `` tag if you prefer image-based rendering or need more control over sizing. + +## Flow + +The frontend 2FA setup flow is split across two pages: + +1. Submit [`{{ user:two_factor_enable_form }}`](/tags/user-two_factor_enable_form) — this generates the user's 2FA secret and, by default, redirects to `statamic.users.two_factor_setup_url` (or wherever you specify via `_redirect`). +2. On that setup page, use `{{ user:two_factor_setup_form }}` (this tag) to display the QR code and confirm the code.