Blog

User accounts & authorisation in MonkeysLegion

Posted by adminJuly 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

    1. email and password are JSON-decoded from the request body.

    2. AuthService::register() hashes the password, persists a new User (id ↗).

    3. 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

    1. Credentials are validated via AuthService::login().

    2. On success a signed JWT is issued:

{
  "sub": 34,            // user id
  "iat": 1753064210,
  "nbf": 1753064210,
  "exp": 1753100810
}
  1. 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

  1. Token storage

    • Use localStorage for simplicity or an httpOnly cookie for stronger XSS protection.

  2. Fetch helper

function authHeaders() {
    const t = localStorage.getItem('jwt');
    return t ? { Authorization: `Bearer ${t}` } : {};
}
  1. React query keys — include user.id so caches invalidate on login/logout.

  2. LogoutlocalStorage.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

  1. Register creates the user.

  2. Login returns a signed JWT (sub = user_id).

  3. JwtUserMiddleware decodes the token, attaches user_id.

  4. 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.