Authentication
Glueful ships with a production‑oriented authentication stack: credential based login, email & OTP verification, password reset, token validation, refresh & permission refresh flows, plus CSRF token support. Tokens are access + refresh pairs (OIDC‑style response shape) with session storage, revocation, rotation and multi‑provider support under the hood.
Quick Start
Glueful provides these out of the box:
- Login endpoint (credential + optional provider & remember-me)
- Verification & OTP (email verification + OTP verify + resend)
- Password recovery (forgot + reset)
- Token lifecycle (validate, refresh-token, refresh-permissions, logout)
- CSRF token endpoint (for SPAs using cookies)
- Auth middleware to protect routes
Registration is NOT automatically provided (by design you define how users are created). A custom registration example is included below.
Built-in Endpoints
| Endpoint | Method | Purpose | Notes |
|---|---|---|---|
| /auth/login | POST | Credential login & session creation | Rate limited (5/min). Returns access + refresh token pair + user + expires_in |
| /auth/verify-email | POST | Send verification code to email | Provide email |
| /auth/verify-otp | POST | Verify OTP sent to email | Rate limited (3/min) |
| /auth/resend-otp | POST | Resend OTP | Stricter rate limit (2/2min) |
| /auth/forgot-password | POST | Initiate password reset (sends code) | Requires existing user |
| /auth/reset-password | POST | Complete password reset with code | Provide email + password + verification code (implementation detail) |
| /auth/validate-token | POST | Validate current access token | Requires auth middleware |
| /auth/refresh-token | POST | Exchange refresh token for new pair | Body contains refresh_token |
| /auth/refresh-permissions | POST | Re-issue token after role/permission changes | Requires auth |
| /auth/logout | POST | Invalidate current access token + session | Requires auth |
| /csrf-token | GET | Retrieve CSRF token metadata | Security / SPA usage |
Custom registration endpoint is intentionally omitted from core to let you enforce business rules (approval flows, invitations, etc.).
Login (Built-In)
Example request payload:
{
"username": "[email protected]",
"password": "secret123",
"remember": true
}
Successful response shape (fields may vary depending on provider & configuration):
{
"success": true,
"message": "Login successful",
"data": {
"access_token": "<JWT>",
"refresh_token": "<refresh>",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": "user-uuid",
"email": "[email protected]",
"username": "user",
"email_verified": false,
"locale": "en-US"
}
}
}
Error responses align with middleware guidance (e.g. TOKEN_EXPIRED, SESSION_EXPIRED).
Protecting Routes
Use the auth middleware to require authentication:
// routes/api.php
// Public routes
$router->post('/auth/login', [AuthController::class, 'login']);
$router->post('/auth/register', [AuthController::class, 'register']);
// Protected routes
$router->group(['middleware' => ['auth']], function ($router) {
$router->get('/profile', [ProfileController::class, 'show']);
$router->put('/profile', [ProfileController::class, 'update']);
$router->post('/posts', [PostController::class, 'store']);
});
// Single protected route
$router->get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth');
Making Authenticated Requests
Include the JWT token in the Authorization header:
curl http://localhost:8000/api/profile \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..."
Accessing Current User
During a request protected by auth middleware, user data is attached to the request context and included in the login / token responses. Typical pattern:
use Glueful\Controllers\BaseController;
use Glueful\Http\Response;
use Symfony\Component\HttpFoundation\Request;
class ProfileController extends BaseController
{
public function show(Request $request): Response
{
if ($this->currentUser === null) {
return $this->unauthorized('Not authenticated');
}
return $this->success([
'user' => $this->currentUser->toArray(),
]);
}
}
Adding Registration (Custom)
Registration is application-specific. Minimal illustrative flow:
use Glueful\Auth\TokenManager;
use Glueful\Controllers\BaseController;
use Glueful\Helpers\Utils;
use Symfony\Component\HttpFoundation\Request;
class RegistrationController extends BaseController
{
public function register(Request $request): Response
{
$data = $this->getRequestData();
if ($error = $this->validateRequest($data, [
'email' => 'required|email|max:255',
'password' => 'required|max:255',
'username' => 'max:100',
])) {
return $error;
}
$userRecord = [
'uuid' => Utils::generateNanoID(),
'email' => $data['email'],
'username' => $data['username'] ?? null,
'password' => password_hash($data['password'], PASSWORD_BCRYPT),
'created_at' => date('Y-m-d H:i:s')
];
$this->db->table('users')->insert($userRecord);
// Don't return password
unset($userRecord['password']);
$tokenManager = app($this->getContext(), TokenManager::class);
$tokens = $tokenManager->generateTokenPair([
'uuid' => $userRecord['uuid'],
'email' => $userRecord['email'],
'username' => $userRecord['username']
]);
return $this->created([
'user' => $userRecord,
...$tokens
]);
}
}
Add a route manually (e.g. POST /auth/register) if desired.
The built-in AuthController::login() already handles credential validation, provider selection, remember-me and token issuance.
Token Lifetimes & Configuration
Relevant keys (e.g. in config/session.php or environment):
return [
'access_token_lifetime' => 3600, // seconds
'refresh_token_lifetime' => 2592000, // 30 days default
'jwt_key' => env('JWT_KEY'), // signing secret
];
Values are consumed by TokenManager & providers. Refresh tokens are opaque (stored server-side); access tokens are JWT.
Refresh Token
Exchange a refresh token for a new pair:
POST /auth/refresh-token
Content-Type: application/json
{
"refresh_token": "<refresh>"
}
Response:
{
"success": true,
"message": "Token refreshed successfully",
"data": {
"access_token": "<new_access>",
"refresh_token": "<new_refresh>",
"expires_in": 3600
}
}
Logout
POST /auth/logout
Authorization: Bearer <access_token>
Response: { "success": true, "message": "Logged out successfully" }
Invalidates access token + associated session.
Validate Token
POST /auth/validate-token
Authorization: Bearer <access_token>
Returns { user, is_valid: true } if token active.
Refresh Permissions
Re-issues token after permission/role changes:
POST /auth/refresh-permissions
Authorization: Bearer <access_token>
Role & Permission Access
Role / permission management is provided via the optional Aegis (RBAC) extension. If you store roles directly in the token payload (basic case):
if (!in_array('admin', $user['roles'] ?? [], true)) {
return Response::forbidden('Admin access required');
}
For advanced RBAC, integrate Aegis services instead of manual checks.
Role Middleware
The core AuthMiddleware already supports an "admin required" flag if you pass admin as an additional middleware parameter on a route/group. Custom role middleware is only needed for more granular or domain-specific checks (e.g. permission scopes, feature flags).
Using Built-In Admin Gate
// Require authentication AND admin privilege (AuthMiddleware interprets 'admin')
$router->get('/admin/stats', [AdminController::class, 'stats'])
->middleware('auth:admin'); // passing parameter after colon
If your router does not yet parse colon parameters for middleware you can instead configure the route group with an explicit parameter array (if supported), or implement a thin wrapper.
Custom Role/Permission Middleware (Granular Example)
Implements the same RouteMiddleware interface pattern as core middleware:
use Glueful\Auth\AuthenticationManager;
use Glueful\Bootstrap\ApplicationContext;
use Glueful\Routing\RouteMiddleware;
use Glueful\Http\Response;
use Symfony\Component\HttpFoundation\Request;
class RequireRolesMiddleware implements RouteMiddleware
{
public function __construct(
private ApplicationContext $context
) {}
public function handle(Request $request, callable $next, ...$requiredRoles): mixed
{
$authManager = app($this->context, AuthenticationManager::class);
$user = $authManager->authenticate($request);
if (!$user) {
return Response::unauthorized('Authentication required');
}
$userRoles = $user['roles'] ?? [];
foreach ($requiredRoles as $role) {
if (!in_array($role, $userRoles, true)) {
return Response::forbidden('Missing required role: ' . $role);
}
}
return $next($request);
}
}
// Usage (middleware alias 'roles'):
$router->group(['middleware' => ['auth', 'roles:editor,reviewer']], function($router) {
$router->post('/content/review', [ContentController::class, 'review']);
});
For complex authorization (resource-level permissions, dynamic policies), prefer the RBAC Aegis extension which supplies permission repositories and assignment services rather than rolling custom middleware repeatedly.
Resource Ownership
Pattern example:
if ($this->currentUser === null) {
return $this->unauthorized();
}
$post = $this->db->table('posts')
->where(['uuid' => $postUuid])
->first();
if ($post === null) {
return $this->notFound('Post not found');
}
if (($post['user_uuid'] ?? null) !== $this->currentUser->uuid) {
return $this->forbidden('You can only edit your own posts');
}
$this->db->table('posts')
->where(['uuid' => $postUuid])
->update($payload);
return $this->success(null, 'Post updated');
Password Reset
Use built-in endpoints:
POST /auth/forgot-password– initiate (sends email / code)POST /auth/reset-password– complete reset
The controller handles validation, session consistency, and email dispatch. Implement a custom flow only if you need alternative storage or multi-factor requirements.
Common Patterns
User Profile Endpoint (Custom Example)
public function profile()
{
if ($this->currentUser === null) {
return $this->unauthorized();
}
// Load related data
$userData = $this->db->table('users')
->where(['uuid' => $this->currentUser->uuid])
->first();
$postCount = $this->db->table('posts')
->where(['user_uuid' => $this->currentUser->uuid])
->count();
return $this->success([
'user' => $userData,
'stats' => [
'posts_count' => $postCount
]
]);
}
Update Profile
public function updateProfile()
{
if ($this->currentUser === null) {
return $this->unauthorized();
}
$data = $this->getRequestData();
if ($error = $this->validateRequest($data, [
'name' => 'max:100',
'bio' => 'max:500',
'website' => 'max:255'
])) {
return $error;
}
$this->db->table('users')
->where('uuid', $this->currentUser->uuid)
->update($data);
return $this->success(null, 'Profile updated');
}
Change Password
public function changePassword()
{
if ($this->currentUser === null) {
return $this->unauthorized();
}
$data = $this->getRequestData();
if ($error = $this->validateRequest($data, [
'current_password' => 'required|max:255',
'new_password' => 'required|max:255'
])) {
return $error;
}
$user = $this->db->table('users')
->where('uuid', $this->currentUser->uuid)
->first();
// Verify current password
if (!$user || !password_verify($data['current_password'], $user['password'])) {
return Response::error('Current password is incorrect', 400);
}
// Update password
$this->db->table('users')
->where(['uuid' => $this->currentUser->uuid])
->update([
'password' => password_hash($data['new_password'], PASSWORD_DEFAULT)
]);
return $this->success(null, 'Password changed successfully');
}
Best Practices
Never Store Plain Passwords
// ✅ Always hash passwords
$userRecord['password'] = password_hash($password, PASSWORD_DEFAULT);
// ❌ Never store plain text
$userRecord['password'] = $password;
Use HTTPS in Production
// config/app.php
'force_https' => env('FORCE_HTTPS', true)
Validate Strong Passwords
use Glueful\Validation\Support\RuleParser;
use Glueful\Validation\Validator;
use Glueful\Validation\ValidationException;
$rules = (new RuleParser())->parse([
'password' => 'required|min:8|regex:/[a-z]/|regex:/[A-Z]/|regex:/[0-9]/'
]);
$validator = new Validator($rules);
$errors = $validator->validate($data);
if ($errors !== []) {
throw new ValidationException($errors);
}
Rate Limit Auth Endpoints
$router->post('/auth/login', [AuthController::class, 'login'])
->middleware('rate_limit:5,60'); // 5 attempts per minute
Troubleshooting
"Unauthorized" on protected routes?
- Ensure
authmiddleware applied - Check Authorization header present & formatted
Bearer <token> - Confirm access token not expired (inspect
expires_inreturned at login)
"Invalid token"?
- Token revoked or malformed
jwt_keymismatch between environments- User session removed (logout, cleanup) – re-login
User null?
- Middleware missing OR token failed validation
- Using refresh token instead of access token
- Provider mismatch (multi-provider scenario)
Next Steps
- Routing – Apply middleware & groups
- Controllers – Structure request handling
- Validation – Harden input rules
- RBAC (Aegis Extension) – Advanced roles/permissions