mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 00:14:23 +00:00
fix: drop max_age=0 from OIDC re-authentication requests #938
This commit is contained in:
@@ -121,12 +121,13 @@ export async function POST(request: NextRequest) {
|
||||
authUrl.searchParams.set('ui_locales', locale);
|
||||
}
|
||||
|
||||
// Force a fresh credential entry for step-up re-auth. prompt=login and
|
||||
// max_age=0 both ask the IdP to re-authenticate even if it has an active
|
||||
// session; honoring them depends on the IdP supporting these OIDC params.
|
||||
// Force a fresh credential entry for step-up re-auth. prompt=login asks
|
||||
// the IdP to re-authenticate even if it has an active session; honoring it
|
||||
// depends on the IdP supporting this OIDC param. Deliberately no max_age=0:
|
||||
// some IdPs (Authelia) re-evaluate max_age at the consent step and loop
|
||||
// back to login forever, and nothing on our side reads auth_time.
|
||||
if (isReauth) {
|
||||
authUrl.searchParams.set('prompt', 'login');
|
||||
authUrl.searchParams.set('max_age', '0');
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('next/server', () => ({
|
||||
NextResponse: {
|
||||
json: (data: unknown, init?: { status?: number }) => ({
|
||||
json: async () => data,
|
||||
status: init?.status ?? 200,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const cookieSet = vi.fn();
|
||||
vi.mock('next/headers', () => ({
|
||||
cookies: async () => ({ set: cookieSet }),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/logger', () => ({
|
||||
logger: { warn: vi.fn(), error: vi.fn(), debug: vi.fn(), info: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/crypto', () => ({
|
||||
encryptPayload: () => 'encrypted',
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/oauth/pkce-server', () => ({
|
||||
generateCodeVerifierServer: () => 'verifier',
|
||||
generateCodeChallengeServer: () => 'challenge',
|
||||
generateStateServer: () => 'state-123',
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/oauth/token-exchange', () => ({
|
||||
getRequiredConfig: () => ({ clientId: 'client', discoveryUrl: 'https://idp.example.com' }),
|
||||
getDiscoveryValidator: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/oauth/discovery', () => ({
|
||||
discoverOAuth: async () => ({
|
||||
issuer: 'https://idp.example.com',
|
||||
authorization_endpoint: 'https://idp.example.com/authorize',
|
||||
token_endpoint: 'https://idp.example.com/token',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/oauth/tokens', () => ({
|
||||
getOauthScopes: () => 'openid email',
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/oauth/cookie-config', () => ({
|
||||
getCookieOptions: () => ({ httpOnly: true }),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/session-secret', () => ({
|
||||
hasSessionSecret: () => true,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/admin/config-manager', () => ({
|
||||
configManager: { get: () => '' },
|
||||
}));
|
||||
|
||||
function mockRequest(body: Record<string, unknown>): unknown {
|
||||
return {
|
||||
json: async () => body,
|
||||
headers: { get: (k: string) => (k === 'origin' ? 'https://mail.example.com' : null) },
|
||||
nextUrl: { origin: 'https://mail.example.com' },
|
||||
};
|
||||
}
|
||||
|
||||
async function callRoute(body: Record<string, unknown>) {
|
||||
const { POST } = await import('@/app/api/auth/sso/start/route');
|
||||
const res = (await POST(mockRequest(body) as Parameters<typeof POST>[0])) as unknown as {
|
||||
status: number;
|
||||
json: () => Promise<{ authorize_url: string }>;
|
||||
};
|
||||
return { status: res.status, body: await res.json() };
|
||||
}
|
||||
|
||||
describe('sso start route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('forces prompt=login without max_age for re-authentication', async () => {
|
||||
const { status, body } = await callRoute({
|
||||
redirect_uri: 'https://mail.example.com/auth/callback',
|
||||
purpose: 'reauth',
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
const url = new URL(body.authorize_url);
|
||||
expect(url.searchParams.get('prompt')).toBe('login');
|
||||
expect(url.searchParams.has('max_age')).toBe(false);
|
||||
});
|
||||
|
||||
it('sends neither prompt nor max_age for a regular login', async () => {
|
||||
const { status, body } = await callRoute({
|
||||
redirect_uri: 'https://mail.example.com/auth/callback',
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
const url = new URL(body.authorize_url);
|
||||
expect(url.searchParams.has('prompt')).toBe(false);
|
||||
expect(url.searchParams.has('max_age')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user