From f9b3428865db96f347892967298332bfbf38078e Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:12:38 +0200 Subject: [PATCH] Add DELETE /api/mobile/token so the app can revoke its token on logout --- app/Http/Controllers/MobileAuthController.php | 23 +++++++++++++ routes/api.php | 7 ++++ tests/Feature/Auth/MobileAuthTest.php | 32 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/app/Http/Controllers/MobileAuthController.php b/app/Http/Controllers/MobileAuthController.php index 9547ace..b60d918 100644 --- a/app/Http/Controllers/MobileAuthController.php +++ b/app/Http/Controllers/MobileAuthController.php @@ -15,6 +15,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use Laravel\Sanctum\NewAccessToken; +use Laravel\Sanctum\PersonalAccessToken; /** * Auth flow for the Einundzwanzig mobile app. @@ -125,6 +126,28 @@ final class MobileAuthController extends Controller ]); } + /** + * Revoke the personal access token that authenticated this request. + * + * Called by the mobile app on logout so the token does not linger + * server-side after the app has deleted it from the device keystore. + */ + public function revoke(Request $request): JsonResponse + { + $token = $request->user()->currentAccessToken(); + + if ($token instanceof PersonalAccessToken) { + $token->delete(); + + Log::info('Mobile app token revoked', [ + 'user_id' => $request->user()->id, + 'device_name' => $token->name, + ]); + } + + return response()->json(['status' => 'OK']); + } + /** * Headless Nostr launcher for the mobile app. * diff --git a/routes/api.php b/routes/api.php index 814255b..0b8d6af 100644 --- a/routes/api.php +++ b/routes/api.php @@ -96,5 +96,12 @@ Route::post('/mobile/token', [MobileAuthController::class, 'token']) ->middleware('throttle:30,1') ->name('auth.mobile.token'); +// Logout for the mobile app: revokes the personal access token that +// authenticated this request, so a local "disconnect" in the app also +// invalidates the token server-side. +Route::delete('/mobile/token', [MobileAuthController::class, 'revoke']) + ->middleware(['auth:sanctum', 'throttle:30,1']) + ->name('auth.mobile.token.revoke'); + Route::post('/check-auth-error', [LnurlAuthController::class, 'checkError']) ->name('auth.check-error'); diff --git a/tests/Feature/Auth/MobileAuthTest.php b/tests/Feature/Auth/MobileAuthTest.php index 3b81635..7088c0e 100644 --- a/tests/Feature/Auth/MobileAuthTest.php +++ b/tests/Feature/Auth/MobileAuthTest.php @@ -224,3 +224,35 @@ it('returns the token owner profile on /api/user', function () { it('denies /api/user without a token', function () { $this->getJson('/api/user')->assertUnauthorized(); }); + +it('revokes the requesting token on mobile logout', function () { + $user = User::factory()->create(); + $plainTextToken = $user->createToken('Pixel 10')->plainTextToken; + + $this->deleteJson('/api/mobile/token', [], ['Authorization' => 'Bearer '.$plainTextToken]) + ->assertOk() + ->assertJson(['status' => 'OK']); + + expect($user->tokens()->count())->toBe(0); + + // The revoked token no longer authenticates API requests. The guard + // caches the resolved user within a test, so reset it first. + $this->app['auth']->forgetGuards(); + $this->getJson('/api/user', ['Authorization' => 'Bearer '.$plainTextToken]) + ->assertUnauthorized(); +}); + +it('only revokes the token used for the logout request', function () { + $user = User::factory()->create(); + $phoneToken = $user->createToken('Pixel 10')->plainTextToken; + $user->createToken('Tablet'); + + $this->deleteJson('/api/mobile/token', [], ['Authorization' => 'Bearer '.$phoneToken]) + ->assertOk(); + + expect($user->tokens()->pluck('name')->all())->toBe(['Tablet']); +}); + +it('denies the mobile logout without a token', function () { + $this->deleteJson('/api/mobile/token')->assertUnauthorized(); +});