Skip to content

chore(backend,nextjs): Refactor unauthenticated auth object fallback to respect intent #6112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 13, 2025
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
6 changes: 6 additions & 0 deletions .changeset/open-swans-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

Respect `acceptsToken` when returning unauthenticated session or machine object.
4 changes: 3 additions & 1 deletion packages/backend/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ describe('subpath /internal exports', () => {
"getAuthObjectForAcceptedToken",
"getAuthObjectFromJwt",
"getMachineTokenType",
"isMachineToken",
"invalidTokenAuthObject",
"isMachineTokenByPrefix",
"isMachineTokenType",
"isTokenTypeAccepted",
"makeAuthObjectSerializable",
"reverificationError",
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,5 @@ export type {
/**
* Auth objects
*/
export type { AuthObject } from './tokens/authObjects';
export type { AuthObject, InvalidTokenAuthObject } from './tokens/authObjects';
export type { SessionAuthObject, MachineAuthObject } from './tokens/types';
3 changes: 2 additions & 1 deletion packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
signedInAuthObject,
authenticatedMachineObject,
unauthenticatedMachineObject,
invalidTokenAuthObject,
getAuthObjectFromJwt,
getAuthObjectForAcceptedToken,
} from './tokens/authObjects';
Expand All @@ -53,4 +54,4 @@ export { reverificationError, reverificationErrorResponse } from '@clerk/shared/

export { verifyMachineAuthToken } from './tokens/verify';

export { isMachineToken, getMachineTokenType, isTokenTypeAccepted } from './tokens/machine';
export { isMachineTokenByPrefix, isMachineTokenType, getMachineTokenType, isTokenTypeAccepted } from './tokens/machine';
45 changes: 45 additions & 0 deletions packages/backend/src/tokens/__tests__/authObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { describe, expect, it } from 'vitest';

import { mockTokens, mockVerificationResults } from '../../fixtures/machine';
import type { AuthenticateContext } from '../authenticateContext';
import type { InvalidTokenAuthObject, UnauthenticatedMachineObject } from '../authObjects';
import {
authenticatedMachineObject,
getAuthObjectForAcceptedToken,
makeAuthObjectSerializable,
signedInAuthObject,
signedOutAuthObject,
Expand Down Expand Up @@ -387,3 +389,46 @@ describe('unauthenticatedMachineObject', () => {
expect(retrievedToken).toBeNull();
});
});

describe('getAuthObjectForAcceptedToken', () => {
const debugData = { foo: 'bar' };
const sessionAuth = signedOutAuthObject(debugData);
const machineAuth = authenticatedMachineObject('api_key', 'ak_xxx', mockVerificationResults.api_key, debugData);

it('returns original object if acceptsToken is "any"', () => {
const result = getAuthObjectForAcceptedToken({ authObject: machineAuth, acceptsToken: 'any' });
expect(result).toBe(machineAuth);
});

it('returns original object if token type matches', () => {
const result = getAuthObjectForAcceptedToken({ authObject: machineAuth, acceptsToken: 'api_key' });
expect(result).toBe(machineAuth);
});

it('returns InvalidTokenAuthObject if acceptsToken is array and token type does not match', () => {
const result = getAuthObjectForAcceptedToken({
authObject: machineAuth,
acceptsToken: ['machine_token', 'oauth_token'],
});
expect((result as InvalidTokenAuthObject).tokenType).toBeNull();
expect((result as InvalidTokenAuthObject).isAuthenticated).toBe(false);
});

it('returns InvalidTokenAuthObject if parsed type is not a machine token and does not match any in acceptsToken array', () => {
const result = getAuthObjectForAcceptedToken({ authObject: sessionAuth, acceptsToken: ['api_key', 'oauth_token'] });
expect((result as InvalidTokenAuthObject).tokenType).toBeNull();
expect((result as InvalidTokenAuthObject).isAuthenticated).toBe(false);
});

it('returns signed-out session object if parsed type is not a machine token and does not match', () => {
const result = getAuthObjectForAcceptedToken({ authObject: sessionAuth, acceptsToken: ['api_key', 'oauth_token'] });
expect((result as InvalidTokenAuthObject).tokenType).toBeNull();
expect((result as InvalidTokenAuthObject).isAuthenticated).toBe(false);
});

it('returns unauthenticated object for requested type if acceptsToken is a single value and does not match', () => {
const result = getAuthObjectForAcceptedToken({ authObject: machineAuth, acceptsToken: 'machine_token' });
expect((result as UnauthenticatedMachineObject<'machine_token'>).tokenType).toBe('machine_token');
expect((result as UnauthenticatedMachineObject<'machine_token'>).id).toBeNull();
});
});
6 changes: 3 additions & 3 deletions packages/backend/src/tokens/__tests__/getAuth.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectTypeOf, test } from 'vitest';

