Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/server/auth/handlers/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,16 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
let state;
try {
// Parse and validate authorization parameters
const parseResult = RequestAuthorizationParamsSchema.safeParse(req.method === 'POST' ? req.body : req.query);
const params = req.method === 'POST' ? req.body : req.query;
const parseResult = RequestAuthorizationParamsSchema.safeParse(params);
if (!parseResult.success) {
// RFC 6749 §4.1.2.1: if the request contained a state, error
// redirects MUST echo it so the client can correlate the
// response. Recover it from the raw params before failing.
const rawState = (params as { state?: unknown }).state;
if (typeof rawState === 'string') {
state = rawState;
}
throw new InvalidRequestError(parseResult.error.message);
}

Expand Down
34 changes: 34 additions & 0 deletions test/server/auth/handlers/authorize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,40 @@ describe('Authorization Handler', () => {
});
});

describe('State preservation on error redirects (RFC 6749 §4.1.2.1)', () => {
it('includes state in the error redirect when request parameters fail validation', async () => {
const response = await supertest(app).get('/authorize').query({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
code_challenge: 'challenge123',
code_challenge_method: 'plain', // invalid - only S256 is supported
state: 'csrf-state-42'
});

expect(response.status).toBe(302);
const location = new URL(response.header.location);
expect(location.searchParams.get('error')).toBe('invalid_request');
expect(location.searchParams.get('state')).toBe('csrf-state-42');
});

it('includes state in the error redirect for POST requests', async () => {
const response = await supertest(app).post('/authorize').type('form').send({
client_id: 'valid-client',
redirect_uri: 'https://example.com/callback',
response_type: 'code',
code_challenge_method: 'S256',
state: 'post-csrf-state-7'
// Missing code_challenge
});

expect(response.status).toBe(302);
const location = new URL(response.header.location);
expect(location.searchParams.get('error')).toBe('invalid_request');
expect(location.searchParams.get('state')).toBe('post-csrf-state-7');
});
});

describe('Resource parameter validation', () => {
it('propagates resource parameter', async () => {
const mockProviderWithResource = vi.spyOn(mockProvider, 'authorize');
Expand Down
Loading