Skip to content

All auth changes - #7848

Merged
distantnative merged 111 commits into
v6/developfrom
v6/refact/auth
Jun 30, 2026
Merged

All auth changes#7848
distantnative merged 111 commits into
v6/developfrom
v6/refact/auth

Conversation

@distantnative

@distantnative distantnative commented Dec 31, 2025

Copy link
Copy Markdown
Member

Description

This PR replaces the monolithic Kirby\Cms\Auth class with a restructured new Kirby\Auth namespace. The old class has grown into a 30ish-method mighty bag that handles rate limiting, CSRF, session/user resolution, basic-auth gating, password validation, challenge creation/verification, status assembly and impersonation all in one file. In the new namespace, these different responsibilities have been extracted into various classes.

On top of the refactor, it introduces an extension for auth methods that mirrors our extension for custom auth challenges. It rebuilds the Panel login view (incl. more backend-driven), has a new Panel security drawer to manage all security-related settings for a user and adds the ability to switch between challenges mid-login.

New Kirby\Auth namespace

Kirby\Cms\App::auth() ──► Kirby\Auth\Auth   (facade / orchestrator)
                              │
        ┌──────────┬─────────┼───────────┬────────────┐
        ▼          ▼         ▼           ▼            ▼
     Methods    Challenges   User       Csrf        Limits
   (registry)  (registry +  (current               (rate limit +
        │       state mach.) user +                  .logins log)
        │           │        imperson.)
        ▼           ▼
   Method                    Challenge
    ├ PasswordMethod  ──┐    ├ EmailChallenge ─┐
    ├ BasicAuthMethod   │    └ TotpChallenge ──┤
    ├ CodeMethod        ├─► Auth::createChallenge() ─► produce/consume ─► Pending
    └ PasswordResetMethod   (mode: 2fa /                (public + #[Sensitive] secret,
       (extends CodeMethod)  login / password-reset)     stored in the session)

Class reference

