From 32298bfa4e497cefc25763b8f5b4fd98cfff0d92 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 05:52:59 -0400 Subject: [PATCH] Scope the two-factor middleware alias and gate Pint in CI Middleware aliases live in one flat map, so a bare two-factor collides with any app or package alias of that name. The middleware is now laranail-authkit-two-factor; the bare alias still enforces the same checks and warns once per process. A Code style workflow runs the shared Pint config on every pull request. The captcha, console, db-tools and enumerator vcs entries are outside the require closure and are removed; the author email moves to opensource@simtabi.com; repeated Unreleased subsections are merged with every bullet kept. --- .github/workflows/code-style.yml | 63 +++++++++++++++++++ CHANGELOG.md | 32 +++++++--- composer.json | 18 +----- docs/two-factor-authentication.md | 11 +++- .../Middleware/DeprecatedTwoFactorAlias.php | 51 +++++++++++++++ src/Providers/AuthKitServiceProvider.php | 16 ++++- tests/Feature/NamingConventionTest.php | 50 +++++++++++++++ 7 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/code-style.yml create mode 100644 src/Http/Middleware/DeprecatedTwoFactorAlias.php diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml new file mode 100644 index 0000000..596664c --- /dev/null +++ b/.github/workflows/code-style.yml @@ -0,0 +1,63 @@ +name: Code style + +on: + pull_request: + branches: [main] + # No path filter: this workflow gates merges as a required status check, and a + # required check that never reports (a docs-only pull request) blocks it for good. + workflow_dispatch: + +concurrency: + group: code-style-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +# The laranail siblings resolve through git rather than Packagist, so the install +# hits the GitHub API to enumerate their refs. The token is the built-in one; no +# secret to configure. +env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GITHUB_TOKEN }}"}}' + +jobs: + pint: + name: Pint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v7 + + - name: Set up PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.5' + coverage: none + + - name: Resolve composer cache directory + id: composer-cache + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Restore composer cache + uses: actions/cache@v6 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ runner.os }}-8.5-${{ hashFiles('**/composer.json') }} + restore-keys: composer-${{ runner.os }}-8.5- + + - name: Drop cached laranail archives + # laranail/* resolve through a single MOVING v0.1.0 tag, so composer's dist + # cache is keyed on a name whose contents change underneath it. A restored + # archive is then silently stale. + shell: bash + run: rm -rf "$(composer config cache-files-dir)/laranail" + + - name: Install dependencies + run: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Check formatting + # The repository's own lint script: vendor/bin/laranail-pint --test, which runs + # Pint against the shared laranail/package-tools config and never rewrites files. + run: composer pint diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b68f8..c086ec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,28 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- The two-factor middleware is registered under the vendor-scoped alias `laranail-authkit-two-factor` + (`AuthKitServiceProvider::TWO_FACTOR_MIDDLEWARE`). `NamingConventionTest` asserts it, and that no + other bare alias is registered, against the live router. + - **Opt-in TOTP two-factor authentication for API clients.** Login can return a short-lived MFA challenge; clients can verify TOTP or recovery codes, manage enrollment and recovery codes, and protect routes with the `two-factor` middleware. Verified API tokens receive a `two-factor:verified` ability. The `two_factor_method` field defaults to `none` and is ready for future methods. +- A `NamingConventionTest` that asserts the public names against the **live registries** on a booted + application, rather than the provider source, so the guard survives a refactor. + ### Changed +- Pull requests run `composer pint` (Pint with the shared laranail config, check-only) in a new + *Code style* workflow. + +- The `vcs` repository entries for `laranail/captcha`, `console`, `db-tools` and `enumerator` are gone. Nothing in this package's + `require` or `require-dev` closure pulls them in (`composer why` finds none of them installed), + so they only told Composer to clone repositories it never used. The Packagist exclusion for + `laranail/*` stays. + +- `composer.json` `authors` email is `opensource@simtabi.com`, the community metadata address, + replacing `hello@simtabi.com`. + - **Breaking. Social login moved to `laranail/authkit-social-login`.** Fifteen classes, the `socials` migration, its factory and the `social` config block left this package. See [UPGRADING.md](UPGRADING.md); `rector-migrate-social.php` codemods the class renames. @@ -60,19 +78,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `dev-main`. A `dev-` constraint in `require` propagates dev stability to every consumer, and the org convention states no laranail package carries one. +### Deprecated -### Added +- **The bare `two-factor` middleware alias.** Middleware aliases share one flat map, so a bare name + collides with any application or package alias of the same name. It still enforces the same + checks, through `DeprecatedTwoFactorAlias`, and logs one warning per process naming + `laranail-authkit-two-factor`. The earliest release that could remove it is the next minor after 0.1. -- A `NamingConventionTest` that asserts the public names against the **live registries** on a booted - application, rather than the provider source, so the guard survives a refactor. +### Removed +- `composer.lock` is no longer tracked. A library's lock records a resolution consumers never use. ### Fixed - The user-model exception named the old package. -### Removed - -- `composer.lock` is no longer tracked. A library's lock records a resolution consumers never use. - [Unreleased]: https://github.com/laranail/authkit/compare/v0.1.0...HEAD diff --git a/composer.json b/composer.json index f93ab7e..992fc5c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "authors": [ { "name": "Simtabi", - "email": "hello@simtabi.com" + "email": "opensource@simtabi.com" } ], "require": { @@ -52,26 +52,10 @@ } }, "repositories": [ - { - "type": "vcs", - "url": "https://github.com/laranail/console.git" - }, - { - "type": "vcs", - "url": "https://github.com/laranail/enumerator.git" - }, { "type": "vcs", "url": "https://github.com/laranail/package-tools.git" }, - { - "type": "vcs", - "url": "https://github.com/laranail/captcha.git" - }, - { - "type": "vcs", - "url": "https://github.com/laranail/db-tools.git" - }, { "type": "composer", "url": "https://repo.packagist.org", diff --git a/docs/two-factor-authentication.md b/docs/two-factor-authentication.md index 5883dcf..df6a83c 100644 --- a/docs/two-factor-authentication.md +++ b/docs/two-factor-authentication.md @@ -19,20 +19,25 @@ The migration adds `two_factor_method` (`none` or `totp`, default `none`), the e The preset provides account setup at `/auth/user/two-factor` and gates password sign-in with an authenticator or recovery-code challenge for accounts whose method is `totp`. Setup secrets remain inactive until a valid code confirms enrollment. Recovery codes are shown once and each can be used once. -Apply the `two-factor` middleware to routes that must require an enabled factor and successful verification in the current browser session: +Apply the `laranail-authkit-two-factor` middleware to routes that must require an enabled factor and successful verification in the current browser session: ```php -Route::middleware(['auth', 'two-factor'])->group(function () { +Route::middleware(['auth', 'laranail-authkit-two-factor'])->group(function () { // Sensitive routes }); ``` +> The bare `two-factor` alias is a deprecated alias of `laranail-authkit-two-factor`. It enforces +> the same checks and logs one warning per process naming the replacement. Middleware aliases live +> in one flat map, so a bare name collides with any other package or application alias of the same +> name. The earliest release that could remove it is the next minor after 0.1. + ## API flow When the core setting is enabled and the account method is `totp`, `POST /api/auth/login` returns HTTP 202 with `status: mfa_required`, a short-lived `challenge_token`, and `expires_in`. Submit that token with the user's TOTP or recovery code to `POST /api/auth/two-factor/challenge`; only a successful challenge returns a Sanctum bearer token. Failed attempts are throttled, and challenges expire after five minutes by default. Authenticated clients can inspect `GET /api/auth/user/two-factor`, start enrollment with `POST /api/auth/user/two-factor` (send the current `password`), confirm with `POST /api/auth/user/two-factor/confirm`, disable with `POST /api/auth/user/two-factor/disable`, and replace recovery codes with `POST /api/auth/user/two-factor/recovery-codes`. Enrollment confirmation returns the recovery codes once; disable and recovery-code replacement require a current TOTP or unused recovery code. -API tokens issued after the challenge carry the `two-factor:verified` ability. The `two-factor` middleware checks this exact ability (not wildcard ability matching) and confirms that TOTP remains enabled. Accounts with method `none` retain the existing password-only login response. +API tokens issued after the challenge carry the `two-factor:verified` ability. The `laranail-authkit-two-factor` middleware checks this exact ability (not wildcard ability matching) and confirms that TOTP remains enabled. Accounts with method `none` retain the existing password-only login response. TOTP verification delegates to Fortify's provider, which applies its configured verification window and replay cache. Keep the application encryption key secure and stable because it protects stored TOTP and recovery secrets. diff --git a/src/Http/Middleware/DeprecatedTwoFactorAlias.php b/src/Http/Middleware/DeprecatedTwoFactorAlias.php new file mode 100644 index 0000000..74121f8 --- /dev/null +++ b/src/Http/Middleware/DeprecatedTwoFactorAlias.php @@ -0,0 +1,51 @@ +middleware->handle($request, $next); + } +} diff --git a/src/Providers/AuthKitServiceProvider.php b/src/Providers/AuthKitServiceProvider.php index 64686c1..9b827ed 100644 --- a/src/Providers/AuthKitServiceProvider.php +++ b/src/Providers/AuthKitServiceProvider.php @@ -15,6 +15,9 @@ class AuthKitServiceProvider extends PackageServiceProvider { + /** The vendor-scoped middleware alias that requires verified two-factor authentication. */ + public const string TWO_FACTOR_MIDDLEWARE = 'laranail-authkit-two-factor'; + public function configurePackage(Package $package): void { $package @@ -68,11 +71,20 @@ public function packageBooted(): void // laranail.authkit.api.enabled to false. $this->loadRoutesFrom($this->packagePath('routes/api.php')); - $this->app->make('router')->aliasMiddleware( - 'two-factor', + $router = $this->app->make('router'); + + $router->aliasMiddleware( + self::TWO_FACTOR_MIDDLEWARE, \Simtabi\Laranail\AuthKit\Http\Middleware\RequireTwoFactorAuthentication::class, ); + // Deprecated bare alias, kept so routes written against it keep enforcing two-factor. + // It logs once per process naming the scoped alias above. + $router->aliasMiddleware( + \Simtabi\Laranail\AuthKit\Http\Middleware\DeprecatedTwoFactorAlias::ALIAS, + \Simtabi\Laranail\AuthKit\Http\Middleware\DeprecatedTwoFactorAlias::class, + ); + $this->configureFortify(); } diff --git a/tests/Feature/NamingConventionTest.php b/tests/Feature/NamingConventionTest.php index 3a62303..44747a3 100644 --- a/tests/Feature/NamingConventionTest.php +++ b/tests/Feature/NamingConventionTest.php @@ -2,7 +2,12 @@ declare(strict_types=1); +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; +use Simtabi\Laranail\AuthKit\Providers\AuthKitServiceProvider; +use Simtabi\Laranail\AuthKit\Http\Middleware\DeprecatedTwoFactorAlias; +use Simtabi\Laranail\AuthKit\Http\Middleware\RequireTwoFactorAuthentication; /** * The core registers no views, translations or routes — but it does own publish tags and a @@ -31,3 +36,48 @@ expect(config($bare))->toBeNull(); } }); + +it('registers the two-factor middleware under the vendor-scoped alias', function (): void { + $aliases = app('router')->getMiddleware(); + + expect($aliases)->toHaveKey(AuthKitServiceProvider::TWO_FACTOR_MIDDLEWARE) + ->and($aliases[AuthKitServiceProvider::TWO_FACTOR_MIDDLEWARE])->toBe(RequireTwoFactorAuthentication::class) + ->and(AuthKitServiceProvider::TWO_FACTOR_MIDDLEWARE)->toBe('laranail-authkit-two-factor'); +}); + +it('registers no bare middleware alias other than the deprecated two-factor one', function (): void { + $ours = array_filter( + app('router')->getMiddleware(), + fn (string $class): bool => str_starts_with($class, 'Simtabi\\Laranail\\AuthKit\\'), + ); + + // Non-vacuity: the scan must see this package's aliases at all. + expect($ours)->not->toBeEmpty(); + + $bare = array_filter( + array_keys($ours), + fn (string $alias): bool => ! str_starts_with($alias, 'laranail-authkit') && $alias !== DeprecatedTwoFactorAlias::ALIAS, + ); + + expect(array_values($bare))->toBe([]); +}); + +it('keeps the deprecated bare two-factor alias enforcing, and warns once', function (): void { + expect(app('router')->getMiddleware()[DeprecatedTwoFactorAlias::ALIAS] ?? null) + ->toBe(DeprecatedTwoFactorAlias::class); + + DeprecatedTwoFactorAlias::resetWarning(); + Log::spy(); + + Route::middleware(DeprecatedTwoFactorAlias::ALIAS)->get('/__two-factor-bare', fn () => 'ok'); + Route::middleware(AuthKitServiceProvider::TWO_FACTOR_MIDDLEWARE)->get('/__two-factor-scoped', fn () => 'ok'); + + // A guest is refused through both spellings: the bare alias is not a bypass. + $this->get('/__two-factor-bare')->assertStatus(401); + $this->get('/__two-factor-bare')->assertStatus(401); + $this->get('/__two-factor-scoped')->assertStatus(401); + + Log::shouldHaveReceived('warning') + ->once() + ->withArgs(fn (string $message): bool => str_contains($message, 'laranail-authkit-two-factor')); +});