Auth Logout
Single-device token revocation with Moodle event forwarding — POST /api/v1/auth/logout
Overview — 5W
Token Revocation Endpoint
POST /api/v1/auth/logout— revokes the authenticated student's current Bearer token- Requires a valid Sanctum token (
auth:sanctummiddleware) - Returns HTTP 200 with a standard success envelope:
data: null - Dispatches
StudentLoggedOutevent (async → Moodle plugin)
Request Lifecycle Trigger
Triggered by any authenticated student calling POST /api/v1/auth/logout with a valid Bearer token. The auth:sanctum middleware resolves the token before the controller executes. The event is dispatched asynchronously via the queue after the token is deleted — it does not block the HTTP response.
Security & Audit Compliance
- Students on shared devices need a way to explicitly invalidate their session
- Compromised accounts can be secured immediately by revoking the active token
- Moodle's event system requires a
user_loggedoutevent for audit trails and activity reports - Prevents token persistence until natural expiry (the only prior safeguard)
Codebase Location
- Controller:
app/Modules/Auth/Controllers/LogoutController.php - Service:
app/Modules/Auth/Services/AuthenticationService.php - Event:
app/Modules/Auth/Events/StudentLoggedOut.php - Shared:
app/Shared/Http/Resources/ApiResource.php - Routes:
app/Modules/Auth/routes.php
End-to-End Mechanism
- Sanctum resolves
MoodleUserfrom the Authorization header and setscurrentAccessToken() AuthenticationService::logout()calls$user->currentAccessToken()->delete()— only the token from the current request is revoked; other device tokens remain intactEvent::dispatch(new StudentLoggedOut(...))queues the Moodle event asynchronously viaForwardEventToMoodlelistenerApiResource::success()returns the standard envelope withdata: null
Actors
- Students: call the endpoint with their active Bearer token to end their session
- Sanctum middleware: authenticates the token before the controller runs
- Queue worker: forwards the
StudentLoggedOutevent to Moodle asynchronously - Moodle plugin: receives
\core\event\user_loggedoutand logs it in Moodle's event system
Sequence Diagram
Authorization: Bearer {token} M->>DB: SELECT token WHERE token = hash(bearer) DB-->>M: PersonalAccessToken row M->>M: Set currentAccessToken on MoodleUser M->>+C: invoke(Request) C->>C: $user = $request->user() C->>+S: logout(MoodleUser $user) S->>+DB: currentAccessToken()->delete() DB-->>-S: token row deleted S->>Q: Event::dispatch(StudentLoggedOut) Note over Q: Async — does not block response S-->>-C: void C->>C: ApiResource::success('Logged out successfully.') C-->>-M: JsonResponse 200 M-->>-B: 200 { success: true, message: "Logged out successfully.", data: null } Q->>+MP: ForwardEventToMoodle listener
POST Moodle REST API Note over MP: user_loggedout recorded in Moodle event log MP-->>-Q: 200 ok
Flowchart
Files Changed
| File Path | Layer | Description |
|---|---|---|
| app/Modules/Auth/Controllers/LogoutController.php | Controller | Invokable controller for POST /api/v1/auth/logout. Resolves the authenticated MoodleUser from the request, delegates to AuthenticationService::logout(), and returns ApiResource::success(). |
| app/Modules/Auth/Events/StudentLoggedOut.php | Event | Domain event extending BaseEvent. Carries Moodle-compatible metadata: event name \core\event\user_loggedout, component core, action loggedout, target user, CrudType::Read, EduLevel::Other, ContextLevel::User. Context instance ID equals the user's ID. |
| app/Modules/Auth/Services/AuthenticationService.php | Service | Added logout(MoodleUser $user): void method. Revokes the current access token via currentAccessToken()->delete() and dispatches StudentLoggedOut event. |
| app/Modules/Auth/routes.php | Routes | Split into two middleware groups under v1/auth prefix: throttle:5,1 for login, and auth:sanctum for logout. Added POST logout → LogoutController named api.v1.auth.logout. |
| app/Shared/Http/Resources/ApiResource.php | Resource | Added static success(string $message, int $status = 200): JsonResponse. Returns the standard envelope with data: null, errors: null, code: null. Mirrors the existing error() static method pattern. |
| tests/Feature/Auth/LogoutTest.php | Test | Feature test suite (6 tests): 200 response shape, 401 when unauthenticated, token revocation verified in DB, revoked token returns 401 on reuse, StudentLoggedOut event dispatched, other device tokens not affected. |
| tests/Unit/Modules/Auth/Events/StudentLoggedOutTest.php | Test | Unit test suite (10 tests): verifies every abstract method implementation on StudentLoggedOut — event name, component, action, target, object table, CRUD type, edu level, context level, and context instance ID. |
| tests/Unit/Shared/Http/Resources/ApiResourceTest.php | Test | Added 2 tests for the new ApiResource::success() method: verifies null data payload and correct HTTP status code. |
| docs/openapi.yaml | Docs | Added POST /v1/auth/logout path with Bearer security requirement, 200 success response schema, and 401 unauthenticated response. |
| docs/postman_collection.json | Docs | Added Logout request to the Auth folder with Bearer {{token}} header, example 200 and 401 responses, and test script validating the envelope shape. |
Rules Applied
Thin Controller (5–15 lines)
LogoutController::__invoke() is 6 lines: resolve user, delegate to service, return response. Zero business logic. All token deletion and event dispatch live in AuthenticationService::logout().
Module Self-Containment
All Auth-specific files (LogoutController, StudentLoggedOut, route definition) reside under app/Modules/Auth/. Only the cross-cutting ApiResource::success() was added to app/Shared/ because it serves any module's no-data responses.
Domain Event System — BaseEvent
StudentLoggedOut extends BaseEvent and implements all abstract methods with Moodle-compatible values. Dispatched via Event::dispatch() (Laravel façade). The ForwardEventToMoodle listener runs on the queue — event forwarding does not block the HTTP response.
Standard API Response Envelope
Response follows the mandated shape: success, message, data, errors, code. No bespoke LogoutResource was created — ApiResource::success() fills this role for any operation that returns no resource data.
auth:sanctum Middleware — Authentication Required
The logout route is grouped under auth:sanctum. Requests without a valid Bearer token receive 401 before reaching the controller. No endpoint is public by default.
Single-Device Token Revocation
currentAccessToken()->delete() revokes only the token carried in the current request — other active sessions on other devices are unaffected. This is the narrowest possible revocation scope, protecting sessions the student did not intend to end.
final class + Constructor Injection
Both LogoutController and StudentLoggedOut are declared final. AuthenticationService is injected via constructor promotion into LogoutController. No app() or resolve() calls in business logic.
PHPDoc on Every Class and Public Method
All new classes and public methods carry brief PHPDoc blocks. The class docblock is one sentence describing responsibility. The logout() method docblock is one sentence with no redundant @param/@return (types are self-evident).
Enums for Fixed Value Sets
StudentLoggedOut returns CrudType::Read, EduLevel::Other, and ContextLevel::User — never raw strings or integer codes. Enums enforce type safety and prevent magic value drift across event implementations.
TDD Workflow — Red → Green → Refactor
Tests were written before implementation code for every component: ApiResourceTest (2 new tests), StudentLoggedOutTest (10 tests), and LogoutTest (6 feature tests). Implementation was the minimum code needed to make each test pass.
RefreshDatabase + Real DB (No Mocks for Eloquent)
Feature tests use the RefreshDatabase trait and hit a real SQLite test database. Token revocation and multi-token isolation are verified with assertDatabaseMissing/assertDatabaseHas. The Moodle REST call is mocked via Http::fake() per the rules.
100% Event Dispatch Coverage
test_it_dispatches_student_logged_out_event uses Event::fake() and Event::assertDispatched(StudentLoggedOut::class). All 10 abstract method implementations on the event are individually verified in StudentLoggedOutTest.