User accounts & authorisation in MonkeysLegion
Posted by admin – July 21, 2025

From signup to querying “my data” with $request->getAttribute('user_id')
┌──────────┐ ┌─────────────────┐ ┌───────────────────────┐
│ Front-end│ ── (POST) ─▶ /auth/register │ │ Creates user record │
│ React │ └─────────────────┘ └───────────────────────┘
↓ │ │
│ │ (hash pwd, persist, emit events) │
│ ┌─────▼─────┐ │
│ │ User │ │
│ └───────────┘ │
┌─────────────────┐ │ │
│ /auth/login │ ◀─ (POST) ─┘ │
└─────────────────┘ │
│ (validate pwd) │
│ issue JWT: `{ sub: userId, exp … }` │
▼ │
localStorage.setItem('jwt', token) │
│ │
│ (GET /companies) │
├────────────────────────── Authorization: Bearer <token> ─┘
▼
JwtUserMiddleware
├─ decode token ➜ sub → user_id attr
├─ attach jwt_claims
└─ pass request down pipeline
▼
CompanyController::list()
$userId = $request->getAttribute('user_id')
…query company_user join-table…
JSON list of companies
2 Registration endpoint (/auth/register)
Route: POST /auth/register
Controller: UserController::register()
Behaviour
email and password are JSON-decoded from the request body.
AuthService::register() hashes the password, persists a new User (id ↗).
Returns 201 Created with the new user’s id + email.
Notes
No token is returned here—front-end usually redirects to login afterwards.
3 Login endpoint (/auth/login)
Route: POST /auth/login
Controller: UserController::login()
Behaviour
Credentials are validated via AuthService::login().
On success a signed JWT is issued:
{
"sub": 34, // user id
"iat": 1753064210,
"nbf": 1753064210,
"exp": 1753100810
}
Response: {"token":"<big-string>"}.
Front-end stores that token (e.g. localStorage.setItem('jwt', token)) and sends it on every subsequent request:
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc…
4 JwtUserMiddleware — extracting the user id
$claims = $this->jwt->decode($token);
$userId = (int) ($claims['sub'] ?? 0);
$request = $request
->withAttribute('jwt_claims', $claims)
->withAttribute('user_id', $userId);
Runs before your controllers.
Adds two attributes:
key | value |
---|---|
jwt_claims | full decoded payload (array) |
user_id | numeric user id (0 if missing/invalid) |
If the route is public (/auth/*, /health, …) the middleware still attaches these when a token exists, but will not reject if the token is absent.
5 Using $userId inside controllers
$userId = (int) $request->getAttribute('user_id', 0);
if ($userId <= 0) {
throw new RuntimeException('Unauthorized', 401);
}
A. Filtering data
$companyRepo = $repos->getRepository(Company::class);
$companies = $companyRepo->findByRelation('users', $userId);
Internally, that translates to:
SELECT c.*
FROM company AS c
JOIN company_user AS j ON j.company_id = c.id
WHERE j.user_id = :userId;
B. Authorisation & permissions
Because every request now carries user_id, you can:
Check ownership
if ($article->authorId !== $userId) {
throw new RuntimeException('Forbidden', 403);
}
Load roles once and cache
$roles = $request->getAttribute('jwt_claims')['roles'] ?? [];
if (!in_array('admin', $roles, true)) {
…
}
Build ACL queries in repositories (e.g. where("owner_id", '=', $userId)).
6 Creating new resources that belong to the user
// CompanyController::create()
// 1) persist the Company
$company = new Company();
$company->setName($name);
$companyRepo->save($company);
// 2) attach pivot row (company_id, user_id)
$companyRepo->attachRelation($company, 'users', $userId);
On the database side, that yields:
INSERT INTO company_user (company_id, user_id)
VALUES (:companyId, :userId);
The company instantly appears in subsequent GET /companies calls.
7 Front-end integration tips
Token storage
Use localStorage for simplicity or an httpOnly cookie for stronger XSS protection.
Fetch helper
function authHeaders() {
const t = localStorage.getItem('jwt');
return t ? { Authorization: `Bearer ${t}` } : {};
}
React query keys — include user.id so caches invalidate on login/logout.
Logout — localStorage.removeItem('jwt'); router.push('/login');.
8 Security recommendations
Rotate your JWT secret if compromised (forces re-login).
Short TTL + silent refresh endpoint (/auth/refresh) for long-lived sessions.
Always hash passwords with Argon2 or bcrypt inside AuthService.
Validate input (filter_var($email, FILTER_VALIDATE_EMAIL) etc.).
9 TL;DR
Register creates the user.
Login returns a signed JWT (sub = user_id).
JwtUserMiddleware decodes the token, attaches user_id.
Controllers read $request->getAttribute('user_id') to
fetch user-specific data (findByRelation)
enforce permissions
link new resources via attachRelation.
With that pattern, you get stateless, scalable authentication while keeping controller code clean and focused on business logic.