diff --git a/apps/backend/src/__tests__/auth.validation.test.ts b/apps/backend/src/__tests__/auth.validation.test.ts new file mode 100644 index 00000000..03e3de01 --- /dev/null +++ b/apps/backend/src/__tests__/auth.validation.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; + +import { mobileExchangeSchema, refreshTokenSchema } from '../validations/auth.validation'; + +describe('auth.validation', () => { + describe('mobileExchangeSchema', () => { + it('accepts a valid UUID code', () => { + const result = mobileExchangeSchema.safeParse({ + code: '550e8400-e29b-41d4-a716-446655440000', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.code).toBe('550e8400-e29b-41d4-a716-446655440000'); + } + }); + + it('rejects a non-UUID string', () => { + const result = mobileExchangeSchema.safeParse({ + code: 'not-a-uuid', + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.flatten().fieldErrors.code).toBeDefined(); + } + }); + + it('rejects missing code', () => { + const result = mobileExchangeSchema.safeParse({}); + expect(result.success).toBe(false); + }); + }); + + describe('refreshTokenSchema', () => { + it('accepts a valid refresh_token string', () => { + const result = refreshTokenSchema.safeParse({ + refresh_token: 'some-refresh-token', + }); + expect(result.success).toBe(true); + }); + + it('accepts an empty body (refresh_token is optional)', () => { + const result = refreshTokenSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.refresh_token).toBeUndefined(); + } + }); + + it('rejects a non-string refresh_token', () => { + const result = refreshTokenSchema.safeParse({ + refresh_token: 123, + }); + expect(result.success).toBe(false); + }); + }); +}); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index 3bc39ad4..b86c448d 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -1,11 +1,13 @@ import { handleDbError, isGitHubTokenError, isGoogleTokenError } from '../utils/error.util.js'; -import { extractRawJwt, blocklistKey, signAccessToken } from '../utils/jwt.js'; +import { extractRawJwt, blocklistKey, signAccessToken } from '../utils/jwt.js'; import { buildOAuthState, getMobileRedirectUri } from '../utils/oauth.js'; import { generateRefreshToken, hashIp, hashRefreshToken } from '../utils/refreshToken.js'; +import { mobileExchangeSchema, refreshTokenSchema } from '../validations/auth.validation'; import type { GitHubTokenErrorResponse, GitHubTokenResponse } from '../utils/error.util.js'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + interface GitHubEmailResponse { email: string; primary: boolean; @@ -495,7 +497,15 @@ export async function authRoutes(app: FastifyInstance): Promise { }); app.post('/refresh', async(request: FastifyRequest, reply: FastifyReply) => { - const refreshToken = request.cookies.refresh_token ?? (request.body as { refresh_token?: string })?.refresh_token; + // Validate request body with Zod + const bodyParsed = refreshTokenSchema.safeParse(request.body); + if (!bodyParsed.success) { + return reply.status(400).send({ + error: 'Invalid request body', + issues: bodyParsed.error.flatten().fieldErrors, + }); + } + const refreshToken = request.cookies.refresh_token ?? bodyParsed.data.refresh_token; if (!refreshToken) { return reply.status(401).send({ @@ -599,8 +609,15 @@ export async function authRoutes(app: FastifyInstance): Promise { }) - app.post('/mobile/exchange', async (request: FastifyRequest<{Body: {code: string}}>, reply: FastifyReply) => { - const { code } = request.body; + app.post('/mobile/exchange', async (request: FastifyRequest, reply: FastifyReply) => { + const parsed = mobileExchangeSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ + error: 'Invalid request body', + issues: parsed.error.flatten().fieldErrors, + }); + } + const { code } = parsed.data; const raw = await app.redis.getdel(`mobile_exchange:${code}`); if (!raw) {return reply.status(400).send({ error: 'Invalid or expired exchange code' });} diff --git a/apps/backend/src/validations/auth.validation.ts b/apps/backend/src/validations/auth.validation.ts new file mode 100644 index 00000000..c10294c5 --- /dev/null +++ b/apps/backend/src/validations/auth.validation.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const mobileExchangeSchema = z.object({ + code: z + .string({ message: 'Exchange code is required' }) + .uuid({ message: 'Exchange code must be a valid UUID' }), +}); + +export const refreshTokenSchema = z.object({ + refresh_token: z + .string({ message: 'Refresh token must be a string' }) + .min(1, 'Refresh token cannot be empty') + .optional(), +});