From 81712ad3ce7517c8985f18efad1b8ecd26324158 Mon Sep 17 00:00:00 2001 From: Orvanix Date: Thu, 12 Mar 2026 17:15:07 +0000 Subject: [PATCH] fix(auth): send login token in POST body --- documentation/backend/services/auth.md | 3 ++- src/app/api/auth/token/login/route.ts | 4 ++-- src/app/auth/token/login/page.tsx | 8 ++++++-- tests/api/auth-login-token.routes.test.ts | 20 ++++++++++---------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/documentation/backend/services/auth.md b/documentation/backend/services/auth.md index 5011789..93c0aeb 100644 --- a/documentation/backend/services/auth.md +++ b/documentation/backend/services/auth.md @@ -253,7 +253,8 @@ oidc.admin_claim_value = 'readmeabook-admin' - Login token stored as SHA-256 hash in `User.loginTokenHash` - Admin generates/revokes via user permissions modal -- User login with token `/auth/token/login?token=rmab_...` +- User navigates to `/auth/token/login?token=rmab_...` → page POSTs token to API in request body +- API: `POST /api/auth/token/login` with `{ token }` in JSON body - Invalid token redirects to `/login` ## Security diff --git a/src/app/api/auth/token/login/route.ts b/src/app/api/auth/token/login/route.ts index ac952a3..f95d343 100644 --- a/src/app/api/auth/token/login/route.ts +++ b/src/app/api/auth/token/login/route.ts @@ -11,9 +11,9 @@ import crypto from 'crypto'; const logger = RMABLogger.create('API.Auth.TokenLogin'); -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { try { - const token = request.nextUrl.searchParams.get('token'); + const { token } = await request.json(); if (!token) { return NextResponse.json({ error: 'Missing token parameter' }, { status: 400 }); diff --git a/src/app/auth/token/login/page.tsx b/src/app/auth/token/login/page.tsx index 2aff00a..84b3ad9 100644 --- a/src/app/auth/token/login/page.tsx +++ b/src/app/auth/token/login/page.tsx @@ -22,7 +22,11 @@ function TokenLoginContent() { return; } - fetch(`/api/auth/token/login?token=${encodeURIComponent(token)}`) + fetch('/api/auth/token/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }), + }) .then((res) => res.json()) .then((data) => { if (data.error) { @@ -35,7 +39,7 @@ function TokenLoginContent() { localStorage.setItem('user', JSON.stringify(data.user)); setAuthData(data.user, data.accessToken); - window.location.href = '/'; + window.location.href = '/'; }) .catch(() => { router.replace('/login'); diff --git a/tests/api/auth-login-token.routes.test.ts b/tests/api/auth-login-token.routes.test.ts index 861aca2..c72a2eb 100644 --- a/tests/api/auth-login-token.routes.test.ts +++ b/tests/api/auth-login-token.routes.test.ts @@ -19,7 +19,7 @@ vi.mock('@/lib/utils/jwt', () => ({ generateRefreshToken: generateRefreshTokenMock, })); -describe('GET /api/auth/token/login', () => { +describe('POST /api/auth/token/login', () => { beforeEach(() => { vi.clearAllMocks(); generateAccessTokenMock.mockReturnValue('access-token'); @@ -37,9 +37,9 @@ describe('GET /api/auth/token/login', () => { }); prismaMock.user.update.mockResolvedValueOnce({}); - const { GET } = await import('@/app/api/auth/token/login/route'); - const request = { nextUrl: { searchParams: new URLSearchParams('token=rmab_valid_token') } }; - const response = await GET(request as any); + const { POST } = await import('@/app/api/auth/token/login/route'); + const request = { json: vi.fn().mockResolvedValue({ token: 'rmab_valid_token' }) }; + const response = await POST(request as any); const payload = await response.json(); expect(response.status).toBe(200); @@ -50,9 +50,9 @@ describe('GET /api/auth/token/login', () => { }); it('returns 400 when token parameter is missing', async () => { - const { GET } = await import('@/app/api/auth/token/login/route'); - const request = { nextUrl: { searchParams: new URLSearchParams() } }; - const response = await GET(request as any); + const { POST } = await import('@/app/api/auth/token/login/route'); + const request = { json: vi.fn().mockResolvedValue({}) }; + const response = await POST(request as any); const payload = await response.json(); expect(response.status).toBe(400); @@ -62,9 +62,9 @@ describe('GET /api/auth/token/login', () => { it('returns 401 when token is invalid or user not found', async () => { prismaMock.user.findFirst.mockResolvedValueOnce(null); - const { GET } = await import('@/app/api/auth/token/login/route'); - const request = { nextUrl: { searchParams: new URLSearchParams('token=rmab_invalid') } }; - const response = await GET(request as any); + const { POST } = await import('@/app/api/auth/token/login/route'); + const request = { json: vi.fn().mockResolvedValue({ token: 'rmab_invalid' }) }; + const response = await POST(request as any); const payload = await response.json(); expect(response.status).toBe(401);