import type { AuthObject } from '../authObjects';
import type { AuthObject, InvalidTokenAuthObject } from '../authObjects';
import type { GetAuthFn, MachineAuthObject, SessionAuthObject } from '../types';

// Across our SDKs, we have a getAuth() function
Expand All @@ -22,10 +22,10 @@ test('infers the correct AuthObject type for each accepted token type', () => {

// Array of token types
expectTypeOf(getAuth(request, { acceptsToken: ['session_token', 'machine_token'] })).toMatchTypeOf<
SessionAuthObject | MachineAuthObject<'machine_token'>
SessionAuthObject | MachineAuthObject<'machine_token'> | InvalidTokenAuthObject
>();
expectTypeOf(getAuth(request, { acceptsToken: ['machine_token', 'oauth_token'] })).toMatchTypeOf<
MachineAuthObject<'machine_token' | 'oauth_token'>
MachineAuthObject<'machine_token' | 'oauth_token'> | InvalidTokenAuthObject
>();

// Any token type
Expand Down
29 changes: 21 additions & 8 deletions packages/backend/src/tokens/__tests__/machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,34 @@ import { describe, expect, it } from 'vitest';
import {
API_KEY_PREFIX,
getMachineTokenType,
isMachineToken,
isMachineTokenByPrefix,
isMachineTokenType,
isTokenTypeAccepted,
M2M_TOKEN_PREFIX,
OAUTH_TOKEN_PREFIX,
} from '../machine';

describe('isMachineToken', () => {
it('returns true for tokens with M2M prefix', () => {
expect(isMachineToken(`${M2M_TOKEN_PREFIX}some-token-value`)).toBe(true);
expect(isMachineTokenByPrefix(`${M2M_TOKEN_PREFIX}some-token-value`)).toBe(true);
});

it('returns true for tokens with OAuth prefix', () => {
expect(isMachineToken(`${OAUTH_TOKEN_PREFIX}some-token-value`)).toBe(true);
expect(isMachineTokenByPrefix(`${OAUTH_TOKEN_PREFIX}some-token-value`)).toBe(true);
});

it('returns true for tokens with API key prefix', () => {
expect(isMachineToken(`${API_KEY_PREFIX}some-token-value`)).toBe(true);
expect(isMachineTokenByPrefix(`${API_KEY_PREFIX}some-token-value`)).toBe(true);
});

it('returns false for tokens without a recognized prefix', () => {
expect(isMachineToken('unknown_prefix_token')).toBe(false);
expect(isMachineToken('session_token_value')).toBe(false);
expect(isMachineToken('jwt_token_value')).toBe(false);
expect(isMachineTokenByPrefix('unknown_prefix_token')).toBe(false);
expect(isMachineTokenByPrefix('session_token_value')).toBe(false);
expect(isMachineTokenByPrefix('jwt_token_value')).toBe(false);
});

it('returns false for empty tokens', () => {
expect(isMachineToken('')).toBe(false);
expect(isMachineTokenByPrefix('')).toBe(false);
});
});

Expand Down Expand Up @@ -78,3 +79,15 @@ describe('isTokenTypeAccepted', () => {
expect(isTokenTypeAccepted('api_key', 'machine_token')).toBe(false);
});
});

describe('isMachineTokenType', () => {
it('returns true for machine token types', () => {
expect(isMachineTokenType('api_key')).toBe(true);
expect(isMachineTokenType('machine_token')).toBe(true);
expect(isMachineTokenType('oauth_token')).toBe(true);
});

it('returns false for non-machine token types', () => {
expect(isMachineTokenType('session_token')).toBe(false);
});
});
87 changes: 49 additions & 38 deletions packages/backend/src/tokens/authObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { APIKey, CreateBackendApiOptions, IdPOAuthAccessToken, MachineToken
import { createBackendApiClient } from '../api';
import { isTokenTypeAccepted } from '../internal';
import type { AuthenticateContext } from './authenticateContext';
import { isMachineTokenType } from './machine';
import type { MachineTokenType, SessionTokenType } from './tokenTypes';
import { TokenType } from './tokenTypes';
import type { AuthenticateRequestOptions, MachineAuthType } from './types';
Expand Down Expand Up @@ -57,6 +58,7 @@ export type SignedInAuthObject = SharedSignedInAuthObjectProperties & {
* Used to help debug issues when using Clerk in development.
*/
debug: AuthObjectDebug;
isAuthenticated: true;
};

/**
Expand All @@ -77,6 +79,7 @@ export type SignedOutAuthObject = {
getToken: ServerGetToken;
has: CheckAuthorizationFromSessionClaims;
debug: AuthObjectDebug;
isAuthenticated: false;
};

/**
Expand Down Expand Up @@ -120,6 +123,7 @@ export type AuthenticatedMachineObject<T extends MachineTokenType = MachineToken
has: CheckAuthorizationFromSessionClaims;
debug: AuthObjectDebug;
tokenType: T;
isAuthenticated: true;
} & MachineObjectExtendedProperties<true>[T]
: never;

Expand All @@ -140,17 +144,27 @@ export type UnauthenticatedMachineObject<T extends MachineTokenType = MachineTok
has: CheckAuthorizationFromSessionClaims;
debug: AuthObjectDebug;
tokenType: T;
isAuthenticated: false;
} & MachineObjectExtendedProperties<false>[T]
: never;

export type InvalidTokenAuthObject = {
isAuthenticated: false;
tokenType: null;
getToken: () => Promise<null>;
has: () => false;
debug: AuthObjectDebug;
};

/**
* @interface
*/
export type AuthObject =
| SignedInAuthObject
| SignedOutAuthObject
| AuthenticatedMachineObject
| UnauthenticatedMachineObject;
| UnauthenticatedMachineObject
| InvalidTokenAuthObject;

const createDebug = (data: AuthObjectDebugData | undefined) => {
return () => {
Expand Down Expand Up @@ -200,6 +214,7 @@ export function signedInAuthObject(
plans: (sessionClaims.pla as string) || '',
}),
debug: createDebug({ ...authenticateContext, sessionToken }),
isAuthenticated: true,
};
}

