guides/REST-Auth.md

REST API With Auth

This guide shows a common REST API setup using the new Auth helpers.

Setup

// Configure JWT verification (optional but recommended for APIs).
Options::set('core.auth.jwt.secret', $_ENV['APP_JWT_SECRET'] ?? 'dev-secret');
Options::set('core.auth.jwt.require_exp', true);

// Optional defaults for security add-ons.
Options::set('core.security.rate_limit.enabled', true);
Options::set('core.security.rate_limit.key', 'ip:route');

// Boot route helpers.
Auth::boot();

User Resolver

Auth::resolver(function ($identity, $source) {
  // Session: identity might be a user id stored via Auth::login().
  if ($source === 'session') {
    return User::find($identity);
  }

  // Bearer/JWT: identity is payload when JWT is valid.
  if ($source === 'bearer' && is_object($identity) && isset($identity->sub)) {
    return User::find($identity->sub);
  }

  return null;
});

Authorization Rules

Gate::define('users.read', function ($user) {
  return $user && $user->role === 'admin';
});

Login (Session or JWT)

Route::post('/login', function () {
  $email = Request::input('email');
  $pass = Request::input('password');

  $user = User::where("email = ?", [$email])->first();
  if (!$user || !Password::verify($pass, $user->password)) {
    Response::error(401, 'Invalid credentials');
    return ['error' => 'Invalid credentials'];
  }

  // Session-based login.
  Auth::login($user->id);

  // JWT-based login (for API clients).
  $jwt = Token::encode([
    'sub' => $user->id,
    'exp' => time() + 3600,
  ], Options::get('core.auth.jwt.secret'));

  return ['token' => $jwt];
})->secureHeaders()->rateLimit(10, 60);

Protected REST Routes

Route::group('/api', function () {
  Route::get('/users', function () {
    return User::all();
  })->auth()->can('users.read')->rateLimit(60, 60)->secureHeaders();

  Route::get('/users/:id', function ($id) {
    return User::find($id);
  })->auth()->rateLimit(120, 60)->secureHeaders();

  Route::post('/users', function () {
    $data = (array)Request::data();
    return User::create($data);
  })->auth()->rateLimit(30, 60)->secureHeaders();
});

API::resource With Auth

The API::resource() helper registers routes internally, so attach auth at the group level.

Route::group('/api', function () {
  API::resource('/articles', [
    'class' => 'Article',
    'sql' => [
      'table' => 'articles',
      'primary_key' => 'id',
    ],
  ]);

  API::resource('/users', [
    'class' => 'UserResource',
    'sql' => [
      'table' => 'users',
      'primary_key' => 'id',
    ],
  ]);
})
->before(function () {
  // Auth gate for all /api resources
  if (!Auth::check()) {
    Response::error(401, 'Unauthorized');
    Response::add('Unauthorized');
    return false;
  }

  // Optional rate limiting for all /api resources
  if (Options::get('core.security.rate_limit.enabled', true)) {
    $result = RateLimiter::check(RateLimiter::defaultKey(), 120, 60);
    RateLimiter::applyHeaders(120, $result['remaining'], $result['reset']);
    if (!$result['allowed']) {
      Response::error(429, 'Too Many Requests');
      Response::add('Too Many Requests');
      return false;
    }
  }

  SecurityHeaders::apply();
});

API::resource With Per-Resource Abilities

If you need per-resource authorization, you can wrap each resource in its own group and use Gate.

Gate::define('articles.read', function ($user) {
  return $user && $user->role === 'editor';
});

Gate::define('users.read', function ($user) {
  return $user && $user->role === 'admin';
});

Route::group('/api/articles', function () {
  API::resource('/', [
    'class' => 'Article',
    'sql' => [
      'table' => 'articles',
      'primary_key' => 'id',
    ],
  ]);
})
->before(function () {
  if (!Auth::check() || !Gate::allows('articles.read')) {
    Response::error(403, 'Forbidden');
    Response::add('Forbidden');
    return false;
  }
  SecurityHeaders::apply();
});

Route::group('/api/users', function () {
  API::resource('/', [
    'class' => 'UserResource',
    'sql' => [
      'table' => 'users',
      'primary_key' => 'id',
    ],
  ]);
})
->before(function () {
  if (!Auth::check() || !Gate::allows('users.read')) {
    Response::error(403, 'Forbidden');
    Response::add('Forbidden');
    return false;
  }
  SecurityHeaders::apply();
});

CSRF (Session Clients Only)

For browser-based clients that use session auth, enable CSRF protection on write routes:

Route::post('/account', function () {
  return ['ok' => true];
})->auth()->csrf()->secureHeaders();

CSRF tokens can be read with:

$token = CSRF::token();

Send the token via X-CSRF-Token header or _csrf input.