Skip to content

Commit 183e382

Browse files
authored
chore(backend): Add getAuth() type helper tests (#6079)
1 parent 19e9e11 commit 183e382

File tree

9 files changed

+186
-37
lines changed

9 files changed

+186
-37
lines changed

.changeset/social-carrots-melt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/backend': patch
3+
'@clerk/nextjs': patch
4+
---
5+
6+
Resolve machine token property mixing in discriminated unions

packages/backend/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,4 @@ export type {
162162
* Auth objects
163163
*/
164164
export type { AuthObject } from './tokens/authObjects';
165+
export type { SessionAuthObject, MachineAuthObject } from './tokens/types';

packages/backend/src/internal.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ export type {
1212
OrganizationSyncOptions,
1313
InferAuthObjectFromToken,
1414
InferAuthObjectFromTokenArray,
15-
SessionAuthObject,
16-
MachineAuthObject,
1715
GetAuthFn,
1816
} from './tokens/types';
1917

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { assertType, test } from 'vitest';
2+
3+
import type { AuthObject } from '../authObjects';
4+
import type { GetAuthFn, MachineAuthObject, SessionAuthObject } from '../types';
5+
6+
// Across our SDKs, we have a getAuth() function
7+
const getAuth: GetAuthFn<Request> = (_request: any, _options: any) => {
8+
return {} as any;
9+
};
10+
11+
test('infers the correct AuthObject type for each accepted token type', () => {
12+
const request = new Request('https://example.com');
13+
14+
// Session token by default
15+
assertType<SessionAuthObject>(getAuth(request));
16+
17+
// Individual token types
18+
assertType<SessionAuthObject>(getAuth(request, { acceptsToken: 'session_token' }));
19+
assertType<MachineAuthObject<'api_key'>>(getAuth(request, { acceptsToken: 'api_key' }));
20+
assertType<MachineAuthObject<'machine_token'>>(getAuth(request, { acceptsToken: 'machine_token' }));
21+
assertType<MachineAuthObject<'oauth_token'>>(getAuth(request, { acceptsToken: 'oauth_token' }));
22+
23+
// Array of token types
24+
assertType<SessionAuthObject | MachineAuthObject<'machine_token'>>(
25+
getAuth(request, { acceptsToken: ['session_token', 'machine_token'] }),
26+
);
27+
assertType<MachineAuthObject<'machine_token' | 'oauth_token'>>(
28+
getAuth(request, { acceptsToken: ['machine_token', 'oauth_token'] }),
29+
);
30+
31+
// Any token type
32+
assertType<AuthObject>(getAuth(request, { acceptsToken: 'any' }));
33+
});
34+
35+
test('verifies correct properties exist for each token type', () => {
36+
const request = new Request('https://example.com');
37+
38+
// Session token should have userId
39+
const sessionAuth = getAuth(request, { acceptsToken: 'session_token' });
40+
assertType<string | null>(sessionAuth.userId);
41+
42+
// All machine tokens should have id and subject
43+
const apiKeyAuth = getAuth(request, { acceptsToken: 'api_key' });
44+
const machineTokenAuth = getAuth(request, { acceptsToken: 'machine_token' });
45+
const oauthTokenAuth = getAuth(request, { acceptsToken: 'oauth_token' });
46+
47+
assertType<string | null>(apiKeyAuth.id);
48+
assertType<string | null>(machineTokenAuth.id);
49+
assertType<string | null>(oauthTokenAuth.id);
50+
assertType<string | null>(apiKeyAuth.subject);
51+
assertType<string | null>(machineTokenAuth.subject);
52+
assertType<string | null>(oauthTokenAuth.subject);
53+
54+
// Only api_key and machine_token should have name and claims
55+
assertType<string | null>(apiKeyAuth.name);
56+
assertType<Record<string, any> | null>(apiKeyAuth.claims);
57+
58+
assertType<string | null>(machineTokenAuth.name);
59+
assertType<Record<string, any> | null>(machineTokenAuth.claims);
60+
61+
// oauth_token should NOT have name and claims
62+
// @ts-expect-error oauth_token does not have name property
63+
void oauthTokenAuth.name;
64+
// @ts-expect-error oauth_token does not have claims property
65+
void oauthTokenAuth.claims;
66+
});
67+
68+
test('verifies discriminated union works correctly with acceptsToken: any', () => {
69+
const request = new Request('https://example.com');
70+
71+
const auth = getAuth(request, { acceptsToken: 'any' });
72+
73+
if (auth.tokenType === 'session_token') {
74+
// Should be SessionAuthObject - has userId
75+
assertType<string | null>(auth.userId);
76+
// Should NOT have machine token properties
77+
// @ts-expect-error session_token does not have id property
78+
void auth.id;
79+
} else if (auth.tokenType === 'api_key') {
80+
// Should be AuthenticatedMachineObject<'api_key'> - has id, name, claims
81+
assertType<string | null>(auth.id);
82+
assertType<string | null>(auth.name);
83+
assertType<Record<string, any> | null>(auth.claims);
84+
// Should NOT have session token properties
85+
// @ts-expect-error api_key does not have userId property
86+
void auth.userId;
87+
} else if (auth.tokenType === 'machine_token') {
88+
// Should be AuthenticatedMachineObject<'machine_token'> - has id, name, claims
89+
assertType<string | null>(auth.id);
90+
assertType<string | null>(auth.name);
91+
assertType<Record<string, any> | null>(auth.claims);
92+
// Should NOT have session token properties
93+
// @ts-expect-error machine_token does not have userId property
94+
void auth.userId;
95+
} else if (auth.tokenType === 'oauth_token') {
96+
// Should be AuthenticatedMachineObject<'oauth_token'> - has id but NOT name/claims
97+
assertType<string | null>(auth.id);
98+
// Should NOT have name or claims
99+
// @ts-expect-error oauth_token does not have name property
100+
void auth.name;
101+
// @ts-expect-error oauth_token does not have claims property
102+
void auth.claims;
103+
// Should NOT have session token properties
104+
// @ts-expect-error oauth_token does not have userId property
105+
void auth.userId;
106+
}
107+
});

packages/backend/src/tokens/authObjects.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -104,29 +104,43 @@ type MachineObjectExtendedProperties<TAuthenticated extends boolean> = {
104104

105105
/**
106106
* @internal
107+
*
108+
* Uses `T extends any` to create a distributive conditional type.
109+
* This ensures that union types like `'api_key' | 'oauth_token'` are processed
110+
* individually, creating proper discriminated unions where each token type
111+
* gets its own distinct properties (e.g., oauth_token won't have claims).
107112
*/
108-
export type AuthenticatedMachineObject<T extends MachineTokenType = MachineTokenType> = {
109-
id: string;
110-
subject: string;
111-
scopes: string[];
112-
getToken: () => Promise<string>;
113-
has: CheckAuthorizationFromSessionClaims;
114-
debug: AuthObjectDebug;
115-
tokenType: T;
116-
} & MachineObjectExtendedProperties<true>[T];
113+
export type AuthenticatedMachineObject<T extends MachineTokenType = MachineTokenType> = T extends any
114+
? {
115+
id: string;
116+
subject: string;
117+
scopes: string[];
118+
getToken: () => Promise<string>;
119+
has: CheckAuthorizationFromSessionClaims;
120+
debug: AuthObjectDebug;
121+
tokenType: T;
122+
} & MachineObjectExtendedProperties<true>[T]
123+
: never;
117124

118125
/**
119126
* @internal
127+
*
128+
* Uses `T extends any` to create a distributive conditional type.
129+
* This ensures that union types like `'api_key' | 'oauth_token'` are processed
130+
* individually, creating proper discriminated unions where each token type
131+
* gets its own distinct properties (e.g., oauth_token won't have claims).
120132
*/
121-
export type UnauthenticatedMachineObject<T extends MachineTokenType = MachineTokenType> = {
122-
id: null;
123-
subject: null;
124-
scopes: null;
125-
getToken: () => Promise<null>;
126-
has: CheckAuthorizationFromSessionClaims;
127-
debug: AuthObjectDebug;
128-
tokenType: T;
129-
} & MachineObjectExtendedProperties<false>[T];
133+
export type UnauthenticatedMachineObject<T extends MachineTokenType = MachineTokenType> = T extends any
134+
? {
135+
id: null;
136+
subject: null;
137+
scopes: null;
138+
getToken: () => Promise<null>;
139+
has: CheckAuthorizationFromSessionClaims;
140+
debug: AuthObjectDebug;
141+
tokenType: T;
142+
} & MachineObjectExtendedProperties<false>[T]
143+
: never;
130144

131145
/**
132146
* @interface
@@ -243,7 +257,7 @@ export function authenticatedMachineObject<T extends MachineTokenType>(
243257
name: result.name,
244258
claims: result.claims,
245259
scopes: result.scopes,
246-
};
260+
} as unknown as AuthenticatedMachineObject<T>;
247261
}
248262
case TokenType.MachineToken: {
249263
const result = verificationResult as MachineToken;
@@ -253,7 +267,7 @@ export function authenticatedMachineObject<T extends MachineTokenType>(
253267
name: result.name,
254268
claims: result.claims,
255269
scopes: result.scopes,
256-
};
270+
} as unknown as AuthenticatedMachineObject<T>;
257271
}
258272
case TokenType.OAuthToken: {
259273
return {
@@ -290,15 +304,15 @@ export function unauthenticatedMachineObject<T extends MachineTokenType>(
290304
tokenType,
291305
name: null,
292306
claims: null,
293-
};
307+
} as unknown as UnauthenticatedMachineObject<T>;
294308
}
295309
case TokenType.MachineToken: {
296310
return {
297311
...baseObject,
298312
tokenType,
299313
name: null,
300314
claims: null,
301-
};
315+
} as unknown as UnauthenticatedMachineObject<T>;
302316
}
303317
case TokenType.OAuthToken: {
304318
return {

packages/backend/src/tokens/types.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ export type InferAuthObjectFromTokenArray<
163163
> = SessionTokenType extends T[number]
164164
? T[number] extends SessionTokenType
165165
? SessionType
166-
: SessionType | (MachineType & { tokenType: T[number] })
167-
: MachineType & { tokenType: T[number] };
166+
: SessionType | (MachineType & { tokenType: Exclude<T[number], SessionTokenType> })
167+
: MachineType & { tokenType: Exclude<T[number], SessionTokenType> };
168168

169169
/**
170170
* Infers auth object type from a single token type.
@@ -174,12 +174,12 @@ export type InferAuthObjectFromToken<
174174
T extends TokenType,
175175
SessionType extends AuthObject,
176176
MachineType extends AuthObject,
177-
> = T extends SessionTokenType ? SessionType : MachineType & { tokenType: T };
177+
> = T extends SessionTokenType ? SessionType : MachineType & { tokenType: Exclude<T, SessionTokenType> };
178178

179179
export type SessionAuthObject = SignedInAuthObject | SignedOutAuthObject;
180-
export type MachineAuthObject<T extends TokenType> = (AuthenticatedMachineObject | UnauthenticatedMachineObject) & {
181-
tokenType: T;
182-
};
180+
export type MachineAuthObject<T extends Exclude<TokenType, SessionTokenType>> = T extends any
181+
? AuthenticatedMachineObject<T> | UnauthenticatedMachineObject<T>
182+
: never;
183183

184184
type AuthOptions = PendingSessionOptions & { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };
185185

@@ -199,7 +199,10 @@ export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false>
199199
<T extends TokenType[]>(
200200
req: RequestType,
201201
options: AuthOptions & { acceptsToken: T },
202-
): MaybePromise<InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<T[number]>>, ReturnsPromise>;
202+
): MaybePromise<
203+
InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<Exclude<T[number], SessionTokenType>>>,
204+
ReturnsPromise
205+
>;
203206

204207
/**
205208
* @example
@@ -208,7 +211,10 @@ export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false>
208211
<T extends TokenType>(
209212
req: RequestType,
210213
options: AuthOptions & { acceptsToken: T },
211-
): MaybePromise<InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<T>>, ReturnsPromise>;
214+
): MaybePromise<
215+
InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<Exclude<T, SessionTokenType>>>,
216+
ReturnsPromise
217+
>;
212218

213219
/**
214220
* @example

packages/backend/vitest.config.mts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { defineConfig } from 'vitest/config';
33
export default defineConfig({
44
plugins: [],
55
test: {
6+
typecheck: {
7+
enabled: true,
8+
include: ['**/*.test.ts'],
9+
},
610
coverage: {
711
provider: 'v8',
812
},

packages/nextjs/src/app-router/server/auth.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import type { AuthObject } from '@clerk/backend';
1+
import type { AuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
22
import type {
33
AuthenticateRequestOptions,
44
InferAuthObjectFromToken,
55
InferAuthObjectFromTokenArray,
6-
MachineAuthObject,
76
RedirectFun,
8-
SessionAuthObject,
7+
SessionTokenType,
98
} from '@clerk/backend/internal';
109
import { constants, createClerkRequest, createRedirect, TokenType } from '@clerk/backend/internal';
1110
import type { PendingSessionOptions } from '@clerk/types';
@@ -56,15 +55,23 @@ export interface AuthFn<TRedirect = ReturnType<typeof redirect>> {
5655
*/
5756
<T extends TokenType[]>(
5857
options: AuthOptions & { acceptsToken: T },
59-
): Promise<InferAuthObjectFromTokenArray<T, SessionAuthWithRedirect<TRedirect>, MachineAuthObject<T[number]>>>;
58+
): Promise<
59+
InferAuthObjectFromTokenArray<
60+
T,
61+
SessionAuthWithRedirect<TRedirect>,
62+
MachineAuthObject<Exclude<T[number], SessionTokenType>>
63+
>
64+
>;
6065

6166
/**
6267
* @example
6368
* const authObject = await auth({ acceptsToken: 'session_token' })
6469
*/
6570
<T extends TokenType>(
6671
options: AuthOptions & { acceptsToken: T },
67-
): Promise<InferAuthObjectFromToken<T, SessionAuthWithRedirect<TRedirect>, MachineAuthObject<T>>>;
72+
): Promise<
73+
InferAuthObjectFromToken<T, SessionAuthWithRedirect<TRedirect>, MachineAuthObject<Exclude<T, SessionTokenType>>>
74+
>;
6875

6976
/**
7077
* @example

packages/nextjs/src/server/protect.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ export interface AuthProtect {
7373
options?: AuthProtectOptions & { token: T },
7474
): Promise<InferAuthObjectFromTokenArray<T, SignedInAuthObject, AuthenticatedMachineObject>>;
7575

76+
/**
77+
* @example
78+
* auth.protect({ token: 'any' });
79+
*/
80+
(options?: AuthProtectOptions & { token: 'any' }): Promise<SignedInAuthObject | AuthenticatedMachineObject>;
81+
7682
/**
7783
* @example
7884
* auth.protect();

0 commit comments

Comments
 (0)