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\Auth\AuthBootstrap;
use Symfony\Component\HttpFoundation\Request;
class ProfileController
{
public function show(Request $request)
{
// Using the auth manager to re-authenticate (idempotent)
$authManager = AuthBootstrap::getManager();
$user = $authManager->authenticate($request); // array|null
if (!$user) {
return Response::unauthorized('Not authenticated');
}
return Response::success([
'user' => [
'id' => $user['uuid'] ?? null,
'email' => $user['email'] ?? null,
'username' => $user['username'] ?? null,
]
]);
}
}
Adding Registration (Custom)
Registration is application-specific. Minimal illustrative flow:
use Glueful\Auth\TokenManager;
use Glueful\Helpers\Utils;
class RegistrationController
{
public function register(Request $request)
{
$data = RequestHelper::getRequestData();
// (Validate using your validator service)
$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')
];
db()->table('users')->insert($userRecord);
// Don't return password
unset($userRecord['password']);
$tokens = TokenManager::generateTokenPair([
'uuid' => $userRecord['uuid'],
'email' => $userRecord['email'],
'username' => $userRecord['username']
]);
return Response::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\AuthBootstrap;
use Glueful\Routing\RouteMiddleware;
use Symfony\Component\HttpFoundation\Request;
class RequireRolesMiddleware implements RouteMiddleware
{
public function handle(Request $request, callable $next, ...$requiredRoles): mixed
{
$authManager = AuthBootstrap::getManager();
$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:
$post = db()->table('posts')
->where(['uuid' => $postUuid])
->get()[0] ?? null;
if (!$post) return Response::notFound('Post not found');
if (($post['user_uuid'] ?? null) !== ($user['uuid'] ?? null)) {
return Response::forbidden('You can only edit your own posts');
}
db()->table('posts')->where(['uuid' => $postUuid])->update($payload);
return Response::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()
{
$user = $this->auth->user();
// Load related data
$userData = db()->table('users')
->where(['uuid' => $user['uuid']])
->get()[0] ?? null;
$postCount = db()->table('posts')
->where(['user_uuid' => $user['uuid']])
->count();
return Response::success([
'user' => $userData,
'stats' => [
'posts_count' => $postCount
]
]);
}
Update Profile
public function updateProfile()
{
$validated = $this->validate($this->request->input(), [
'name' => 'nullable|min:2',
'bio' => 'nullable|max:500',
'website' => 'nullable|url'
]);
db()->table('users')
->where(['uuid' => $user['uuid']])
->update($validated);
return Response::success(null, 'Profile updated');
}
Change Password
public function changePassword()
{
$data = $this->validate($this->request->input(), [
'current_password' => 'required',
'new_password' => 'required|min:8|confirmed'
]);
$user = db()->table('users')
->where(['uuid' => $current['uuid']])
->get()[0] ?? null;
// Verify current password
if (!password_verify($data['current_password'], $user->password)) {
return Response::error('Current password is incorrect', 400);
}
// Update password
db()->table('users')
->where(['uuid' => $user['uuid']])
->update([
'password' => password_hash($data['new_password'], PASSWORD_DEFAULT)
]);
return Response::success(null, 'Password changed successfully');
}
Best Practices
Never Store Plain Passwords
// ✅ Always hash passwords
$user->password = password_hash($password, PASSWORD_DEFAULT);
// ❌ Never store plain text
$user->password = $password;
Use HTTPS in Production
// config/app.php
'force_https' => env('FORCE_HTTPS', true)
Validate Strong Passwords
$this->validate($data, [
'password' => 'required|min:8|regex:/[a-z]/|regex:/[A-Z]/|regex:/[0-9]/'
]);
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
auth
middleware applied - Check Authorization header present & formatted
Bearer <token>
- Confirm access token not expired (inspect
expires_in
returned at login)
"Invalid token"?
- Token revoked or malformed
jwt_key
mismatch 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