Expand All @@ -225,6 +240,7 @@ export function signedOutAuthObject(
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
isAuthenticated: false,
};
}

Expand All @@ -243,6 +259,7 @@ export function authenticatedMachineObject<T extends MachineTokenType>(
getToken: () => Promise.resolve(token),
has: () => false,
debug: createDebug(debugData),
isAuthenticated: true,
};

// Type assertions are safe here since we know the verification result type matches the tokenType.
Expand Down Expand Up @@ -302,6 +319,7 @@ export function unauthenticatedMachineObject<T extends MachineTokenType>(
has: () => false,
getToken: () => Promise.resolve(null),
debug: createDebug(debugData),
isAuthenticated: false,
};

switch (tokenType) {
Expand Down Expand Up @@ -340,6 +358,19 @@ export function unauthenticatedMachineObject<T extends MachineTokenType>(
}
}

/**
* @internal
*/
export function invalidTokenAuthObject(): InvalidTokenAuthObject {
return {
isAuthenticated: false,
tokenType: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: () => ({}),
};
}

/**
* Auth objects moving through the server -> client boundary need to be serializable
* as we need to ensure that they can be transferred via the network as pure strings.
Expand Down Expand Up @@ -394,42 +425,13 @@ export const getAuthObjectFromJwt = (

/**
* @internal
* Filters and coerces an AuthObject based on the accepted token type(s).
*
* This function is used after authentication to ensure that the returned auth object
* matches the expected token type(s) specified by `acceptsToken`. If the token type
* of the provided `authObject` does not match any of the types in `acceptsToken`,
* it returns an unauthenticated or signed-out version of the object, depending on the token type.
*
* - If `acceptsToken` is `'any'`, the original auth object is returned.
* - If `acceptsToken` is a single token type or an array of token types, the function checks if
* `authObject.tokenType` matches any of them.
* - If the token type does not match and is a session token, a signed-out object is returned.
* - If the token type does not match and is a machine token, an unauthenticated machine object is returned.
* - If the token type matches, the original auth object is returned.
*
* @param {Object} params
* @param {AuthObject} params.authObject - The authenticated object to filter.
* @param {AuthenticateRequestOptions['acceptsToken']} [params.acceptsToken=TokenType.SessionToken] - The accepted token type(s). Can be a string, array of strings, or 'any'.
* @returns {AuthObject} The filtered or coerced auth object.
* Returns an auth object matching the requested token type(s).
*
* @example
* // Accept only 'api_key' tokens
* const authObject = { tokenType: 'session_token', userId: 'user_123' };
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: 'api_key' });
* // result will be a signed-out object (since tokenType is 'session_token' and does not match)
* 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
* - a signed-out session object otherwise.
*
* @example
* // Accept 'api_key' or 'machine_token'
* const authObject = { tokenType: 'machine_token', id: 'm2m_123' };
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: ['api_key', 'machine_token'] });
* // result will be the original authObject (since tokenType matches one in the array)
*
* @example
* // Accept any token type
* const authObject = { tokenType: 'api_key', id: 'ak_123' };
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: 'any' });
* // result will be the original authObject
* This ensures the returned object always matches the developer's intent.
*/
export function getAuthObjectForAcceptedToken({
authObject,
Expand All @@ -442,11 +444,20 @@ export function getAuthObjectForAcceptedToken({
return authObject;
}

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
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (authObject.tokenType === TokenType.SessionToken) {
return signedOutAuthObject(authObject.debug);
if (isMachineTokenType(acceptsToken)) {
return unauthenticatedMachineObject(acceptsToken, authObject.debug);
}
return unauthenticatedMachineObject(authObject.tokenType, authObject.debug);
return signedOutAuthObject(authObject.debug);
}

return authObject;
Expand Down
Loading
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy