diff --git a/packages/backend/src/__tests__/exports.test.ts b/packages/backend/src/__tests__/exports.test.ts index 17c4894285..7730495c08 100644 --- a/packages/backend/src/__tests__/exports.test.ts +++ b/packages/backend/src/__tests__/exports.test.ts @@ -37,6 +37,7 @@ describe('subpath /internal exports', () => { expect(Object.keys(internalExports).sort()).toMatchInlineSnapshot(` [ "AuthStatus", + "authenticatedMachineObject", "constants", "createAuthenticateRequest", "createClerkRequest", @@ -49,6 +50,7 @@ describe('subpath /internal exports', () => { "signedInAuthObject", "signedOutAuthObject", "stripPrivateDataFromObject", + "unauthenticatedMachineObject", ] `); }); diff --git a/packages/backend/src/api/endpoints/MachineTokensApi.ts b/packages/backend/src/api/endpoints/MachineTokensApi.ts new file mode 100644 index 0000000000..01332959ed --- /dev/null +++ b/packages/backend/src/api/endpoints/MachineTokensApi.ts @@ -0,0 +1,24 @@ +import type { MachineToken } from '../resources'; +import { AbstractAPI } from './AbstractApi'; + +interface MachineTokensClaims { + [k: string]: unknown; +} + +type CreateMachineTokensParams = { + machineId: string; + claims?: MachineTokensClaims; + expiresInSeconds?: number; + allowedClockSkew?: number; +}; + +const basePath = '/machine_tokens'; +export class MachineTokensAPI extends AbstractAPI { + public async create(params: CreateMachineTokensParams) { + return this.request({ + method: 'POST', + path: basePath, + bodyParams: params, + }); + } +} diff --git a/packages/backend/src/api/endpoints/index.ts b/packages/backend/src/api/endpoints/index.ts index f1ea10ac9c..3f967da854 100644 --- a/packages/backend/src/api/endpoints/index.ts +++ b/packages/backend/src/api/endpoints/index.ts @@ -5,6 +5,7 @@ export * from './ClientApi'; export * from './DomainApi'; export * from './EmailAddressApi'; export * from './InvitationApi'; +export * from './MachineTokensApi'; export * from './OrganizationApi'; export * from './PhoneNumberApi'; export * from './RedirectUrlApi'; diff --git a/packages/backend/src/api/factory.ts b/packages/backend/src/api/factory.ts index 47098188db..9fb4b0f8a1 100644 --- a/packages/backend/src/api/factory.ts +++ b/packages/backend/src/api/factory.ts @@ -5,6 +5,7 @@ import { DomainAPI, EmailAddressAPI, InvitationAPI, + MachineTokensAPI, OrganizationAPI, PhoneNumberAPI, RedirectUrlAPI, @@ -27,6 +28,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) { __experimental_accountlessApplications: new AccountlessApplicationAPI( buildRequest({ ...options, requireSecretKey: false }), ), + __internal_machineTokens: new MachineTokensAPI(request), allowlistIdentifiers: new AllowlistIdentifierAPI(request), clients: new ClientAPI(request), emailAddresses: new EmailAddressAPI(request), diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 0c5c544ba6..b494d21e4c 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -16,6 +16,7 @@ export const ObjectType = { FacebookAccount: 'facebook_account', GoogleAccount: 'google_account', Invitation: 'invitation', + MachineToken: 'machine_token', OauthAccessToken: 'oauth_access_token', Organization: 'organization', OrganizationInvitation: 'organization_invitation', @@ -114,6 +115,11 @@ export interface ExternalAccountJSON extends ClerkResourceJSON { verification: VerificationJSON | null; } +export interface MachineTokenJSON { + object: typeof ObjectType.MachineToken; + jwt: string; +} + export interface SamlAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.SamlAccount; provider: string; diff --git a/packages/backend/src/api/resources/MachineToken.ts b/packages/backend/src/api/resources/MachineToken.ts new file mode 100644 index 0000000000..fecfc9cd5e --- /dev/null +++ b/packages/backend/src/api/resources/MachineToken.ts @@ -0,0 +1,9 @@ +import type { MachineTokenJSON } from './JSON'; + +export class MachineToken { + constructor(readonly token: string) {} + + static fromJSON(data: MachineTokenJSON): MachineToken { + return new MachineToken(data.jwt); + } +} diff --git a/packages/backend/src/api/resources/index.ts b/packages/backend/src/api/resources/index.ts index 20854c5644..2e93a669d2 100644 --- a/packages/backend/src/api/resources/index.ts +++ b/packages/backend/src/api/resources/index.ts @@ -19,6 +19,7 @@ export * from './ExternalAccount'; export * from './IdentificationLink'; export * from './Invitation'; export * from './JSON'; +export * from './MachineToken'; export * from './OauthAccessToken'; export * from './Organization'; export * from './OrganizationInvitation'; diff --git a/packages/backend/src/fixtures/index.ts b/packages/backend/src/fixtures/index.ts index 69381f9ced..54f15016b7 100644 --- a/packages/backend/src/fixtures/index.ts +++ b/packages/backend/src/fixtures/index.ts @@ -4,6 +4,15 @@ import { base64url } from '../util/rfc4648'; export const mockJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.j3rB92k32WqbQDkFB093H4GoQsBVLH4HLGF6ObcwUaVGiHC8SEu6T31FuPf257SL8A5sSGtWWM1fqhQpdLohgZb_hbJswGBuYI-Clxl9BtpIRHbWFZkLBIj8yS9W9aVtD3fWBbF6PHx7BY1udio-rbGWg1YAOZNtVcxF02p-MvX-8XIK92Vwu3Un5zyfCoVIg__qo3Xntzw3tznsZ4XDe212c6kVz1R_L1d5DKjeWXpjUPAS_zFeZSIJEQLf4JNr4JCY38tfdnc3ajfDA3p36saf1XwmTdWXQKCXi75c2TJAXROs3Pgqr5Kw_5clygoFuxN5OEMhFWFSnvIBdi3M6w'; +export const mockMachineJwt = + 'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDIyMkFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjIwNDQ0MzU4MTIsImlhdCI6MTczMzg2NjI5MiwiaXNzIjoiaHR0cHM6Ly9zYWZlLWJlYWdsZS0zMi5jbGVyay5hY2NvdW50c3N0YWdlLmRldiIsImp0aSI6IjA2NTAwNTBkNzlhMDZlNjhjNTY1IiwibmJmIjoxNzMzODY1OTkyLCJzdWIiOiJtY2hfdGVzdCJ9.oM7RTA4j-WWF9zFbWq0QCepSC4Lysq9rPuNYDVBYJg_mw1viXRYhbQO5q2_Tsvncshm1JSwvTilHwnGokuBAT1F4wpRwGn22Fd4w-GkyKq6sYMVpvnIQOQdQB2OeZbxqYujtwVuT67vwV_vt4jjTFMI8c4AXG9P8aIckEjys2txx79eY1CgdILKGaMXsWqOy5vkKboIdktWO8bUhca6ESb2HnU4k5SgZepkjNPJq_Ei1IOQBzsotZ7_HJaqiZgvWhtWv_buJ-JH-VtFiDN6HUbqS4yF9K4krqo-6g5nsok_kXLzPH1iVdCPhcjo-34Wx1lwIR035SHjI9BaNaJYXvg'; + +export const mockExpiredMachineJwt = + 'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDIyMkFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJkZWV6IjoibnV0cyIsImV4cCI6MTczMzc5MjgwOCwiaWF0IjoxNzMzNzkyMjA4LCJpc3MiOiJodHRwczovL3NhZmUtYmVhZ2xlLTMyLmNsZXJrLmFjY291bnRzc3RhZ2UuZGV2IiwianRpIjoiMzY3OTAyNzViZGY0OWZiZDBiNTUiLCJuYmYiOjE3MzM3OTIyMDMsInN1YiI6Im1jaF90ZXN0In0.FwqEfAZsY0vmV7tWU9vJ6VkAKBzFHUOqX6MTkMXGtUzzYaR7eHPnZpDhAb9wizM2xeCbUX4gNe8znNeKRYHJEvmtEjg-PPkPxveIl8PI5ZNF1rAceDL0T0F3MyMJOX34KcyOH99c-CUEpcvezahH2qGb6STMKdb29AQa-fyCNnP4_VqHNqFapwFcweeCUJSGXSo4N4Qcmadm7wvqwOQMbyOkGXJdna2a4quWTM7OdxWwXShGotUlmYzr3kejHXyjUtJ4j7m6g9huADaj9r7lC4VX6dykV115GTd6uExLA8ZS7pd4fuxoOS9sbEMNyKPS9cEZIOA1Xvf8njgcqn8fhw'; + +export const mockUserTokenForMachineTesting = + 'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDExMUFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjIwNDkyMjkxNjksImZ2YSI6WzIsLTFdLCJpYXQiOjE3MzM4NjkxNjksImlzcyI6Imh0dHBzOi8vc2FmZS1iZWFnbGUtMzIuY2xlcmsuYWNjb3VudHNzdGFnZS5kZXYiLCJuYmYiOjE3MzM4NjkxNTksInNpZCI6InNlc3NfMnEybVN6UEJLUEVOZzdtRVVlMDhsTXZuRUY5Iiwic3ViIjoidXNlcl8ycTJtU3ZvQWU1VmlYYWVYYUJQdFVLamdoSTAifQ.P2wsTzMBB5wAkeUbeNOF2sTSrE0cHD7ICyjYqgM-Ai9ppTsZeDSI8qQNDwqkAFiJ0FWI6PuwtaYiRkcRDxBe7m-KvF6UrRC5zXkBGD9lZEUInisFSAvdW4BFJ78_xWHGRmhAKKWXYjYit66GAN3Ie2dYmlSeE6UmsrA4tCIqZgJgfYZ_ClBF35OA_Q1j26OQT2PGy7qZ7E3cB_YZO7sDaVLr_vZkyVUdb2hODdQlSpU8pyoTRNrZf9nI_MqJwurVuTThEI3TgCbqUuGGxc5xWx16qaupxTkKMo3SehoD5DZMVpG6yyqHuXD_aUE4hHIT8J9qoqSd98j4eZ5z-63Sgw'; + // signed with signingJwks, same as mockJwt but with lower iat and exp values export const mockExpiredJwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODIwMCwiaWF0IjoxNjY2NjQ3MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.jLImjg2vGwOJDkK9gtIeJnEJWVOgoCMeC46OFtfzJ1d8OT0KVvwRppC60QIMfHKoOwLTLYlq8SccrkARwlJ_jOvMAYMGZT-R4qHoEfGmet1cSTC67zaafq5gpf9759x1kNMyckry_PJNSx-9hTFbBMWhY7XVLVlrauppqHXOQr1-BC7u-0InzKjCQTCJj-81Yt8xRKweLbO689oYSRAFYK5LNH8BYoLZFWuWLO-6nxUJu0_XAq9xpZPqZOqj3LxFS4hHVGGmTqnPgR8vBetLXxSLAOBsEyIkeQkOBA03YA6enTNIppmy0XTLgAYmUO_JWOGjjjDQoEojuXtuLRdQHQ'; @@ -33,6 +42,15 @@ export const mockJwtPayload = { sub: 'user_2GIpXOEpVyJw51rkZn9Kmnc6Sxr', }; +export const mockMachineJwtPayload = { + exp: 2044435812, + iat: 1733866292, + iss: 'https://safe-beagle-32.clerk.accountsstage.dev', + jti: '0650050d79a06e68c565', + nbf: 1733865992, + sub: 'mch_test', +}; + export const mockRsaJwkKid = 'ins_2GIoQhbUpy0hX7B2cVkuTMinXoD'; export const mockRsaJwk = { @@ -48,6 +66,19 @@ export const mockJwks = { keys: [mockRsaJwk], }; +export const mockMachineRsaJwk = { + use: 'sig', + kty: 'RSA', + kid: 'ins_2oaiWB3PCI6Vl9TJ9lYzg0Q8ryy', + alg: 'RS256', + n: 'wMAr7X1GzgyocS74bYe8uEQ3yLRGb91qdsfd7cRAQ6fiZca7wkOQRhud5EV9JlmDcHqElR2q_ZLFjrtkQo1nSgPhvc70hlha4ScKWrmS_LFcaz-oLBTUUi4k4zbvv6LThLmNGbEO88OttSy4tOMQMsyIQJD32aN1MHQLcS9Jnd70ZD4q6wEUAznyS0QPpLwd3X5TUTan9kUoUHw9t4-FzNFQJ_t_xKMVkw2BIr9n4fEOBl-UjLh1frFVmOWMC5ygpZ9A_19qEKgVDNKgRHoYN4sHH6y8pKPEMgUce2Ee10RMtw9rLpK1JQDb4iXN_Dxd7QUfh62aUFRNIuePWLbHIw', + e: 'AQAB', +}; + +export const mockMachineJwks = { + keys: [mockMachineRsaJwk], +}; + export const mockPEMKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8Z1oLQbaYkakUSIYRvjmOoeXMDFFjynGP2+gVy0mQJHYgVhgo34RsQgZoz7rSNm/EOL+l/mHTqQAhwaf9Ef8X5vsPX8vP3RNRRm3XYpbIGbOcANJaHihJZwnzG9zIGYF8ki+m55zftO7pkOoXDtIqCt+5nIUQjGJK5axFELrnWaz2qcR03A7rYKQc3F1gut2Ru1xfmiJVUlQe0tLevQO/FzfYpWu7+691q+ZRUGxWvGc0ays4ACa7JXElCIKXRv/yb3Vc1iry77HRAQ28J7Fqpj5Cb+sxfFI+Vhf1GB1bNeOLPR10nkSMJ74HB0heHi/SsM83JiGekv0CpZPCC8jcQIDAQAB'; diff --git a/packages/backend/src/internal.ts b/packages/backend/src/internal.ts index c9b7daf9ff..5c06495e63 100644 --- a/packages/backend/src/internal.ts +++ b/packages/backend/src/internal.ts @@ -9,11 +9,29 @@ export { debugRequestState } from './tokens/request'; export type { AuthenticateRequestOptions, OrganizationSyncOptions } from './tokens/types'; -export type { SignedInAuthObjectOptions, SignedInAuthObject, SignedOutAuthObject } from './tokens/authObjects'; -export { makeAuthObjectSerializable, signedOutAuthObject, signedInAuthObject } from './tokens/authObjects'; +export type { + SignedInAuthObjectOptions, + SignedInAuthObject, + SignedOutAuthObject, + AuthenticatedMachineObject, + UnauthenticatedMachineObject, +} from './tokens/authObjects'; +export { + makeAuthObjectSerializable, + signedOutAuthObject, + signedInAuthObject, + unauthenticatedMachineObject, + authenticatedMachineObject, +} from './tokens/authObjects'; export { AuthStatus } from './tokens/authStatus'; -export type { RequestState, SignedInState, SignedOutState } from './tokens/authStatus'; +export type { + RequestState, + SignedInState, + SignedOutState, + MachineAuthenticatedState, + MachineUnauthenticatedState, +} from './tokens/authStatus'; export { decorateObjectWithResources, stripPrivateDataFromObject } from './util/decorateObjectWithResources'; diff --git a/packages/backend/src/tokens/__tests__/authObjects.test.ts b/packages/backend/src/tokens/__tests__/authObjects.test.ts index bb63cb7b85..5cd010ad9a 100644 --- a/packages/backend/src/tokens/__tests__/authObjects.test.ts +++ b/packages/backend/src/tokens/__tests__/authObjects.test.ts @@ -2,7 +2,12 @@ import type { JwtPayload } from '@clerk/types'; import { describe, expect, it } from 'vitest'; import type { AuthenticateContext } from '../authenticateContext'; -import { makeAuthObjectSerializable, signedInAuthObject, signedOutAuthObject } from '../authObjects'; +import { + authenticatedMachineObject, + makeAuthObjectSerializable, + signedInAuthObject, + signedOutAuthObject, +} from '../authObjects'; describe('makeAuthObjectSerializable', () => { it('removes non-serializable props', () => { @@ -32,3 +37,20 @@ describe('signedInAuthObject', () => { expect(token).toBe('token'); }); }); + +describe('authenticatedMachineObject', () => { + it('getToken returns the token passed in', () => { + const authObject = authenticatedMachineObject('token', { + act: null, + sid: null, + org_id: null, + org_role: null, + org_slug: null, + org_permissions: null, + sub: 'mch_id', + } as unknown as JwtPayload); + + const token = authObject.getToken(); + expect(token).toBe('token'); + }); +}); diff --git a/packages/backend/src/tokens/__tests__/authStatus.test.ts b/packages/backend/src/tokens/__tests__/authStatus.test.ts index 87b50c567b..9031f60a90 100644 --- a/packages/backend/src/tokens/__tests__/authStatus.test.ts +++ b/packages/backend/src/tokens/__tests__/authStatus.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { handshake, signedIn, signedOut } from '../authStatus'; +import { handshake, machineAuthenticated, machineUnauthenticated, signedIn, signedOut } from '../authStatus'; describe('signed-in', () => { it('does not include debug headers', () => { @@ -41,6 +41,45 @@ describe('signed-out', () => { }); }); +describe('machine-unauthenticated', () => { + it('includes debug headers', () => { + const headers = new Headers({ 'custom-header': 'value' }); + const authObject = machineUnauthenticated({} as any, 'auth-reason', 'auth-message', headers); + + expect(authObject.headers.get('custom-header')).toBe('value'); + expect(authObject.headers.get('x-clerk-auth-status')).toBe('machine-unauthenticated'); + expect(authObject.headers.get('x-clerk-auth-reason')).toBe('auth-reason'); + expect(authObject.headers.get('x-clerk-auth-message')).toBe('auth-message'); + }); + + it('handles debug headers containing invalid unicode characters without throwing', () => { + const headers = new Headers({ 'custom-header': 'value' }); + const authObject = machineUnauthenticated({} as any, 'auth-reason+RR�56', 'auth-message+RR�56', headers); + + expect(authObject.headers.get('custom-header')).toBe('value'); + expect(authObject.headers.get('x-clerk-auth-status')).toBe('machine-unauthenticated'); + expect(authObject.headers.get('x-clerk-auth-reason')).toBeNull(); + expect(authObject.headers.get('x-clerk-auth-message')).toBeNull(); + }); +}); + +describe('machine-authenticated', () => { + it('does not include debug headers', () => { + const authObject = machineAuthenticated({} as any, undefined, 'token', {} as any); + + expect(authObject.headers.get('x-clerk-auth-status')).toBeNull(); + expect(authObject.headers.get('x-clerk-auth-reason')).toBeNull(); + expect(authObject.headers.get('x-clerk-auth-message')).toBeNull(); + }); + + it('authObject returned by toAuth() returns the token passed', async () => { + const signedInAuthObject = signedIn({} as any, { sid: 'sid' } as any, undefined, 'token').toAuth(); + const token = await signedInAuthObject.getToken(); + + expect(token).toBe('token'); + }); +}); + describe('handshake', () => { it('includes debug headers', () => { const headers = new Headers({ location: '/' }); diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 76a00601bf..697d9dd75b 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -4,11 +4,16 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { TokenVerificationErrorReason } from '../../errors'; import { mockExpiredJwt, + mockExpiredMachineJwt, mockInvalidSignatureJwt, mockJwks, mockJwt, mockJwtPayload, + mockMachineJwks, + mockMachineJwt, + mockMachineJwtPayload, mockMalformedJwt, + mockUserTokenForMachineTesting, } from '../../fixtures'; import { server } from '../../mock-server'; import type { AuthReason } from '../authStatus'; @@ -31,6 +36,10 @@ interface CustomMatchers { toMatchHandshake: (expected: unknown) => R; toBeSignedIn: (expected?: unknown) => R; toBeSignedInToAuth: () => R; + toBeMachineAuthenticated: (expected?: unknown) => R; + toBeMachineAuthenticatedToAuth: () => R; + toBeMachineUnAuthenticated: (expected: unknown) => R; + toBeMachineUnAuthenticatedToAuth: () => R; } declare module 'vitest' { @@ -216,6 +225,115 @@ expect.extend({ }; } }, + toBeMachineAuthenticated( + received, + expected: { + domain?: string; + isSatellite?: boolean; + signInUrl?: string; + }, + ) { + const pass = + received.afterSignInUrl === '' && + received.afterSignUpUrl === '' && + received.domain === (expected?.domain ?? '') && + received.isSatellite === (expected?.isSatellite ?? false) && + received.isSignedIn === false && + received.proxyUrl === '' && + received.signInUrl === (expected?.signInUrl ?? '') && + received.signUpUrl === '' && + received.isMachineAuthenticated === true && + received.status === AuthStatus.MachineAuthenticated; + + if (pass) { + return { + message: () => `expected to be machine authenticated`, + pass: true, + }; + } else { + return { + message: () => `expected not to be machine authenticated`, + pass: false, + }; + } + }, + toBeMachineAuthenticatedToAuth(received) { + const pass = + !received.orgId && + !received.orgRole && + !received.orgSlug && + !received.sessionClaims && + !received.sessionId && + !received.userId; + + if (pass) { + return { + message: () => `expected machine to be authenticated`, + pass: true, + }; + } else { + return { + message: () => `expected machine not to be authenticated`, + pass: false, + }; + } + }, + toBeMachineUnAuthenticated( + received, + expected: { + domain?: string; + isSatellite?: boolean; + reason: AuthReason; + signInUrl?: string; + }, + ) { + const pass = + received.afterSignInUrl === '' && + received.afterSignUpUrl === '' && + received.domain === (expected?.domain ?? '') && + received.isSatellite === (expected?.isSatellite ?? false) && + received.isSignedIn === false && + received.proxyUrl === '' && + received.signInUrl === (expected?.signInUrl ?? '') && + received.signUpUrl === '' && + received.reason === expected.reason && + received.isMachineAuthenticated === false && + received.status === AuthStatus.MachineUnauthenticated; + + if (pass) { + return { + message: () => `expected to be machine authenticated`, + pass: true, + }; + } else { + return { + message: () => `expected not to be machine authenticated`, + pass: false, + }; + } + }, + toBeMachineUnAuthenticatedToAuth(received) { + const pass = + !received.orgId && + !received.orgRole && + !received.orgSlug && + !received.sessionClaims && + !received.sessionId && + !received.userId && + !received.machineId; + + if (pass) { + return { + message: () => `expected machine not to be authenticated`, + pass: true, + }; + } else { + return { + message: () => `expected machine to be authenticated`, + pass: false, + }; + } + }, }); const defaultHeaders: Record = { @@ -243,6 +361,7 @@ const mockOptions = (options?) => { afterSignInUrl: '', afterSignUpUrl: '', domain: '', + entity: '', ...options, } satisfies AuthenticateRequestOptions; }; @@ -570,6 +689,80 @@ describe('tokens.authenticateRequest(options)', () => { expect(requestState).toBeSignedOutToAuth(); }); + test('headerToken: returns machine authenticated state when a valid token [1y.2y]', async () => { + vi.setSystemTime(new Date(mockMachineJwtPayload.iat * 1000)); + + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockMachineJwks); + }), + ); + const requestState = await authenticateRequest( + mockRequestWithHeaderAuth({ + authorization: mockMachineJwt, + }), + mockOptions({ + entity: 'machine', + }), + ); + + expect(requestState).toBeMachineAuthenticated(); + expect(requestState.toAuth()).toBeMachineAuthenticatedToAuth(); + }); + test('headerToken: returns machine unauthenticated state when passing an expired token [1y.2y]', async () => { + vi.setSystemTime(new Date(mockMachineJwtPayload.iat * 1000)); + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockMachineJwks); + }), + ); + const requestState = await authenticateRequest( + mockRequestWithHeaderAuth({ authorization: mockExpiredMachineJwt }), + mockOptions({ entity: 'machine' }), + ); + + expect(requestState).toBeMachineUnAuthenticated({ + reason: TokenVerificationErrorReason.TokenExpired, + message: 'JWT is expired', + }); + expect(requestState.toAuth()).toBeMachineUnAuthenticatedToAuth(); + }); + test('headerToken: returns machine unauthenticated state when passing a user token [1y.2y]', async () => { + vi.setSystemTime(vi.getRealSystemTime()); + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockMachineJwks); + }), + ); + const requestState = await authenticateRequest( + mockRequestWithHeaderAuth({ authorization: mockUserTokenForMachineTesting }), + mockOptions({ entity: 'machine' }), + ); + + expect(requestState).toBeMachineUnAuthenticated({ + reason: + 'Expected a machine token but received a user token. Please verify the token type and ensure you are passing a machine token to the machine authentication function', + }); + expect(requestState.toAuth()).toBeMachineUnAuthenticatedToAuth(); + }); + test('headerToken: returns machine unauthenticated state when passing a no token [1y.2y]', async () => { + vi.setSystemTime(vi.getRealSystemTime()); + server.use( + http.get('https://api.clerk.test/v1/jwks', () => { + return HttpResponse.json(mockMachineJwks); + }), + ); + const requestState = await authenticateRequest( + mockRequestWithHeaderAuth({ authorization: '' }), + mockOptions({ entity: 'machine' }), + ); + + expect(requestState).toBeMachineUnAuthenticated({ + reason: 'no machine token in header', + }); + expect(requestState.toAuth()).toBeMachineUnAuthenticatedToAuth(); + }); + test('cookieToken: returns handshake when clientUat is missing or equals to 0 and is satellite and not is synced [11y]', async () => { server.use( http.get('https://api.clerk.test/v1/jwks', () => { diff --git a/packages/backend/src/tokens/authObjects.ts b/packages/backend/src/tokens/authObjects.ts index a0b967cb4d..de227dfe07 100644 --- a/packages/backend/src/tokens/authObjects.ts +++ b/packages/backend/src/tokens/authObjects.ts @@ -69,10 +69,48 @@ export type SignedOutAuthObject = { debug: AuthObjectDebug; }; +export type AuthenticatedMachineObject = { + sessionClaims: null; + claims: JwtPayload; + machineId: string; + sessionId: null; + actor: null; + userId: null; + orgId: null; + orgRole: null; + orgSlug: null; + orgPermissions: null; + __experimental_factorVerificationAge: null; + has: CheckAuthorizationFromSessionClaims; + getToken: () => string; + debug: AuthObjectDebug; +}; + +export type UnauthenticatedMachineObject = { + sessionClaims: null; + claims: null; + machineId: null; + sessionId: null; + actor: null; + userId: null; + orgId: null; + orgRole: null; + orgSlug: null; + orgPermissions: null; + __experimental_factorVerificationAge: null; + has: CheckAuthorizationFromSessionClaims; + getToken: ServerGetToken; + debug: AuthObjectDebug; +}; + /** * @internal */ -export type AuthObject = SignedInAuthObject | SignedOutAuthObject; +export type AuthObject = + | SignedInAuthObject + | SignedOutAuthObject + | AuthenticatedMachineObject + | UnauthenticatedMachineObject; const createDebug = (data: AuthObjectDebugData | undefined) => { return () => { @@ -147,6 +185,51 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA }; } +export function authenticatedMachineObject( + machineToken: string, + claims: JwtPayload, + debugData?: AuthObjectDebugData, +): AuthenticatedMachineObject { + const { sub: machineId } = claims; + const getToken = () => { + return machineToken; + }; + return { + sessionClaims: null, + claims, + machineId, + sessionId: null, + actor: null, + userId: null, + orgId: null, + orgRole: null, + orgSlug: null, + orgPermissions: null, + __experimental_factorVerificationAge: null, + getToken, + has: () => false, + debug: createDebug(debugData), + }; +} + +export function unauthenticatedMachineObject(debugData?: AuthObjectDebugData): UnauthenticatedMachineObject { + return { + sessionClaims: null, + claims: null, + machineId: null, + sessionId: null, + actor: null, + userId: null, + orgId: null, + orgRole: null, + orgSlug: null, + orgPermissions: null, + __experimental_factorVerificationAge: null, + getToken: () => Promise.resolve(null), + has: () => false, + debug: createDebug(debugData), + }; +} /** * 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. diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 6bb141fd2b..cf7f206566 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -3,13 +3,25 @@ import type { JwtPayload } from '@clerk/types'; import { constants } from '../constants'; import type { TokenVerificationErrorReason } from '../errors'; import type { AuthenticateContext } from './authenticateContext'; -import type { SignedInAuthObject, SignedOutAuthObject } from './authObjects'; -import { signedInAuthObject, signedOutAuthObject } from './authObjects'; +import type { + AuthenticatedMachineObject, + SignedInAuthObject, + SignedOutAuthObject, + UnauthenticatedMachineObject, +} from './authObjects'; +import { + authenticatedMachineObject, + signedInAuthObject, + signedOutAuthObject, + unauthenticatedMachineObject, +} from './authObjects'; export const AuthStatus = { SignedIn: 'signed-in', SignedOut: 'signed-out', Handshake: 'handshake', + MachineAuthenticated: 'machine-authenticated', + MachineUnauthenticated: 'machine-unauthenticated', } as const; export type AuthStatus = (typeof AuthStatus)[keyof typeof AuthStatus]; @@ -56,6 +68,22 @@ export type HandshakeState = Omit & { toAuth: () => null; }; +export type MachineAuthenticatedState = Omit & { + status: typeof AuthStatus.MachineAuthenticated; + reason: null; + message: null; + isMachineAuthenticated: true; + toAuth: () => AuthenticatedMachineObject; + token: string; +}; + +export type MachineUnauthenticatedState = Omit & { + status: typeof AuthStatus.MachineUnauthenticated; + message: string; + isMachineAuthenticated: false; + toAuth: () => UnauthenticatedMachineObject; +}; + export const AuthErrorReason = { ClientUATWithoutSessionToken: 'client-uat-but-no-session-token', DevBrowserMissing: 'dev-browser-missing', @@ -77,7 +105,12 @@ export type AuthErrorReason = (typeof AuthErrorReason)[keyof typeof AuthErrorRea export type AuthReason = AuthErrorReason | TokenVerificationErrorReason; -export type RequestState = SignedInState | SignedOutState | HandshakeState; +export type RequestState = + | SignedInState + | SignedOutState + | HandshakeState + | MachineAuthenticatedState + | MachineUnauthenticatedState; export function signedIn( authenticateContext: AuthenticateContext, @@ -155,6 +188,59 @@ export function handshake( }); } +export function machineAuthenticated( + authenticateContext: AuthenticateContext, + headers: Headers = new Headers(), + token: string, + claims: JwtPayload, +): MachineAuthenticatedState { + const machineAuthObject = authenticatedMachineObject(token, claims); + return { + status: AuthStatus.MachineAuthenticated, + reason: null, + message: null, + proxyUrl: authenticateContext.proxyUrl || '', + publishableKey: authenticateContext.publishableKey || '', + isSatellite: authenticateContext.isSatellite || false, + domain: authenticateContext.domain || '', + signInUrl: authenticateContext.signInUrl || '', + signUpUrl: authenticateContext.signUpUrl || '', + afterSignInUrl: authenticateContext.afterSignInUrl || '', + afterSignUpUrl: authenticateContext.afterSignUpUrl || '', + isSignedIn: false, + isMachineAuthenticated: true, + toAuth: () => machineAuthObject, + headers, + token, + }; +} + +export function machineUnauthenticated( + authenticateContext: AuthenticateContext, + reason: AuthReason, + message = '', + headers: Headers = new Headers(), +): MachineUnauthenticatedState { + return withDebugHeaders({ + status: AuthStatus.MachineUnauthenticated, + reason, + message, + proxyUrl: authenticateContext.proxyUrl || '', + publishableKey: authenticateContext.publishableKey || '', + isSatellite: authenticateContext.isSatellite || false, + domain: authenticateContext.domain || '', + signInUrl: authenticateContext.signInUrl || '', + signUpUrl: authenticateContext.signUpUrl || '', + afterSignInUrl: authenticateContext.afterSignInUrl || '', + afterSignUpUrl: authenticateContext.afterSignUpUrl || '', + isSignedIn: false, + isMachineAuthenticated: false, + toAuth: () => unauthenticatedMachineObject(), + headers, + token: null, + }); +} + const withDebugHeaders = (requestState: T): T => { const headers = new Headers(requestState.headers || {}); diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 46c2445cb7..3be3547725 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -11,8 +11,22 @@ import { isDevelopmentFromSecretKey } from '../util/shared'; import type { AuthenticateContext } from './authenticateContext'; import { createAuthenticateContext } from './authenticateContext'; import type { SignedInAuthObject } from './authObjects'; -import type { HandshakeState, RequestState, SignedInState, SignedOutState } from './authStatus'; -import { AuthErrorReason, handshake, signedIn, signedOut } from './authStatus'; +import type { + HandshakeState, + MachineAuthenticatedState, + MachineUnauthenticatedState, + RequestState, + SignedInState, + SignedOutState, +} from './authStatus'; +import { + AuthErrorReason, + handshake, + machineAuthenticated, + machineUnauthenticated, + signedIn, + signedOut, +} from './authStatus'; import { createClerkRequest } from './clerkRequest'; import { getCookieName, getCookieValue } from './cookie'; import { verifyHandshakeToken } from './handshake'; @@ -92,8 +106,14 @@ function isRequestEligibleForRefresh( export async function authenticateRequest( request: Request, - options: AuthenticateRequestOptions, -): Promise { + options: AuthenticateRequestOptions & { entity: 'machine' }, +): Promise; +export async function authenticateRequest( + request: Request, + options: AuthenticateRequestOptions & { entity: 'user' }, +): Promise; +export async function authenticateRequest(request: Request, options: AuthenticateRequestOptions): Promise; +export async function authenticateRequest(request: Request, options: AuthenticateRequestOptions) { const authenticateContext = await createAuthenticateContext(createClerkRequest(request), options); assertValidSecretKey(authenticateContext.secretKey); @@ -621,8 +641,11 @@ ${error.getFullMessage()}`, async function handleError( err: unknown, tokenCarrier: TokenCarrier, - ): Promise { + ): Promise { if (!(err instanceof TokenVerificationError)) { + if (options.entity === 'machine') { + return machineUnauthenticated(authenticateContext, 'unexpected error', 'unexpected error'); + } return signedOut(authenticateContext, AuthErrorReason.UnexpectedError); } @@ -659,7 +682,7 @@ ${error.getFullMessage()}`, TokenVerificationErrorReason.TokenIatInTheFuture, ].includes(err.reason); - if (reasonToHandshake) { + if (reasonToHandshake && options.entity !== 'machine') { return handleMaybeHandshakeStatus( authenticateContext, convertTokenVerificationErrorReasonToAuthErrorReason({ tokenError: err.reason, refreshError }), @@ -667,13 +690,44 @@ ${error.getFullMessage()}`, ); } + if (options.entity === 'machine') { + return machineUnauthenticated(authenticateContext, err.reason, err.getFullMessage()); + } + return signedOut(authenticateContext, err.reason, err.getFullMessage()); } + async function authenticateMachineRequestWithTokenInHeader() { + const { sessionTokenInHeader } = authenticateContext; + + if (!sessionTokenInHeader) { + return handleError(new Error('No Session token in header, this should not happen in machine requests'), 'header'); + } + + const { data, errors } = await verifyToken(sessionTokenInHeader, authenticateContext); + if (errors) { + return handleError(errors[0], 'header'); + } + if (data.sub.startsWith('user_')) { + return machineUnauthenticated( + authenticateContext, + 'Expected a machine token but received a user token. Please verify the token type and ensure you are passing a machine token to the machine authentication function', + ); + } + return machineAuthenticated(authenticateContext, undefined, sessionTokenInHeader, data); + } + if (authenticateContext.sessionTokenInHeader) { + if (options.entity === 'machine') { + return authenticateMachineRequestWithTokenInHeader(); + } return authenticateRequestWithTokenInHeader(); } + // machine requests cannot have the token in the cookie, it must be in header + if (options.entity === 'machine') { + return machineUnauthenticated(authenticateContext, 'no machine token in header'); + } return authenticateRequestWithTokenInCookie(); } diff --git a/packages/backend/src/tokens/types.ts b/packages/backend/src/tokens/types.ts index 235b08eb07..1eba981116 100644 --- a/packages/backend/src/tokens/types.ts +++ b/packages/backend/src/tokens/types.ts @@ -12,6 +12,7 @@ export type AuthenticateRequestOptions = { afterSignUpUrl?: string; organizationSyncOptions?: OrganizationSyncOptions; apiClient?: ApiClient; + entity?: 'user' | 'machine'; } & VerifyTokenOptions; /** diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 8079e6a8bc..a1bbfa3f26 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,5 +1,6 @@ import type { AuthObject } from '@clerk/backend'; -import { constants, createClerkRequest, createRedirect, type RedirectFun } from '@clerk/backend/internal'; +import type { RedirectFun, SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal'; +import { constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; import { notFound, redirect } from 'next/navigation'; import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants'; @@ -12,18 +13,33 @@ import { buildRequestLike } from './utils'; type Auth = AuthObject & { redirectToSignIn: RedirectFun> }; +type MachineAuth = Exclude & { + redirectToSignIn: RedirectFun>; +}; + +type AuthOptions = { entity?: 'user' | 'machine' }; + export interface AuthFn { (): Promise; protect: AuthProtect; } -export const auth: AuthFn = async () => { +export interface MachineAuthFn { + (options?: AuthOptions): Promise; + protect: AuthProtect; +} + +export function auth(options?: AuthOptions & { entity: 'user' }): Promise; +export function auth(options?: AuthOptions & { entity: 'machine' }): Promise; +export async function auth(options?: AuthOptions): Promise; +export async function auth(options?: AuthOptions) { require('server-only'); const request = await buildRequestLike(); const authObject = createGetAuth({ debugLoggerName: 'auth()', noAuthStatusMessage: authAuthHeaderMissing(), + options, })(request); const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl'); @@ -50,7 +66,7 @@ export const auth: AuthFn = async () => { }; return Object.assign(authObject, { redirectToSignIn }); -}; +} auth.protect = async (...args: any[]) => { require('server-only'); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 2e7ce11bfd..81dd5fefd1 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -1,6 +1,7 @@ import type { AuthObject, ClerkClient } from '@clerk/backend'; import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal'; import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal'; +import { decodeJwt } from '@clerk/backend/jwt'; import { eventMethodCalled } from '@clerk/shared/telemetry'; import { notFound as nextjsNotFound } from 'next/navigation'; import type { NextMiddleware, NextRequest } from 'next/server'; @@ -131,6 +132,13 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => { logger.enable(); } const clerkRequest = createClerkRequest(request); + const headers = JSON.parse(clerkRequest.toJSON().headers); + const authorization = headers.authorization; + if (authorization) { + const bearerToken = decodeJwt(authorization.split(' ')[1]); + const { payload } = bearerToken; + payload.sub.startsWith('mch_') ? (options.entity = 'machine') : null; + } logger.debug('options', options); logger.debug('url', () => clerkRequest.toJSON()); diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index 03c1b7bd35..ac2d30d7e1 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -1,20 +1,30 @@ import type { AuthObject } from '@clerk/backend'; +import type { + AuthenticatedMachineObject, + SignedInAuthObject, + SignedOutAuthObject, + UnauthenticatedMachineObject, +} from '@clerk/backend/internal'; import { constants } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import { isTruthy } from '@clerk/shared/underscore'; import { withLogger } from '../utils/debugLogger'; -import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; +import type { GetAuthDataFromRequestOptions } from './data/getAuthDataFromRequest'; +import { getAuthDataFromRequest as getAuthDataFromRequestOriginal } from './data/getAuthDataFromRequest'; import { getAuthAuthHeaderMissing } from './errors'; import type { RequestLike } from './types'; import { assertAuthStatus, getCookie, getHeader } from './utils'; +type GetAuthOptions = { entity?: 'user' | 'machine' }; export const createGetAuth = ({ noAuthStatusMessage, debugLoggerName, + options, }: { debugLoggerName: string; noAuthStatusMessage: string; + options?: GetAuthOptions; }) => withLogger(debugLoggerName, logger => { return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => { @@ -24,7 +34,21 @@ export const createGetAuth = ({ assertAuthStatus(req, noAuthStatusMessage); - return getAuthDataFromRequest(req, { ...opts, logger }); + // Explicitly declare overloads at the top level + function getAuthDataFromRequest( + req: RequestLike, + opts: GetAuthDataFromRequestOptions & { entity: 'machine' }, + ): Exclude; + function getAuthDataFromRequest( + req: RequestLike, + opts: GetAuthDataFromRequestOptions & { entity: 'user' }, + ): Exclude; + function getAuthDataFromRequest(req: RequestLike, opts: GetAuthDataFromRequestOptions): AuthObject; + function getAuthDataFromRequest(req: RequestLike, opts: GetAuthDataFromRequestOptions) { + // Ensure you spread and pass the correct options, including the logger + return getAuthDataFromRequestOriginal(req, { ...opts, logger, entity: options?.entity }); + } + return getAuthDataFromRequest(req, { ...opts, logger, entity: options?.entity }); }; }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts index c276a29fc5..53e094ba79 100644 --- a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -1,5 +1,18 @@ import type { AuthObject } from '@clerk/backend'; -import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; +import type { + AuthenticatedMachineObject, + SignedInAuthObject, + SignedOutAuthObject, + UnauthenticatedMachineObject, +} from '@clerk/backend/internal'; +import { + authenticatedMachineObject, + AuthStatus, + constants, + signedInAuthObject, + signedOutAuthObject, + unauthenticatedMachineObject, +} from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; import type { LoggerNoCommit } from '../../utils/debugLogger'; @@ -11,10 +24,21 @@ import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, g * Given a request object, builds an auth object from the request data. Used in server-side environments to get access * to auth data for a given request. */ +export type GetAuthDataFromRequestOptions = { + secretKey?: string; + logger?: LoggerNoCommit; + entity?: 'user' | 'machine'; +}; export function getAuthDataFromRequest( req: RequestLike, - opts: { secretKey?: string; logger?: LoggerNoCommit } = {}, -): AuthObject { + opts: GetAuthDataFromRequestOptions & { entity: 'machine' }, +): Exclude; +export function getAuthDataFromRequest( + req: RequestLike, + opts: GetAuthDataFromRequestOptions & { entity: 'user' }, +): Exclude; +export function getAuthDataFromRequest(req: RequestLike, opts?: GetAuthDataFromRequestOptions): AuthObject; +export function getAuthDataFromRequest(req: RequestLike, opts: GetAuthDataFromRequestOptions = {}) { const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); const authToken = getAuthKeyFromRequest(req, 'AuthToken'); const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); @@ -39,7 +63,16 @@ export function getAuthDataFromRequest( opts.logger?.debug('auth options', options); let authObject; - if (!authStatus || authStatus !== AuthStatus.SignedIn) { + if (opts.entity === 'machine' && (!authStatus || authStatus !== AuthStatus.MachineAuthenticated)) { + authObject = unauthenticatedMachineObject(options); + } else if (opts.entity === 'machine' && authStatus === AuthStatus.MachineAuthenticated) { + assertTokenSignature(authToken as string, options.secretKey, authSignature); + + const jwt = decodeJwt(authToken as string); + + opts.logger?.debug('jwt', jwt.raw); + authObject = authenticatedMachineObject(jwt.raw.text, jwt.payload); + } else if (!authStatus || authStatus !== AuthStatus.SignedIn) { authObject = signedOutAuthObject(options); } else { assertTokenSignature(authToken as string, options.secretKey, authSignature); diff --git a/packages/react-router/src/ssr/authenticateRequest.ts b/packages/react-router/src/ssr/authenticateRequest.ts index 27c23da94e..37a4552d75 100644 --- a/packages/react-router/src/ssr/authenticateRequest.ts +++ b/packages/react-router/src/ssr/authenticateRequest.ts @@ -1,5 +1,11 @@ import { createClerkClient } from '@clerk/backend'; -import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal'; +import type { + AuthenticateRequestOptions, + MachineAuthenticatedState, + MachineUnauthenticatedState, + SignedInState, + SignedOutState, +} from '@clerk/backend/internal'; import { AuthStatus } from '@clerk/backend/internal'; import type { LoaderFunctionArgs } from './types'; @@ -8,7 +14,7 @@ import { patchRequest } from './utils'; export async function authenticateRequest( args: LoaderFunctionArgs, opts: AuthenticateRequestOptions, -): Promise { +): Promise { const { request } = args; const { audience, authorizedParties } = opts; diff --git a/packages/remix/src/ssr/authenticateRequest.ts b/packages/remix/src/ssr/authenticateRequest.ts index 27c23da94e..37a4552d75 100644 --- a/packages/remix/src/ssr/authenticateRequest.ts +++ b/packages/remix/src/ssr/authenticateRequest.ts @@ -1,5 +1,11 @@ import { createClerkClient } from '@clerk/backend'; -import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal'; +import type { + AuthenticateRequestOptions, + MachineAuthenticatedState, + MachineUnauthenticatedState, + SignedInState, + SignedOutState, +} from '@clerk/backend/internal'; import { AuthStatus } from '@clerk/backend/internal'; import type { LoaderFunctionArgs } from './types'; @@ -8,7 +14,7 @@ import { patchRequest } from './utils'; export async function authenticateRequest( args: LoaderFunctionArgs, opts: AuthenticateRequestOptions, -): Promise { +): Promise { const { request } = args; const { audience, authorizedParties } = opts; diff --git a/packages/tanstack-start/src/server/authenticateRequest.ts b/packages/tanstack-start/src/server/authenticateRequest.ts index 0ffbf68068..e2490addd2 100644 --- a/packages/tanstack-start/src/server/authenticateRequest.ts +++ b/packages/tanstack-start/src/server/authenticateRequest.ts @@ -1,5 +1,11 @@ import { createClerkClient } from '@clerk/backend'; -import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal'; +import type { + AuthenticateRequestOptions, + MachineAuthenticatedState, + MachineUnauthenticatedState, + SignedInState, + SignedOutState, +} from '@clerk/backend/internal'; import { AuthStatus } from '@clerk/backend/internal'; import { errorThrower } from '../utils'; @@ -8,7 +14,7 @@ import { patchRequest } from './utils'; export async function authenticateRequest( request: Request, opts: AuthenticateRequestOptions, -): Promise { +): Promise { const { audience, authorizedParties } = opts; const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;