Authentication

Authentication is the step where your application decides who a request belongs to.

In practice, most apps need one of three approaches:

  • a session for browser-based login
  • a JWT for API clients
  • an OAuth login flow such as "Sign in with GitHub"

Assegai keeps those concerns in a separate package:

composer require assegaiphp/auth:^0.10

The auth package is intentionally small. It helps you establish auth state, but it does not create a user database, register routes for you, or decide what your login screens should look like.

That separation is useful early on, because you can learn authentication one step at a time instead of adopting a full auth system all at once.

What the package gives you

assegaiphp/auth ships with:

  • SessionAuthStrategy
  • JwtAuthStrategy
  • OAuth2AuthStrategy
  • GitHubOAuthProvider

Think of a strategy as one way to answer the question:

How should this app remember who the current user is?

Session authentication in a Core application

Use a session when your app serves browser pages and you want the user to stay logged in between requests.

Core owns session startup and shutdown around each framework request. Configure browser-login policy and session cookie settings in config/auth.php:

<?php

return [
  'authentication' => [
    'loginRedirect' => [
      'url' => '/auth/login',
      'statusCode' => 302,
      'preserveTarget' => true,
      'targetSessionKey' => 'auth.intended_url',
      'excludedPaths' => [],
    ],
  ],
  'session' => [
    'name' => 'blog_session',
    'cookieLifetime' => 3600,
    'cookiePath' => '/',
    'cookieDomain' => '',
    'cookieSecure' => null,
    'cookieHttpOnly' => true,
    'cookieSameSite' => 'Lax',
  ],
];

Set the URL to a public login route your application actually provides. The configuration does not enable redirects globally.

Your application still loads the user before asking the strategy to verify the submitted credentials:

<?php

namespace App\Auth;

use App\Users\UsersRepository;
use Assegai\Auth\Strategies\SessionAuthStrategy;
use Assegai\Core\Attributes\Injectable;

#[Injectable]
final readonly class AuthService
{
  public function __construct(private UsersRepository $users)
  {
  }

  public function login(string $email, string $password): ?object
  {
    $user = $this->users->findByEmail($email);

    if (!$user) {
      return null;
    }

    $strategy = new SessionAuthStrategy([
      'user' => $user,
      'username_field' => 'email',
      'password_field' => 'password',
    ]);

    return $strategy->authenticate([
      'email' => $email,
      'password' => $password,
    ]) ? $strategy->getUser() : null;
  }

  public function isAuthenticated(): bool
  {
    return (new SessionAuthStrategy())->isAuthenticated();
  }
}

On success, SessionAuthStrategy verifies the password, rotates the session identifier, and stores a sanitized user without the password field.

Keep the access decision in a guard:

<?php

namespace App\Auth;

use Assegai\Core\Attributes\Injectable;
use Assegai\Core\Interfaces\ICanActivate;
use Assegai\Core\Interfaces\IExecutionContext;

#[Injectable]
final readonly class SessionAuthGuard implements ICanActivate
{
  public function __construct(private AuthService $auth)
  {
  }

  public function canActivate(IExecutionContext $context): bool
  {
    return $this->auth->isAuthenticated();
  }
}

Then choose the HTTP behavior at the protected browser controller:

<?php

use App\Auth\SessionAuthGuard;
use Assegai\Core\Attributes\Controller;
use Assegai\Core\Attributes\UseFilters;
use Assegai\Core\Attributes\UseGuards;
use Assegai\Core\Exceptions\Filters\LoginRedirectFilter;
use Assegai\Core\Exceptions\Http\UnauthorizedException;

#[Controller('dashboard')]
#[UseGuards(SessionAuthGuard::class, UnauthorizedException::class)]
#[UseFilters(LoginRedirectFilter::class)]
final class DashboardController
{
}

The guard remains responsible for allowing or denying access. The opt-in exception filter translates UnauthorizedException into a browser redirect and can preserve a safe local GET or HEAD target before Core closes the session.

Do not apply LoginRedirectFilter to API-only controllers. The same guard failure then keeps the normal 401 Unauthorized response with no redirect. No auth interceptor is required.

After a successful login form POST, retrieve the intended target once and redirect with 303:

$target = $session->pull('auth.intended_url', '/dashboard');
return $response->redirect($target, 303);

Validate that the target is a safe local path before redirecting. The built-in filter rejects cross-origin, scheme-relative, malformed, unsafe-method, and login-route targets when storing the original request.

JWT authentication

Use JWT authentication when the client is calling your API directly and you want to return a token instead of depending on PHP session storage.

<?php

use Assegai\Auth\Strategies\JwtAuthStrategy;

$strategy = new JwtAuthStrategy([
  'user' => $user,
  'secret_key' => 'replace-with-a-long-random-secret-key',
  'issuer' => 'blog-api',
  'audience' => 'blog-api-clients',
  'token_lifetime' => '+1 hour',
]);

if ($strategy->authenticate([
  'email' => 'user@example.com',
  'password' => 'secret',
])) {
  $token = $strategy->getToken();
}

The generated token can then be sent back by the client in:

Authorization: Bearer <token>

OAuth login

OAuth is a redirect-based login flow. Instead of asking your app to verify a password directly, your app sends the user to a provider such as GitHub, and the provider sends the user back after login.

Assegai's auth package supports that flow too:

  • build the provider redirect URL
  • validate the callback
  • exchange the authorization code for tokens
  • read the provider profile
  • establish a local session or JWT if you want one
<?php

use Assegai\Auth\OAuth\OAuth2AuthStrategy;
use Assegai\Auth\OAuth\Providers\GitHubOAuthProvider;
use Assegai\Auth\OAuth\State\SessionOAuthStateStore;
use Assegai\Auth\Strategies\SessionAuthStrategy;

$oauth = new OAuth2AuthStrategy(
  provider: new GitHubOAuthProvider(),
  config: GitHubOAuthProvider::defaultConfig(
    clientId: 'your-github-client-id',
    clientSecret: 'your-github-client-secret',
    redirectUri: 'https://example.com/auth/github/callback',
  ),
  stateStore: new SessionOAuthStateStore(),
  sessionStrategy: new SessionAuthStrategy(['user' => (object) []]),
);

$request = $oauth->beginLogin();

That gives you a redirect URL to send the browser to.

What your app still needs to do

The auth package gives you the strategy, not the full application workflow.

You still decide:

  • where users are loaded from
  • what a login endpoint or page looks like
  • whether you want sessions, JWTs, or both
  • how to map an OAuth profile onto a local user record
  • where to redirect the user after login

That is deliberate. It keeps assegaiphp/auth usable in both Assegai and non-Assegai projects.

A simple Assegai login flow

A common pattern in an Assegai app is:

  1. a controller accepts login input
  2. a provider loads the user
  3. the provider gives that user to a strategy
  4. the strategy establishes auth state
  5. the controller returns JSON or redirects

That keeps HTTP work in the controller and authentication behavior in a dedicated service or provider.

When to use which approach

  • use sessions for browser-first apps
  • use JWTs for API-first clients
  • use OAuth when login should be delegated to a provider such as GitHub

It is also normal to combine them. For example, an OAuth callback can establish a session for the browser and mint a JWT for later API calls.

Where to go next

If you want the practical login flow first, continue with the tutorial track once it is relevant to your app.

If you want the deeper auth details next, continue with Authentication and OAuth In Depth.