Skip to content

Comments

[6.x] Two-Factor Authentication#11664

Merged
jasonvarga merged 139 commits intomasterfrom
two-factor-auth
May 1, 2025
Merged

[6.x] Two-Factor Authentication#11664
jasonvarga merged 139 commits intomasterfrom
two-factor-auth

Conversation

@duncanmcclean
Copy link
Member

@duncanmcclean duncanmcclean commented Apr 4, 2025

This pull request implements Two-Factor Authentication. Heavily inspired by the "Two Factor for Statamic" addon by Mity Digital (thanks @martyf and @mscruse!).

Usage

You can enable two-factor authentication from your user profile:

CleanShot.2025-04-24.at.14.36.33.mp4

Then, whenever you login to the Control Panel, you'll be asked to provide either a one-time code from your authenticator app (eg Google Authenticator, 1Password) or a recovery code:

CleanShot.2025-04-24.at.14.37.53.mp4

When you provide a recovery code, the used code will be replaced and an email will be sent allowing you to save the updated set of recovery codes.

Two Factor Authentication is opt-in by default. However, if necessary, you can force users to enable 2FA based on their roles:

// config/statamic/users.php

'two_factor' => [  
    'enforced_roles' => [
	    // Enforce for everyone
	    '*',

		// Enforce for super users
		'super_users',

		// Enforce for a specific role
		'marketing_managers',
		'user_admin',
    ],  
],

Users who haven't already enabled 2FA will be prompted to do so when they next login:

CleanShot 2025-04-24 at 14 39 11

^ Keeping the UI ugly but functional for now. We'll worry about the design after the ui branch has been merged into master.

Technical Approach

Profile

The "Two Factor" section on the user profile page is handled by a custom fieldtype.

It's hidden from the blueprint field selector, and it doesn't actually save anything. It's just a vehicle for showing what we need to show in the user publish form.

Enabling, disabling, and viewing recovery codes all result in AJAX requests being sent off to the backend.

Users with the edit users permission can disable two-factor authentication for other users.

Two Factor challenge

In order for users to be redirected to the challenge page when they're logged in, we needed to make some changes to the login process.

So, instead of us authenticating users right away, like we used to, we're now "validating" their credentials first.

If they're valid, and the user has 2FA enabled, we set a login.id key in the session identifying the user we want to login, and redirect the user along to the challenge page.

When the user completes the two-factor challenge, we validate the provided one-time code / recovery code, and attempt to login the user using the login.id key stored in the session.

This is an approach I've borrowed from Laravel Fortify, which they also seem to be adopting in the new Laravel starter kits.

Frontend

This pull request implements Two Factor Authentication for the Control Panel. We'll tackle 2FA and frontend forms in a separate PR.

However, users logging in via the {{ user:login_form }} tag will still be taken to the two-factor challenge / setup pages, if necessary.

The frontend flow uses the same code as the Control Panel flow, just with different routes/middlewares.

Storage

When you enable two-factor authentication, three keys will be saved in your user data:

  • two_factor_secret
  • two_factor_recovery_codes
  • two_factor_confirmed_at

The two_factor_secret and two_factor_recovery_codes values are encrypted using your applications's encryption key (APP_KEY).

Warning

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).

If you're storing users in the database, a migration should be published during the upgrade process to add the columns to your users table:

<?php  
  
use Illuminate\Database\Migrations\Migration;  
use Illuminate\Database\Schema\Blueprint;  
use Illuminate\Support\Facades\Schema;  
  
return new class extends Migration  
{  
    /**  
     * Run the migrations.     
     */    
    public function up(): void  
    {  
        Schema::table('users', function (Blueprint $table) {  
            $table->text('two_factor_secret')->nullable();  
            $table->text('two_factor_recovery_codes')->nullable();  
            $table->timestamp('two_factor_confirmed_at')->nullable();  
        });  
    }  
  
    /**  
     * Reverse the migrations.     
     */    
    public function down(): void  
    {  
        Schema::table('users', function (Blueprint $table) {  
            $table->dropColumn(['two_factor_secret', 'two_factor_recovery_codes', 'two_factor_confirmed_at']);  
        });  
    }
};

Statamic will also attempt to add a cast for the two_factor_confirmed_at column to your User model:

protected function casts(): array  
{  
    return [  
        'email_verified_at' => 'datetime',  
        'preferences' => 'json',  
++        'two_factor_confirmed_at' => 'datetime',  
    ];  
}

Requires #11688.
Related: statamic/ideas#1047

Docs: statamic/docs#1671

This prevents the log out route working if you're logged in but dont have permission to access the cp.

This is probably worthwhile to resolve but out of scope for this PR.
…to disable other users on the existing routes
…or yourself. Use the action to disable for others.
@jasonvarga jasonvarga merged commit 5d88a5a into master May 1, 2025
18 checks passed
@jasonvarga jasonvarga deleted the two-factor-auth branch May 1, 2025 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants