Choose the authentication approach that matches the kind of client you are building.
Authentication strategy vs application flow
assegaiphp/auth gives you authentication strategies.
A strategy answers questions like:
- should login result in a PHP session?
- should login result in a JWT?
- should login begin with an OAuth redirect?
Your application still owns the wider flow:
- load a user
- decide which strategy to use
- call the strategy
- decide what response to send
That distinction matters because it keeps the package usable outside Assegai too.
Session strategy in real applications
Session auth is usually the simplest fit for:
- admin panels
- dashboards
- back-office tools
- server-rendered apps
The session strategy expects a known user object. Your repository or service should load that object, including its password hash, before authentication:
<?php
use Assegai\Auth\Strategies\SessionAuthStrategy;
$strategy = new SessionAuthStrategy([
'user' => $user,
'username_field' => 'email',
'password_field' => 'password',
]);
$authenticated = $strategy->authenticate([
'email' => $submittedEmail,
'password' => $submittedPassword,
]);
Inside a Core application, do not repeat session_name or session_lifetime on each strategy. Core owns the active framework session. Configure the shared policy in config/auth.php:
<?php
return [
'authentication' => [
'loginRedirect' => [
'url' => '/auth/login',
'statusCode' => 302,
'preserveTarget' => true,
'targetSessionKey' => 'auth.intended_url',
'excludedPaths' => [],
],
],
'session' => [
'name' => 'admin_portal',
'cookieLifetime' => 28800,
'cookiePath' => '/',
'cookieDomain' => '',
'cookieSecure' => null,
'cookieHttpOnly' => true,
'cookieSameSite' => 'Lax',
],
];
If authentication succeeds, the strategy rotates the session identifier and stores a safe copy of the user without its password field.
That means a later request can ask:
if ($strategy->isAuthenticated()) {
$currentUser = $strategy->getUser();
}
Guard failure and browser response policy
Authentication state, access policy, and HTTP failure behavior are separate concerns:
SessionAuthStrategyestablishes and reads auth state.- A guard decides whether the current request may reach the handler.
UnauthorizedExceptionexpresses the failed access decision.- An exception filter decides whether that failure remains a 401 or becomes a browser redirect.
For a protected browser controller, opt into the built-in redirect filter:
#[UseGuards(SessionAuthGuard::class, UnauthorizedException::class)]
#[UseFilters(LoginRedirectFilter::class)]
final class AdminController
{
}
Core resolves the filter through dependency injection and reads authentication.loginRedirect from the active application configuration. Handler filters take precedence over controller filters, which take precedence over global filters. The first matching filter is terminal and owns the response.
The filter runs before Core closes the session, so it can retain a safe local GET or HEAD target. Retrieve that target once after a successful login form POST:
$target = $session->pull('auth.intended_url', '/admin');
return $response->redirect($target, 303);
Keep API controllers free of LoginRedirectFilter; the guard's UnauthorizedException then follows the normal 401 path. An interceptor is not needed because redirects are failure-response policy, not behavior around a handler that should execute.
If an application needs behavior beyond the built-in options, it can pass a configured LoginRedirectFilter instance or implement ExceptionFilterInterface and apply that bespoke filter.
Standalone session strategy
Outside Core, the strategy owns session startup and accepts standalone options:
$strategy = new SessionAuthStrategy([
'user' => $user,
'username_field' => 'email',
'password_field' => 'password',
'session_name' => 'admin_portal',
'session_lifetime' => '+8 hours',
]);
Cookie path, domain, Secure, HttpOnly, and SameSite policy should be configured through ordinary PHP session settings before authenticating in standalone mode.
JWT strategy in real applications
JWT auth is usually a better fit for:
- SPA clients
- mobile apps
- third-party API consumers
- machine-to-machine calls
The token is generated once and then sent back by the client on later requests:
Authorization: Bearer <token>
If you use JWT auth, think clearly about:
- token lifetime
- secret management
- issuer and audience
- how clients refresh or re-authenticate when tokens expire
assegaiphp/auth handles token creation and validation. Your app still chooses the surrounding policy.
OAuth changes the login flow
OAuth is different from a simple email/password flow because the user leaves your app and returns later.
That means the flow has two distinct phases:
- begin login
- handle the callback
Begin login
<?php
use Assegai\Auth\OAuth\OAuth2AuthStrategy;
use Assegai\Auth\OAuth\Providers\GitHubOAuthProvider;
use Assegai\Auth\OAuth\State\SessionOAuthStateStore;
$strategy = 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(),
);
$request = $strategy->beginLogin();
The returned OAuthAuthorizationRequest contains:
- the provider URL
- the generated state value
- the PKCE verifier
- the PKCE challenge
In a browser flow, your route would usually redirect the user to $request->url.
Handle the callback
<?php
$result = $strategy->handleCallback($_GET);
That callback step:
- validates provider errors
- checks the returned state value
- exchanges the code for tokens
- fetches the provider profile
- resolves a local user object
If you also pass a SessionAuthStrategy or JwtAuthStrategy, the callback can establish local auth state immediately.
GitHub OAuth provider
The package ships a GitHub adapter:
If you need another provider, implement OAuthProviderInterface or extend AbstractOAuthProvider.
Mapping provider users to local users
An OAuth provider profile is not always the same thing as your application's user record.
For example, you may need to:
- look up an existing user by email
- create a user the first time they sign in
- link one GitHub identity to one local user row
- restrict login to a company domain or allow-list
That is why OAuth2AuthStrategy uses a resolver step instead of assuming every provider profile should become a user automatically.
A realistic callback handler
The code below shows the shape of a practical callback flow:
<?php
use Assegai\Auth\OAuth\DefaultOAuthUserResolver;
use Assegai\Auth\OAuth\OAuth2AuthStrategy;
use Assegai\Auth\OAuth\Providers\GitHubOAuthProvider;
use Assegai\Auth\OAuth\State\SessionOAuthStateStore;
use Assegai\Auth\Strategies\SessionAuthStrategy;
$session = new SessionAuthStrategy(['user' => (object) []]);
$strategy = new OAuth2AuthStrategy(
provider: new GitHubOAuthProvider(),
config: GitHubOAuthProvider::defaultConfig(
clientId: $githubClientId,
clientSecret: $githubClientSecret,
redirectUri: $redirectUri,
),
stateStore: new SessionOAuthStateStore(),
userResolver: new DefaultOAuthUserResolver(),
sessionStrategy: $session,
);
$result = $strategy->handleCallback($_GET);
// Redirect to the app, return JSON, or create a local account link here.
In a production app, the resolver is often the first thing you will replace with your own logic.
What remains application-owned
The auth package intentionally leaves these responsibilities to the application:
- login controllers
- callback routes
- guard decorators
- refresh tokens
- full account linking workflows
- provider-specific dashboard configuration
Security notes
Keep these practical rules in mind:
- never store plain-text passwords
- use a long random JWT secret
- keep OAuth client secrets out of source control
- validate your callback URLs carefully
- use HTTPS in real deployments
Choosing the right path
- choose session auth when the browser is the main client
- choose JWT auth when the client is a separate API consumer
- choose OAuth when an external identity provider should handle login
- combine them when the user signs in through a provider and your app still needs local session or token state
Where to go next
If you have not built a full feature yet, go back to Building a Feature.
If you want an end-to-end learning path, continue with the tutorial section and build a small app from scratch using the CLI, modules, DTOs, and API docs together.