Skip to content

Commit ce37ce6

Browse files
committed
improvments:
- Adjust secure-cookie detection to be proxy/request driven and correctly parse `x-forwarded-proto`, avoiding `NODE_ENV` forcing secure cookies. - Adopt Bearer-prefix stripping when setting the auth cookie - Added Cache-Control: no-store headers to prevent stale auth responses: - Treat tokens that fail JWT decoding as unauthenticated - Avoid extra domain fetch when RBAC is off
1 parent d1ec57c commit ce37ce6

File tree

5 files changed

+67
-12
lines changed

5 files changed

+67
-12
lines changed

src/app/api/auth/me/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ import {
77

88
export async function GET(request: NextRequest) {
99
const authContext = await resolveAuthContext(request.cookies);
10-
return NextResponse.json(getPublicAuthContext(authContext));
10+
return NextResponse.json(getPublicAuthContext(authContext), {
11+
headers: { 'Cache-Control': 'no-store' },
12+
});
1113
}

src/app/api/auth/token/route.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,57 @@ import { CADENCE_AUTH_COOKIE_NAME } from '@/utils/auth/auth-context';
44

55
const COOKIE_OPTIONS = {
66
httpOnly: true,
7-
secure: process.env.NODE_ENV !== 'development',
87
sameSite: 'lax' as const,
98
path: '/',
109
};
1110

11+
function getCookieSecureAttribute(request: NextRequest) {
12+
const xfProto = request.headers.get('x-forwarded-proto');
13+
const proto = xfProto?.split(',')[0]?.trim().toLowerCase();
14+
if (proto) return proto === 'https';
15+
return request.nextUrl.protocol === 'https:';
16+
}
17+
1218
export async function POST(request: NextRequest) {
1319
try {
1420
const body = await request.json();
1521
if (!body?.token || typeof body.token !== 'string') {
1622
return NextResponse.json(
1723
{ message: 'A valid token is required' },
18-
{ status: 400 }
24+
{ status: 400, headers: { 'Cache-Control': 'no-store' } }
25+
);
26+
}
27+
28+
const normalizedToken = body.token.trim().replace(/^bearer\s+/i, '');
29+
if (!normalizedToken) {
30+
return NextResponse.json(
31+
{ message: 'A valid token is required' },
32+
{ status: 400, headers: { 'Cache-Control': 'no-store' } }
1933
);
2034
}
2135

2236
const response = NextResponse.json({ ok: true });
23-
response.cookies.set(CADENCE_AUTH_COOKIE_NAME, body.token, COOKIE_OPTIONS);
37+
response.headers.set('Cache-Control', 'no-store');
38+
response.cookies.set(CADENCE_AUTH_COOKIE_NAME, normalizedToken, {
39+
...COOKIE_OPTIONS,
40+
secure: getCookieSecureAttribute(request),
41+
});
2442
return response;
2543
} catch {
2644
return NextResponse.json(
2745
{ message: 'Invalid request body' },
28-
{ status: 400 }
46+
{ status: 400, headers: { 'Cache-Control': 'no-store' } }
2947
);
3048
}
3149
}
3250

33-
export async function DELETE() {
51+
export async function DELETE(request: NextRequest) {
3452
const response = NextResponse.json({ ok: true });
53+
response.headers.set('Cache-Control', 'no-store');
3554
response.cookies.set(CADENCE_AUTH_COOKIE_NAME, '', {
3655
...COOKIE_OPTIONS,
56+
secure: getCookieSecureAttribute(request),
57+
expires: new Date(0),
3758
maxAge: 0,
3859
});
3960
return response;

src/hooks/use-domain-access/use-domain-access.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import useUserInfo from '../use-user-info/use-user-info';
1111

1212
export default function useDomainAccess(params: UseDomainDescriptionParams) {
1313
const userInfoQuery = useUserInfo();
14-
const shouldLoadDomain = Boolean(userInfoQuery.data);
14+
const isRbacEnabled = userInfoQuery.data?.rbacEnabled === true;
1515

1616
const domainQuery = useQuery({
1717
...getDomainDescriptionQueryOptions(params),
18-
enabled: shouldLoadDomain,
18+
enabled: isRbacEnabled,
1919
});
2020

2121
const access = useMemo(() => {
@@ -27,6 +27,10 @@ export default function useDomainAccess(params: UseDomainDescriptionParams) {
2727
return undefined;
2828
}
2929

30+
if (!userInfoQuery.data.rbacEnabled) {
31+
return { canRead: true, canWrite: true };
32+
}
33+
3034
if (domainQuery.data) {
3135
return getDomainAccessForUser(domainQuery.data, userInfoQuery.data);
3236
}
@@ -44,7 +48,7 @@ export default function useDomainAccess(params: UseDomainDescriptionParams) {
4448
]);
4549

4650
const isLoading =
47-
userInfoQuery.isLoading || (shouldLoadDomain && domainQuery.isLoading);
51+
userInfoQuery.isLoading || (isRbacEnabled && domainQuery.isLoading);
4852

4953
return {
5054
access,

src/utils/auth/__tests__/auth-context.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const buildToken = (claims: Record<string, unknown>) => {
2020
return ['header', payload, 'signature'].join('.');
2121
};
2222

23+
const buildTokenWithNonJsonPayload = (payloadText: string) => {
24+
const payload = Buffer.from(payloadText).toString('base64url');
25+
return ['header', payload, 'signature'].join('.');
26+
};
27+
2328
describe('auth-context utilities', () => {
2429
beforeEach(() => {
2530
jest.clearAllMocks();
@@ -85,6 +90,27 @@ describe('auth-context utilities', () => {
8590
});
8691
});
8792

93+
it('treats undecodable tokens as unauthenticated', async () => {
94+
const token = buildTokenWithNonJsonPayload('not-json');
95+
mockGetConfigValue.mockImplementation(async (key: string) => {
96+
if (key === 'CADENCE_WEB_RBAC_ENABLED') return 'true';
97+
return '';
98+
});
99+
100+
const authContext = await resolveAuthContext({
101+
get: (name: string) =>
102+
name === CADENCE_AUTH_COOKIE_NAME ? { value: token } : undefined,
103+
});
104+
105+
expect(authContext).toMatchObject({
106+
rbacEnabled: true,
107+
token: undefined,
108+
groups: [],
109+
isAdmin: false,
110+
userName: undefined,
111+
});
112+
});
113+
88114
it('treats expired tokens as unauthenticated', async () => {
89115
const nowMs = 1_700_000_000_000;
90116
const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);

src/utils/auth/auth-context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@ export async function resolveAuthContext(
5656
const token = tokenFromCookie || undefined;
5757

5858
const claims = token ? decodeCadenceJwtClaims(token) : undefined;
59+
const isInvalidToken = token !== undefined && claims === undefined;
5960
const expiresAtMsRaw =
6061
typeof claims?.exp === 'number' ? claims.exp * 1000 : undefined;
6162
const isExpired =
6263
expiresAtMsRaw !== undefined && Date.now() >= expiresAtMsRaw;
63-
const effectiveClaims = isExpired ? undefined : claims;
64-
const expiresAtMs = isExpired ? undefined : expiresAtMsRaw;
65-
const effectiveToken = isExpired ? undefined : token;
64+
const shouldDropToken = isInvalidToken || isExpired;
65+
const effectiveClaims = shouldDropToken ? undefined : claims;
66+
const expiresAtMs = shouldDropToken ? undefined : expiresAtMsRaw;
67+
const effectiveToken = shouldDropToken ? undefined : token;
6668

6769
const normalizeGroups = (): string[] => {
6870
const raw = effectiveClaims?.groups ?? effectiveClaims?.Groups;

0 commit comments

Comments
 (0)