Authentication Guide¶
Implement authentication and authorization in your Tusk applications.
JWT Authentication¶
Setup¶
composer require firebase/php-jwt
JWT Service¶
<?php
namespace App\Service;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService
{
private string $secret;
private string $algorithm = 'HS256';
public function __construct(string $secret)
{
$this->secret = $secret;
}
public function encode(array $payload): string
{
$payload['iat'] = time();
$payload['exp'] = time() + (60 * 60 * 24); // 24 hours
return JWT::encode($payload, $this->secret, $this->algorithm);
}
public function decode(string $token): object
{
return JWT::decode($token, new Key($this->secret, $this->algorithm));
}
public function verify(string $token): bool
{
try {
$this->decode($token);
return true;
} catch (\Exception $e) {
return false;
}
}
}
Auth Service¶
<?php
namespace App\Service;
use App\Repository\UserRepository;
class AuthService
{
public function __construct(
private UserRepository $userRepo,
private JwtService $jwt
) {}
public function login(string $email, string $password): ?string
{
$user = $this->userRepo->findByEmail($email);
if (!$user || !password_verify($password, $user['password'])) {
return null;
}
return $this->jwt->encode([
'user_id' => $user['id'],
'email' => $user['email'],
]);
}
public function verify(string $token): ?array
{
try {
$payload = $this->jwt->decode($token);
return $this->userRepo->findById($payload->user_id);
} catch (\Exception $e) {
return null;
}
}
public function register(array $data): int
{
$data['password'] = password_hash($data['password'], PASSWORD_BCRYPT);
return $this->userRepo->create($data);
}
}
Auth Controller¶
<?php
namespace App\Controller;
use Tusk\Web\Attribute\Route;
use Tusk\Web\Http\Request;
use Tusk\Web\Http\Response;
use App\Service\AuthService;
class AuthController
{
public function __construct(
private AuthService $auth
) {}
#[Route('/auth/register', methods: ['POST'])]
public function register(Request $request): Response
{
$data = json_decode($request->getBody(), true);
// Validate
if (!isset($data['email'], $data['password'], $data['name'])) {
return new Response(400, [], json_encode([
'error' => 'Missing required fields'
]));
}
// Register user
try {
$userId = $this->auth->register($data);
return new Response(201, [], json_encode([
'id' => $userId,
'message' => 'User registered successfully'
]));
} catch (\Exception $e) {
return new Response(500, [], json_encode([
'error' => 'Registration failed'
]));
}
}
#[Route('/auth/login', methods: ['POST'])]
public function login(Request $request): Response
{
$data = json_decode($request->getBody(), true);
if (!isset($data['email'], $data['password'])) {
return new Response(400, [], json_encode([
'error' => 'Email and password required'
]));
}
$token = $this->auth->login($data['email'], $data['password']);
if (!$token) {
return new Response(401, [], json_encode([
'error' => 'Invalid credentials'
]));
}
return new Response(200, [], json_encode([
'token' => $token
]));
}
#[Route('/auth/me', methods: ['GET'])]
public function me(Request $request): Response
{
$authHeader = $request->getHeader('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
return new Response(401, [], json_encode([
'error' => 'Unauthorized'
]));
}
$token = substr($authHeader, 7);
$user = $this->auth->verify($token);
if (!$user) {
return new Response(401, [], json_encode([
'error' => 'Invalid token'
]));
}
// Remove sensitive data
unset($user['password']);
return new Response(200, [], json_encode($user));
}
}
Middleware (Future Feature)¶
<?php
namespace App\Middleware;
use Tusk\Web\Http\Request;
use Tusk\Web\Http\Response;
use App\Service\AuthService;
class AuthMiddleware
{
public function __construct(
private AuthService $auth
) {}
public function handle(Request $request, callable $next): Response
{
$authHeader = $request->getHeader('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
return new Response(401, [], json_encode([
'error' => 'Unauthorized'
]));
}
$token = substr($authHeader, 7);
$user = $this->auth->verify($token);
if (!$user) {
return new Response(401, [], json_encode([
'error' => 'Invalid token'
]));
}
// Attach user to request
$request->setAttribute('user', $user);
return $next($request);
}
}
Protected Routes¶
Manual Check¶
#[Route('/users/{id}', methods: ['PUT'])]
public function update(Request $request, int $id): Response
{
// Get authenticated user
$authHeader = $request->getHeader('Authorization');
if (!$authHeader) {
return new Response(401, [], json_encode(['error' => 'Unauthorized']));
}
$token = substr($authHeader, 7);
$user = $this->auth->verify($token);
if (!$user) {
return new Response(401, [], json_encode(['error' => 'Invalid token']));
}
// Check authorization
if ($user['id'] !== $id && !$user['is_admin']) {
return new Response(403, [], json_encode(['error' => 'Forbidden']));
}
// Update user
$data = json_decode($request->getBody(), true);
$this->userRepo->update($id, $data);
return new Response(200, [], json_encode(['message' => 'Updated']));
}
Role-Based Access Control (RBAC)¶
User Roles¶
class UserRepository extends AbstractRepository
{
public function findWithRoles(int $userId): array
{
$user = $this->findById($userId);
$stmt = $this->connection->prepare('
SELECT r.name FROM roles r
INNER JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = ?
');
$stmt->execute([$userId]);
$user['roles'] = array_column($stmt->fetchAll(\PDO::FETCH_ASSOC), 'name');
return $user;
}
public function hasRole(int $userId, string $role): bool
{
$stmt = $this->connection->prepare('
SELECT COUNT(*) FROM user_roles ur
INNER JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = ? AND r.name = ?
');
$stmt->execute([$userId, $role]);
return (int) $stmt->fetchColumn() > 0;
}
}
Authorization Helper¶
class AuthorizationService
{
public function __construct(
private UserRepository $userRepo
) {}
public function can(int $userId, string $permission): bool
{
$stmt = $this->connection->prepare('
SELECT COUNT(*) FROM permissions p
INNER JOIN role_permissions rp ON rp.permission_id = p.id
INNER JOIN user_roles ur ON ur.role_id = rp.role_id
WHERE ur.user_id = ? AND p.name = ?
');
$stmt->execute([$userId, $permission]);
return (int) $stmt->fetchColumn() > 0;
}
}
Session-Based Authentication¶
Session Service¶
class SessionService
{
public function start(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
public function get(string $key, mixed $default = null): mixed
{
return $_SESSION[$key] ?? $default;
}
public function has(string $key): bool
{
return isset($_SESSION[$key]);
}
public function remove(string $key): void
{
unset($_SESSION[$key]);
}
public function destroy(): void
{
session_destroy();
}
}
Complete Example¶
<?php
// Register services in bootstrap.php
$container->set(JwtService::class, function() {
return new JwtService(getenv('JWT_SECRET'));
});
$container->set(AuthService::class, function(Container $c) {
return new AuthService(
$c->get(UserRepository::class),
$c->get(JwtService::class)
);
});
// Usage in client
// POST /auth/register
{
"name": "Alice",
"email": "alice@example.com",
"password": "secret123"
}
// POST /auth/login
{
"email": "alice@example.com",
"password": "secret123"
}
// Response: { "token": "eyJ0eXAiOiJKV1QiLCJhbGc..." }
// GET /auth/me
// Headers: Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
// Response: { "id": 1, "name": "Alice", "email": "alice@example.com" }