Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Http/Controllers/Auth/Concerns/RedirectsToIntendedUrl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Code16\Sharp\Http\Controllers\Auth\Concerns;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

trait RedirectsToIntendedUrl
{
/**
* Inertia follows redirects in XHR: when the intended URL is not a Sharp page
* (e.g. an app route using the sharp_auth middleware), force a full page visit.
*/
protected function redirectToIntendedUrl(?string $url = null): SymfonyResponse
{
$redirect = $url
? redirect()->to($url)
: redirect()->intended(route('code16.sharp.home'));

if (request()->inertia() && ! $this->isSharpUrl($redirect->getTargetUrl())) {
return Inertia::location($redirect->getTargetUrl());
}

return $redirect;
}

private function isSharpUrl(string $url): bool
{
try {
return Route::getRoutes()->match(Request::create($url))->named('code16.sharp.*');
} catch (NotFoundHttpException|MethodNotAllowedHttpException) {
return false;
}
}
}
8 changes: 6 additions & 2 deletions src/Http/Controllers/Auth/ImpersonateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
namespace Code16\Sharp\Http\Controllers\Auth;

use Code16\Sharp\Auth\Impersonate\SharpImpersonationHandler;
use Code16\Sharp\Http\Controllers\Auth\Concerns\RedirectsToIntendedUrl;
use Code16\Sharp\Http\Controllers\Auth\Requests\ImpersonateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Controller;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class ImpersonateController extends Controller
{
use RedirectsToIntendedUrl;

public function create(?SharpImpersonationHandler $impersonationHandler): RedirectResponse|Response
{
return Inertia::render('Auth/Impersonate', [
Expand All @@ -20,13 +24,13 @@ public function create(?SharpImpersonationHandler $impersonationHandler): Redire
]);
}

public function store(ImpersonateRequest $request): RedirectResponse
public function store(ImpersonateRequest $request): SymfonyResponse
{
auth(sharp()->config()->get('auth.guard'))
->loginUsingId($request->input('user_id'));

$request->session()->regenerate();

return redirect()->intended(route('code16.sharp.home'));
return $this->redirectToIntendedUrl();
}
}
8 changes: 6 additions & 2 deletions src/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Code16\Sharp\Auth\Passkeys\PasskeyManager;
use Code16\Sharp\Exceptions\Auth\SharpAuthenticationNeeds2faException;
use Code16\Sharp\Http\Controllers\Auth\Concerns\RedirectsToIntendedUrl;
use Code16\Sharp\Http\Controllers\Auth\Requests\LoginRequest;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
Expand All @@ -13,9 +14,12 @@
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class LoginController extends Controller
{
use RedirectsToIntendedUrl;

public function create(PasskeyManager $passkeys): RedirectResponse|Response
{
if ($loginPageUrl = sharp()->config()->get('auth.login_page_url')) {
Expand All @@ -41,7 +45,7 @@ public function create(PasskeyManager $passkeys): RedirectResponse|Response
]);
}

public function store(LoginRequest $request, PasskeyManager $passkeys): RedirectResponse
public function store(LoginRequest $request, PasskeyManager $passkeys): SymfonyResponse
{
try {
$request->authenticate();
Expand All @@ -61,7 +65,7 @@ public function store(LoginRequest $request, PasskeyManager $passkeys): Redirect
return redirect()->route('code16.sharp.passkeys.create', ['prompt' => true]);
}

return redirect()->intended(route('code16.sharp.home'));
return $this->redirectToIntendedUrl();
}

public function destroy(Request $request): RedirectResponse
Expand Down
8 changes: 6 additions & 2 deletions src/Http/Controllers/Auth/LoginMultiFactorController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
use Code16\Sharp\Auth\Passkeys\PasskeyManager;
use Code16\Sharp\Auth\TwoFactor\MultiFactorManager;
use Code16\Sharp\Enums\MultiFactorMethod;
use Code16\Sharp\Http\Controllers\Auth\Concerns\RedirectsToIntendedUrl;
use Code16\Sharp\Http\Controllers\Auth\Requests\Login2faRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class LoginMultiFactorController extends Controller
{
use RedirectsToIntendedUrl;

public function create(Request $request, MultiFactorManager $multiFactor, PasskeyManager $passkeys): RedirectResponse|Response
{
$user = $multiFactor->pendingUser();
Expand All @@ -34,11 +38,11 @@ public function create(Request $request, MultiFactorManager $multiFactor, Passke
return redirect()->route('code16.sharp.login');
}

public function store(Login2faRequest $request, MultiFactorManager $twoFactor): RedirectResponse
public function store(Login2faRequest $request, MultiFactorManager $twoFactor): SymfonyResponse
{
$request->authenticate($twoFactor->currentHandler());
$request->session()->regenerate();

return redirect()->intended(route('code16.sharp.home'));
return $this->redirectToIntendedUrl();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
namespace Code16\Sharp\Http\Controllers\Auth\Passkeys;

use Code16\Sharp\Auth\TwoFactor\MultiFactorManager;
use Code16\Sharp\Http\Controllers\Auth\Concerns\RedirectsToIntendedUrl;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;

class PasskeyAuthenticatedController extends Controller
{
use RedirectsToIntendedUrl;

public function __invoke(Request $request, MultiFactorManager $multiFactor)
{
if ($multiFactor->isUsingPasskeyMethod()) {
Expand All @@ -29,6 +32,6 @@ public function __invoke(Request $request, MultiFactorManager $multiFactor)
$multiFactor->currentHandler()->forgetCode();
}

return redirect(request()->input('intended_url', route('code16.sharp.home')));
return $this->redirectToIntendedUrl($request->input('intended_url', route('code16.sharp.home')));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
namespace Code16\Sharp\Http\Controllers\Auth\Passkeys;

use Code16\Sharp\Auth\TwoFactor\MultiFactorManager;
use Code16\Sharp\Http\Controllers\Auth\Concerns\RedirectsToIntendedUrl;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;

class PasskeyRegisteredController extends Controller
{
use RedirectsToIntendedUrl;

public function __invoke(Request $request, MultiFactorManager $multiFactor)
{
if (! session()->has('registered_passkey')) {
Expand All @@ -24,6 +27,6 @@ public function __invoke(Request $request, MultiFactorManager $multiFactor)
$multiFactor->currentHandler()->forgetCode();
}

return redirect()->intended(route('code16.sharp.home'));
return $this->redirectToIntendedUrl();
}
}
29 changes: 29 additions & 0 deletions tests/Http/Auth/AuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ function setTestAuthGuard(): void
expect(auth('sharp')->check())->toBeTrue();
});

it('redirects Inertia login to the intended Sharp url', function () {
setTestAuthGuard();

$this
->withSession(['url.intended' => route('code16.sharp.list', 'person')])
->withHeader('X-Inertia', 'true')
->post(route('code16.sharp.login.post'), [
'login' => 'test@example.org',
'password' => 'password',
])
->assertRedirect(route('code16.sharp.list', 'person'));
});

it('forces a full page visit on Inertia login when the intended url is not a Sharp page', function () {
setTestAuthGuard();

$this
->withSession(['url.intended' => url('/tools/glyphs')])
->withHeader('X-Inertia', 'true')
->post(route('code16.sharp.login.post'), [
'login' => 'test@example.org',
'password' => 'password',
])
->assertStatus(409)
->assertHeader('X-Inertia-Location', url('/tools/glyphs'));

expect(auth('sharp')->check())->toBeTrue();
});

it('does not allow to login with invalid payload', function () {
$this->post(route('code16.sharp.login.post'))
->assertSessionHasErrors(['login', 'password']);
Expand Down
Loading