Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/twenty-beds-serve.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

- Optimize `auth()` calls to avoid unnecessary verification calls when the provided token type is not in the `acceptsToken` array.
- Add handling for invalid token types when `acceptsToken` is an array in `authenticateRequest()`: now returns a clear unauthenticated state (`tokenType: null`) if the token is not in the accepted list.

2 changes: 1 addition & 1 deletion packages/backend/src/tokens/__tests__/request.test-d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,7 +26,7 @@ test('returns the correct `authenticateRequest()` return type for each accepted
// Array of token types
expectTypeOf(
authenticateRequest(request, { acceptsToken: ['session_token', 'api_key', 'machine_token'] }),
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token'>>>();
).toMatchTypeOf<Promise<RequestState<'session_token' | 'api_key' | 'machine_token' | null>>>();

// Any token type
expectTypeOf(authenticateRequest(request, { acceptsToken: 'any' })).toMatchTypeOf<Promise<RequestState<TokenType>>>();
Expand Down
26 changes: 15 additions & 11 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import type { AuthReason } from '../authStatus';
import { AuthErrorReason, AuthStatus } from '../authStatus';
import { OrganizationMatcher } from '../organizationMatcher';
import { authenticateRequest, RefreshTokenErrorReason } from '../request';
import type { MachineTokenType } from '../tokenTypes';
import { type MachineTokenType, TokenType } from '../tokenTypes';
import type { AuthenticateRequestOptions } from '../types';

const PK_TEST = 'pk_test_Y2xlcmsuaW5zcGlyZWQucHVtYS03NC5sY2wuZGV2JA';
Expand DownExpand Up@@ -236,7 +236,7 @@ expect.extend({
toBeMachineUnauthenticated(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
reason: AuthReason;
message: string;
},
Expand All@@ -246,6 +246,7 @@ expect.extend({
received.tokenType === expected.tokenType &&
received.reason === expected.reason &&
received.message === expected.message &&
!received.isAuthenticated &&
!received.token;

if (pass) {
Expand All@@ -264,15 +265,11 @@ expect.extend({
toBeMachineUnauthenticatedToAuth(
received,
expected: {
tokenType: MachineTokenType;
tokenType: MachineTokenType | null;
},
) {
const pass =
received.tokenType === expected.tokenType &&
!received.claims &&
!received.subject &&
!received.name &&
!received.id;
received.tokenType === expected.tokenType && !received.isAuthenticated && !received.name && !received.id;

if (pass) {
return {
Expand DownExpand Up@@ -1203,7 +1200,7 @@ describe('tokens.authenticateRequest(options)', () => {
});

// Test each token type with parameterized tests
const tokenTypes = ['api_key', 'oauth_token', 'machine_token'] as const;
const tokenTypes = [TokenType.ApiKey, TokenType.OAuthToken, TokenType.MachineToken];

describe.each(tokenTypes)('%s Authentication', tokenType => {
const mockToken = mockTokens[tokenType];
Expand DownExpand Up@@ -1240,6 +1237,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType,
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1289,6 +1287,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'api_key',
isAuthenticated: false,
});
});

Expand All@@ -1303,6 +1302,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'oauth_token',
isAuthenticated: false,
});
});

Expand All@@ -1317,6 +1317,7 @@ describe('tokens.authenticateRequest(options)', () => {
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});

Expand All@@ -1328,9 +1329,11 @@ describe('tokens.authenticateRequest(options)', () => {
tokenType: 'machine_token',
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
isAuthenticated: false,
});
expect(result.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
isAuthenticated: false,
});
});
});
Expand DownExpand Up@@ -1360,12 +1363,13 @@ describe('tokens.authenticateRequest(options)', () => {
);

expect(requestState).toBeMachineUnauthenticated({
tokenType: 'machine_token',
tokenType: null,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
});
expect(requestState.toAuth()).toBeMachineUnauthenticatedToAuth({
tokenType: 'machine_token',
tokenType: null,
isAuthenticated: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Expand Down
15 changes: 9 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -428,37 +428,40 @@ export const getAuthObjectFromJwt = (
* Returns an auth object matching the requested token type(s).
*
* If the parsed token type does not match any in acceptsToken, returns:
* - an unauthenticated machine object for the first machine token type in acceptsToken (if present), or
* - an invalid token auth object if the token is not in the accepted array
* - an unauthenticated machine object for machine tokens, or
* - a signed-out session object otherwise.
*
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
export const getAuthObjectForAcceptedToken = ({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
}): AuthObject => {
// 1. any token: return as-is
if (acceptsToken === 'any') {
return authObject;
}

// 2. array of tokens: must match one of the accepted types
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
// If the token is not in the accepted array, return invalid token auth object
return invalidTokenAuthObject();
}
return authObject;
}

// Single value: Intent based
// 3. single token: must match exactly, else return appropriate unauthenticated object
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return signedOutAuthObject(authObject.debug);
Comment thread
wobsoriano marked this conversation as resolved.
}

// 4. default: return as-is
return authObject;
}
};
47 changes: 37 additions & 10 deletions packages/backend/src/tokens/authStatus.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,12 +5,14 @@ import type { TokenVerificationErrorReason } from '../errors';
import type { AuthenticateContext } from './authenticateContext';
import type {
AuthenticatedMachineObject,
InvalidTokenAuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import {
authenticatedMachineObject,
invalidTokenAuthObject,
signedInAuthObject,
signedOutAuthObject,
unauthenticatedMachineObject,
Expand All@@ -27,13 +29,15 @@ export const AuthStatus = {

export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus];

type ToAuth<T extends TokenType, Authenticated extends boolean> = T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType>>;
type ToAuth<T extends TokenType | null, Authenticated extends boolean> = T extends null
? () => InvalidTokenAuthObject
: T extends SessionTokenType
? Authenticated extends true
? (opts?: PendingSessionOptions) => SignedInAuthObject
: () => SignedOutAuthObject
: Authenticated extends true
? () => AuthenticatedMachineObject<Exclude<T, SessionTokenType | null>>
: () => UnauthenticatedMachineObject<Exclude<T, SessionTokenType | null>>;

export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
status: typeof AuthStatus.SignedIn;
Expand All@@ -58,7 +62,7 @@ export type AuthenticatedState<T extends TokenType = SessionTokenType> = {
toAuth: ToAuth<T, true>;
};

export type UnauthenticatedState<T extends TokenType = SessionTokenType> = {
export type UnauthenticatedState<T extends TokenType | null = SessionTokenType> = {
status: typeof AuthStatus.SignedOut;
reason: AuthReason;
message: string;
Expand DownExpand Up@@ -120,8 +124,8 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea

export type AuthReason = AuthErrorReason | TokenVerificationErrorReason;

export type RequestState<T extends TokenType = SessionTokenType> =
| AuthenticatedState<T>
export type RequestState<T extends TokenType | null = SessionTokenType> =
| AuthenticatedState<T extends null ? never : T>
| UnauthenticatedState<T>
| (T extends SessionTokenType ? HandshakeState : never);

Expand DownExpand Up@@ -240,6 +244,29 @@ export function handshake(
});
}

export function signedOutInvalidToken(): UnauthenticatedState<null> {
const authObject = invalidTokenAuthObject();
return withDebugHeaders({
status: AuthStatus.SignedOut,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
proxyUrl: '',
publishableKey: '',
isSatellite: false,
domain: '',
signInUrl: '',
signUpUrl: '',
afterSignInUrl: '',
afterSignUpUrl: '',
isSignedIn: false,
isAuthenticated: false,
tokenType: null,
toAuth: () => authObject,
headers: new Headers(),
token: null,
});
}

const withDebugHeaders = <T extends { headers: Headers; message?: string; reason?: AuthReason; status?: AuthStatus }>(
requestState: T,
): T => {
Expand Down
32 changes: 26 additions & 6 deletions packages/backend/src/tokens/request.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import type { AuthenticateContext } from './authenticateContext';
import { createAuthenticateContext } from './authenticateContext';
import type { SignedInAuthObject } from './authObjects';
import type { HandshakeState, RequestState, SignedInState, SignedOutState, UnauthenticatedState } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus';
import { AuthErrorReason, handshake, signedIn, signedOut, signedOutInvalidToken } from './authStatus';
import { createClerkRequest } from './clerkRequest';
import { getCookieName, getCookieValue } from './cookie';
import { HandshakeService } from './handshake';
Expand DownExpand Up@@ -88,6 +88,20 @@ function checkTokenTypeMismatch(
return null;
}

function isTokenTypeInAcceptedArray(acceptsToken: TokenType[], authenticateContext: AuthenticateContext): boolean {
let parsedTokenType: TokenType | null = null;
const { tokenInHeader } = authenticateContext;
if (tokenInHeader) {
if (isMachineTokenByPrefix(tokenInHeader)) {
parsedTokenType = getMachineTokenType(tokenInHeader);
} else {
parsedTokenType = TokenType.SessionToken;
}
}
const typeToCheck = parsedTokenType ?? TokenType.SessionToken;
return isTokenTypeAccepted(typeToCheck, acceptsToken);
}

export interface AuthenticateRequest {
/**
* @example
Expand All@@ -96,7 +110,7 @@ export interface AuthenticateRequest {
<T extends readonly TokenType[]>(
request: Request,
options: AuthenticateRequestOptions & { acceptsToken: T },
): Promise<RequestState<T[number]>>;
): Promise<RequestState<T[number] | null>>;

/**
* @example
Expand All@@ -123,7 +137,7 @@ export interface AuthenticateRequest {
export const authenticateRequest: AuthenticateRequest = (async (
request: Request,
options: AuthenticateRequestOptions,
): Promise<RequestState<TokenType>> => {
): Promise<RequestState<TokenType> | UnauthenticatedState<null>> => {
const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options);
assertValidSecretKey(authenticateContext.secretKey);

Expand DownExpand Up@@ -655,7 +669,7 @@ export const authenticateRequest: AuthenticateRequest = (async (
// Handle case where tokenType is any and the token is not a machine token
if (!isMachineTokenByPrefix(tokenInHeader)) {
return signedOut({
tokenType: acceptsToken as MachineTokenType,
tokenType: acceptsToken as TokenType,
authenticateContext,
reason: AuthErrorReason.TokenTypeMismatch,
message: '',
Expand DownExpand Up@@ -722,15 +736,21 @@ export const authenticateRequest: AuthenticateRequest = (async (
});
}

// If acceptsToken is an array, early check if the token is in the accepted array
// to avoid unnecessary verification calls
if (Array.isArray(acceptsToken)) {
if (!isTokenTypeInAcceptedArray(acceptsToken, authenticateContext)) {
return signedOutInvalidToken();
}
}

if (authenticateContext.tokenInHeader) {
if (acceptsToken === 'any') {
return authenticateAnyRequestWithTokenInHeader();
}

if (acceptsToken === TokenType.SessionToken) {
return authenticateRequestWithTokenInHeader();
}

return authenticateMachineRequestWithTokenInHeader();
}

Expand Down
Loading