Essentials

Authentication

Built-in authentication endpoints, token flows, and how to extend them

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:

  1. Login endpoint (credential + optional provider & remember-me)
  2. Verification & OTP (email verification + OTP verify + resend)
  3. Password recovery (forgot + reset)
  4. Token lifecycle (validate, refresh-token, refresh-permissions, logout)
  5. CSRF token endpoint (for SPAs using cookies)
  6. 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

EndpointMethodPurposeNotes
/auth/loginPOSTCredential login & session creationRate limited (5/min). Returns access + refresh token pair + user + expires_in
/auth/verify-emailPOSTSend verification code to emailProvide email
/auth/verify-otpPOSTVerify OTP sent to emailRate limited (3/min)
/auth/resend-otpPOSTResend OTPStricter rate limit (2/2min)
/auth/forgot-passwordPOSTInitiate password reset (sends code)Requires existing user
/auth/reset-passwordPOSTComplete password reset with codeProvide email + password + verification code (implementation detail)
/auth/validate-tokenPOSTValidate current access tokenRequires auth middleware
/auth/refresh-tokenPOSTExchange refresh token for new pairBody contains refresh_token
/auth/refresh-permissionsPOSTRe-issue token after role/permission changesRequires auth
/auth/logoutPOSTInvalidate current access token + sessionRequires auth
/csrf-tokenGETRetrieve CSRF token metadataSecurity / 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:

  1. POST /auth/forgot-password – initiate (sends email / code)
  2. 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