Back to KB
Difficulty
Intermediate
Read Time
122 min

Authentication with MonkeysLegion 2.0 + Next.js / React

By Codcompass TeamΒ·Β·122 min read

A complete guide to implementing JWT-based registration, login, session persistence, and token refresh using the MonkeysLegion v2 API with a Next.js (or React) frontend.


Table of Contents

  1. API Endpoints Overview
  2. Backend Setup (MonkeysLegion)
  3. Frontend: API Client
  4. Frontend: Auth Context Provider
  5. Frontend: Register Page
  6. Frontend: Login Page
  7. Frontend: Protected Routes
  8. Token Refresh Flow
  9. Common Issues & Solutions

API Endpoints Overview

MonkeysLegion v2 exposes the following auth endpoints under the /api/v2/auth prefix:

Method

Endpoint

Auth

Description

POST

/auth/register

No

Create a new user + company

POST

/auth/login

No

Authenticate and get JWT tokens

POST

/auth/refresh

No

Exchange refresh token for new access token

GET

/auth/me

Yes

Get authenticated user profile

POST

/auth/logout

Yes

Invalidate tokens

POST

/auth/forgot-password

No

Request password reset

Response Shapes

Login Response β€” POST /auth/login

{
  "data": {
    "access_token": "eyJ0eXAiOiJKV1Q...",
    "refresh_token": "eyJ0eXAiOiJKV1Q...",
    "token_type": "Bearer",
    "expires_in": 1800,
    "user": {
      "id": 1,
      "email": "user@example.com",
      "full_name": "John Doe"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Register Response β€” POST /auth/register (HTTP 201)

{
  "data": {
    "message": "Registration successful",
    "user": {
      "id": 1,
      "email": "user@example.com",
      "full_name": "John Doe"
    },
    "company": {
      "hash": "abc123",
      "name": "Acme Inc."
    },
    "access_token": "eyJ0eXAiOiJKV1Q...",
    "refresh_token": "eyJ0eXAiOiJKV1Q...",
    "token_type": "Bearer",
    "expires_in": 1800
  }
}

Enter fullscreen mode Exit fullscreen mode

Me Response β€” GET /auth/me

{
  "data": {
    "id": 1,
    "email": "user@example.com",
    "full_name": "John Doe",
    "phone": null,
    "timezone": "UTC",
    "status": "active",
    "verified": false,
    "two_factor": false,
    "created_at": "2026-05-08T01:25:23+00:00"
  }
}

Enter fullscreen mode Exit fullscreen mode


Backend Setup (MonkeysLegion)

1. Install the Framework

composer require monkeyscloud/monkeyslegion:^2.0.8

Enter fullscreen mode Exit fullscreen mode

Important: Version 2.0.8+ is required. Earlier versions have a bug in DatabaseUserProvider where typed properties (DateTimeImmutable, int, bool) throw TypeError during hydration from raw PDO strings.

2. Entity β€” app/Entity/User.php

<?php
declare(strict_types=1);

namespace App\Entity;

use MonkeysLegion\Auth\Contract\AuthenticatableInterface;
use MonkeysLegion\Query\Attribute\{Entity, Field, Id, Timestamps, Hidden, Fillable};

#[Entity(table: 'users')]
#[Timestamps]
class User implements AuthenticatableInterface
{
    #[Id]
    public private(set) int $id;

    #[Field(type: 'string', length: 255, unique: true)]
    #[Fillable]
    public string $email;

    #[Field(type: 'string', length: 255)]
    #[Fillable]
    public string $full_name;

    #[Field(type: 'string', length: 255)]
    #[Hidden]
    public string $password_hash;

    #[Field(type: 'integer', default: 1)]
    public int $token_version = 1;

    #[Field(type: 'datetime')]
    public private(set) \DateTimeImmutable $created_at;

    #[Field(type: 'datetime')]
    public private(set) \DateTimeImmutable $updated_at;

    // ── AuthenticatableInterface ──
    public function getAuthIdentifier(): int { return $this->id; }
    public function getAuthPassword(): string { return $this->password_hash; }
}

Enter fullscreen mode Exit fullscreen mode

3. Controller β€” app/Controller/Api/AuthController.php

<?php
declare(strict_types=1);

namespace App\Controller\Api;

use App\Service\AuthService;
use MonkeysLegion\Http\Attribute\{Route, Prefix, Throttle, Authenticated};
use MonkeysLegion\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;

#[Prefix('/api/v2/auth')]
class AuthController
{
    public function __construct(
        private readonly AuthService $auth,
    ) {}

    #[Route('POST', '/login', name: 'auth.login')]
    #[Throttle(max: 5, per: 60)]
    public function login(ServerRequestInterface $request): Response
    {
        $body = json_decode((string) $request->getBody(), true) ?? [];

        if (empty($body['email']) || empty($body['password'])) {
            return Response::json(['error' => 'Missing credentials'], 400);
        }

        $result = $this->auth->login($body['email'], $body['password']);

        if ($result === null) {
            return Response::json(['error' => 'Invalid credentials'], 401);
        }

        return Response::json([
            'data' => [
                'access_token'  => $result['access_token'],
                'refresh_token' => $result['refresh_token'],
                'token_type'    => 'Bearer',
                'expires_in'    => $result['expires_in'],
                'user' => [
                    'id'        => $result['user']->id,
                    'email'     => $result['user']->email,
                    'full_name' => $result['user']->full_name,
                ],
            ],
        ]);
    }

    #[Route('POST', '/register', name: 'auth.register')]
    #[Throttle(max: 3, per: 60)]
    public function register(ServerRequestInterface $request): Response
    {
        $body = json_decode((string) $request->getBody(), true) ?? [];

        if (empty($body['email']) || empty($body[

πŸŽ‰ Mid-Year Sale β€” Unlock Full Article

Base plan from just $4.99/mo or $49/yr

Sign in to read the full article and unlock all 635+ tutorials.

Sign In / Register β€” Start Free Trial

7-day free trial Β· Cancel anytime Β· 30-day money-back