Group Class Responsibility
Core Auth Facade. Owns the 5 collaborators, caches a Status, keeps the public API (authenticate(), verifyChallenge(), validatePassword(), user(), status(), impersonate(), logout(), …).
Methods Methods Registry + handler (enabled(), get(), has(), firstEnabled(), hasAnyUsingChallenges()). Static $methods map filled by plugins.
Method (abstract) A login strategy. authenticate() returns a User (done) or a pending Status (challenge required).
PasswordMethod Email + password, 2FA if configured (returns a challenge Status when '2fa' => true).
BasicAuthMethod HTTP Basic Auth, re-validated every request; isEnabled() gating (HTTPS, password method on, no 2FA).
CodeMethod Passwordless login via an emailed one-time code / challenge.
PasswordResetMethod Extends CodeMethod; starts the password-reset challenge (always short-lived session).
Challenges Challenges Registry + the session state machine: create(), verify(), switch(), firstAvailable(), ensureNotTimeout(), ensureActiveChallenge().
Challenge (abstract) Now instance-based. `create(): Pending
EmailChallenge / TotpChallenge Email 6-digit code / authenticator-app TOTP.
Value/State State (enum) Active / Impersonated / Pending / Inactive (replaces string status).
Status
Pending In-flight challenge data, split into public (→ frontend) and #[SensitiveParameter] secret (→ server).
User Current-user resolution, caching, impersonation.
Support Csrf CSRF token handling
Limits Rate limiting
Exceptions Exception\RateLimitException, ChallengeTimeoutException, LoginNotPermittedException All extend PermissionException; replace the old details['reason'] string markers.
Kirby\Exception\UserNotFoundException Extends NotFoundException; replaces inline NotFoundException(key: 'user.notFound').

Login flow in one breath:

Auth::authenticate($method, …)Methods::get($method)->authenticate(). The method either logs the user in (returns User) or returns a pending Status after asking Auth::createChallenge()Challenges::create() to pick the first available challenge and store a Pending in the session. A later request calls Auth::verifyChallenge($input)Challenges::verify(), which checks timeout/active-challenge, verifies the code, then logs the user in passwordless (or flags resetPassword for the reset flow).

Changelog

🎉 Features

  • Auth methods extension: Plugins can ship their own login method, just like custom challenges.
  • Switch between challenges mid-login: When a challenge is pending, users can switch the active challenge (e.g. authenticator-app TOTP vs. emailed code) without restarting; switching re-applies rate limiting and rebuilds session state.
  • User security drawer in the Panel: A single place on the user/account view to handle security-related matters, e.g. email, password, two-factor (TOTP) management.
    image
  • Custom forms for auth methods and challenges: Each method/challenge can declare its own Panel form via ::form(), which is what makes the login UI fully extensible end-to-end.

✨ Enhancements

  • New Panel login UI/UX: The login view has been refactored to be more driven by the backend. It now features a method picker, a challenge switcher, and centralized loading/error handling.
  • Auth methods can authenticate without an email, enabling token/SSO-style plugin methods that don't key on email.
  • TOTP management in one drawer: Enable/disable TOTP in a single drawer with QR code, a clickable otpauth:// setup-key link, and password confirmation.
    Screenshot 2026-02-03 at 16 05 45
  • New icons:hashtag, email-unread

🐛 Bug fixes

  • Login-failed hook is no longer fired twice.

♻️ Refactored

  • Kirby\Cms\Auth class split into new Kirby\Auth namespace:
    • New classes: Kirby\Auth\Limits, Kirby\Auth\Csrf, Kirby\Auth\Methods and individual Kirby\Auth\Method classes, Kirby\Auth\User, Kirby\Auth\Challenges and individual Kirby\Auth\Challenge classes, Kirby\Auth\Pending, Kirby\Auth\Status class and Kirby\Auth\State enum
    • New exceptions: Kirby\Auth\Exception\RateLimitException, Kirby\Auth\Exception\LoginNotPermittedException, Kirby\Exception\UserNotFoundException and Kirby\Auth\Exception\ChallengeTimeoutException
  • Auth challenges have been changed from static classes to instance-based objects
  • Panel login decomposed from two fixed forms into small composable components and a thin LoginView.vue shell driven by the backend.
  • New Kirby\Cms\User::changeSecret() / Kirby\Cms\UserRules::changeSecret() generalise the old TOTP-specific secret writing.

☠️ Deprecated

  • Kirby\Cms\Auth → use Kirby\Auth\Auth (kept working via alias).
  • Kirby\Cms\Auth\Status → use Kirby\Auth\Status (old class kept as a stub).
  • Auth::login2fa() → use Auth::authenticate().
  • Auth::enabledChallenges()challenges()->enabled().
  • Auth::isBlocked() / log() / logfile() / track() → the equivalents on ::limits(), e.g. $auth->limits()->isBlocked().
  • Cms\System::loginMethods()$kirby->auth()->methods()->enabled()
  • UserActions::changeTotp() / UserRules::changeTotp()changeSecret('totp', …).

🚨 Breaking changes

  • Everywhere where Kirby\Cms\Auth\Status got returned or expected as parameter, Kirby\Auth\Status is now used.

  • Removed .

  • Auth challenge classes have been redesigned. Existing custom auth challenges must be rewritten and have to adopt the new Kirby\Auth\Challenge base class.

  • Auth session format changed: kirby.challenge.codekirby.challenge.data. In-flight challenges across the Kirby upgrade won't verify.

  • Kirby\Auth\Status: $auth->status() returns Kirby\Auth\Status, is(State), clone() removed.

  • ::validatePassword() returns User|null

  • ::verifyChallenge(mixed $input) returns User|null

  • User::changeTotp() renamed to User::changeSecret('totp', ...)

  • Panel: TOTP enable/disable dialogs removed (UserTotpEnableDialogController, UserTotpDisableDialogController and <k-totp-dialog>). Use UserTotpDrawerController and <k-user-totp-drawer> instead.

  • Panel login route shape changed: multi-step login/method/... and login/challenge/...

  • Panel components removed/renamed: k-login-form, k-login-code-form and the deprecated aliases k-login / k-login-code removed.

  • panel.plugins.login extension point removed. Plugins that overrode the whole login form via this hook break and should provide a custom auth method (incl. ::form()) instead.

  • Kirby\Cms\Find::user() throws UserNotFoundException

Housekeeping

  • Drastically improved speed of PHP Unit tests

Docs

Needs doc updates for:

  • The Kirby\Auth namespace** + class reference / extension points (authMethods, authChallenges, Method::form(), Challenge::form()).
  • New auth methods extension
  • Changed auth challenge extension
  • Migration guide for custom auth challenges
public class FooMethod extends \Kirby\Auth\Method
{
	public function authenticate(
		string $email,
		#[SensitiveParameter]
		string|null $password = null,
		bool $long = false
	): User|Status {
		// returns either the logged in user
		// or creates a challenge and returns the 
		// pending auth status
	}

	public static function isAvailable(
		Auth $auth,
		array $options = []
	): bool {
		// some condition if the auth method
		// should be available, by default true
	}

	public static function isUsingChallenges(
		Auth $auth,
		array $options = []
	): bool {
		// if a challenge is created by the method,
		// default false
	}
}

// register custom auth method
App::plugin(
	name: 'my/plugin',
	extensions: ['authMethods' => ['foo' => FooMethod::class]]
);
class FooChallenge extends \Kirby\Auth\Challenge
{
	public function create(): Pending|null
	{
		// create the challenge and return a Pending object
		// that can store secret as well as public (for the frontend)
		// data to later be used in ::verify().
		// If no data is needed, return null.

		return new Pending(
			secret: 'my-secret-hash',
			public: ['foo' => 'bar']
		);
	}

	public static function isAvailable(User $user, string $mode): bool
	{
		// check if the challenge is available for the user,
		// e.g. the user has set it up
	}

	public function verify(
		#[SensitiveParameter]
		mixed $input,
		Pending $data
	): bool {
		// receives the input from the frontend as well as
		// the Pending data from ::create() and should
		// evaluate the challenge
	}
}

// register the custom challenge
App::plugin(
	name: 'my/plugin',
	extensions: ['authChallenges' => ['foo' => FooChallenge::class]]
);

For review team

  • Add changes & docs to release notes draft in Notion

@distantnative
distantnative marked this pull request as ready for review June 20, 2026 17:18
@distantnative
distantnative requested a review from a team June 20, 2026 17:18
@distantnative distantnative added this to the 6.0.0-alpha.3 milestone Jun 20, 2026
Comment thread src/Auth/Auth.php Outdated
Comment thread src/Auth/Auth.php Outdated
Comment thread src/Auth/Method/PasswordResetMethod.php
Comment thread src/Panel/Controller/View/LoginViewController.php Outdated
@bastianallgeier
bastianallgeier self-requested a review June 30, 2026 14:51
@distantnative
distantnative merged commit 4811dfa into v6/develop Jun 30, 2026
11 of 12 checks passed
@distantnative
distantnative deleted the v6/refact/auth branch June 30, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants