Skip to content

Commit 237e875

Browse files
WIP
1 parent ba54cd8 commit 237e875

File tree

15 files changed

+576
-13
lines changed

15 files changed

+576
-13
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { MachineToken } from '../resources';
2+
import { AbstractAPI } from './AbstractApi';
3+
4+
interface MachineTokensClaims {
5+
[k: string]: unknown;
6+
}
7+
8+
type CreateMachineTokensParams = {
9+
machineId: string;
10+
claims?: MachineTokensClaims;
11+
expiresInSeconds?: number;
12+
allowedClockSkew?: number;
13+
};
14+
15+
const basePath = '/machine_tokens';
16+
export class MachineTokensAPI extends AbstractAPI {
17+
public async create(params: CreateMachineTokensParams) {
18+
return this.request<MachineToken>({
19+
method: 'POST',
20+
path: basePath,
21+
bodyParams: params,
22+
});
23+
}
24+
}

packages/backend/src/api/endpoints/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './ClientApi';
55
export * from './DomainApi';
66
export * from './EmailAddressApi';
77
export * from './InvitationApi';
8+
export * from './MachineTokensApi';
89
export * from './OrganizationApi';
910
export * from './PhoneNumberApi';
1011
export * from './RedirectUrlApi';

packages/backend/src/api/factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DomainAPI,
66
EmailAddressAPI,
77
InvitationAPI,
8+
MachineTokensAPI,
89
OrganizationAPI,
910
PhoneNumberAPI,
1011
RedirectUrlAPI,
@@ -27,6 +28,7 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
2728
__experimental_accountlessApplications: new AccountlessApplicationAPI(
2829
buildRequest({ ...options, requireSecretKey: false }),
2930
),
31+
__internal_machineTokens: new MachineTokensAPI(request),
3032
allowlistIdentifiers: new AllowlistIdentifierAPI(request),
3133
clients: new ClientAPI(request),
3234
emailAddresses: new EmailAddressAPI(request),

packages/backend/src/api/resources/JSON.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const ObjectType = {
1616
FacebookAccount: 'facebook_account',
1717
GoogleAccount: 'google_account',
1818
Invitation: 'invitation',
19+
MachineToken: 'machine_token',
1920
OauthAccessToken: 'oauth_access_token',
2021
Organization: 'organization',
2122
OrganizationInvitation: 'organization_invitation',
@@ -114,6 +115,11 @@ export interface ExternalAccountJSON extends ClerkResourceJSON {
114115
verification: VerificationJSON | null;
115116
}
116117

118+
export interface MachineTokenJSON {
119+
object: typeof ObjectType.MachineToken;
120+
jwt: string;
121+
}
122+
117123
export interface SamlAccountJSON extends ClerkResourceJSON {
118124
object: typeof ObjectType.SamlAccount;
119125
provider: string;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { MachineTokenJSON } from './JSON';
2+
3+
export class MachineToken {
4+
constructor(readonly token: string) {}
5+
6+
static fromJSON(data: MachineTokenJSON): MachineToken {
7+
return new MachineToken(data.jwt);
8+
}
9+
}

packages/backend/src/api/resources/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export * from './ExternalAccount';
1919
export * from './IdentificationLink';
2020
export * from './Invitation';
2121
export * from './JSON';
22+
export * from './MachineToken';
2223
export * from './OauthAccessToken';
2324
export * from './Organization';
2425
export * from './OrganizationInvitation';

packages/backend/src/fixtures/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { base64url } from '../util/rfc4648';
44
export const mockJwt =
55
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.j3rB92k32WqbQDkFB093H4GoQsBVLH4HLGF6ObcwUaVGiHC8SEu6T31FuPf257SL8A5sSGtWWM1fqhQpdLohgZb_hbJswGBuYI-Clxl9BtpIRHbWFZkLBIj8yS9W9aVtD3fWBbF6PHx7BY1udio-rbGWg1YAOZNtVcxF02p-MvX-8XIK92Vwu3Un5zyfCoVIg__qo3Xntzw3tznsZ4XDe212c6kVz1R_L1d5DKjeWXpjUPAS_zFeZSIJEQLf4JNr4JCY38tfdnc3ajfDA3p36saf1XwmTdWXQKCXi75c2TJAXROs3Pgqr5Kw_5clygoFuxN5OEMhFWFSnvIBdi3M6w';
66

7+
export const mockMachineJwt =
8+
'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDIyMkFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjIwNDQ0MzU4MTIsImlhdCI6MTczMzg2NjI5MiwiaXNzIjoiaHR0cHM6Ly9zYWZlLWJlYWdsZS0zMi5jbGVyay5hY2NvdW50c3N0YWdlLmRldiIsImp0aSI6IjA2NTAwNTBkNzlhMDZlNjhjNTY1IiwibmJmIjoxNzMzODY1OTkyLCJzdWIiOiJtY2hfdGVzdCJ9.oM7RTA4j-WWF9zFbWq0QCepSC4Lysq9rPuNYDVBYJg_mw1viXRYhbQO5q2_Tsvncshm1JSwvTilHwnGokuBAT1F4wpRwGn22Fd4w-GkyKq6sYMVpvnIQOQdQB2OeZbxqYujtwVuT67vwV_vt4jjTFMI8c4AXG9P8aIckEjys2txx79eY1CgdILKGaMXsWqOy5vkKboIdktWO8bUhca6ESb2HnU4k5SgZepkjNPJq_Ei1IOQBzsotZ7_HJaqiZgvWhtWv_buJ-JH-VtFiDN6HUbqS4yF9K4krqo-6g5nsok_kXLzPH1iVdCPhcjo-34Wx1lwIR035SHjI9BaNaJYXvg';
9+
10+
export const mockExpiredMachineJwt =
11+
'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDIyMkFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJkZWV6IjoibnV0cyIsImV4cCI6MTczMzc5MjgwOCwiaWF0IjoxNzMzNzkyMjA4LCJpc3MiOiJodHRwczovL3NhZmUtYmVhZ2xlLTMyLmNsZXJrLmFjY291bnRzc3RhZ2UuZGV2IiwianRpIjoiMzY3OTAyNzViZGY0OWZiZDBiNTUiLCJuYmYiOjE3MzM3OTIyMDMsInN1YiI6Im1jaF90ZXN0In0.FwqEfAZsY0vmV7tWU9vJ6VkAKBzFHUOqX6MTkMXGtUzzYaR7eHPnZpDhAb9wizM2xeCbUX4gNe8znNeKRYHJEvmtEjg-PPkPxveIl8PI5ZNF1rAceDL0T0F3MyMJOX34KcyOH99c-CUEpcvezahH2qGb6STMKdb29AQa-fyCNnP4_VqHNqFapwFcweeCUJSGXSo4N4Qcmadm7wvqwOQMbyOkGXJdna2a4quWTM7OdxWwXShGotUlmYzr3kejHXyjUtJ4j7m6g9huADaj9r7lC4VX6dykV115GTd6uExLA8ZS7pd4fuxoOS9sbEMNyKPS9cEZIOA1Xvf8njgcqn8fhw';
12+
13+
export const mockUserTokenForMachineTesting =
14+
'eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDExMUFBQSIsImtpZCI6Imluc18yb2FpV0IzUENJNlZsOVRKOWxZemcwUThyeXkiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjIwNDkyMjkxNjksImZ2YSI6WzIsLTFdLCJpYXQiOjE3MzM4NjkxNjksImlzcyI6Imh0dHBzOi8vc2FmZS1iZWFnbGUtMzIuY2xlcmsuYWNjb3VudHNzdGFnZS5kZXYiLCJuYmYiOjE3MzM4NjkxNTksInNpZCI6InNlc3NfMnEybVN6UEJLUEVOZzdtRVVlMDhsTXZuRUY5Iiwic3ViIjoidXNlcl8ycTJtU3ZvQWU1VmlYYWVYYUJQdFVLamdoSTAifQ.P2wsTzMBB5wAkeUbeNOF2sTSrE0cHD7ICyjYqgM-Ai9ppTsZeDSI8qQNDwqkAFiJ0FWI6PuwtaYiRkcRDxBe7m-KvF6UrRC5zXkBGD9lZEUInisFSAvdW4BFJ78_xWHGRmhAKKWXYjYit66GAN3Ie2dYmlSeE6UmsrA4tCIqZgJgfYZ_ClBF35OA_Q1j26OQT2PGy7qZ7E3cB_YZO7sDaVLr_vZkyVUdb2hODdQlSpU8pyoTRNrZf9nI_MqJwurVuTThEI3TgCbqUuGGxc5xWx16qaupxTkKMo3SehoD5DZMVpG6yyqHuXD_aUE4hHIT8J9qoqSd98j4eZ5z-63Sgw';
15+
716
// signed with signingJwks, same as mockJwt but with lower iat and exp values
817
export const mockExpiredJwt =
918
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODIwMCwiaWF0IjoxNjY2NjQ3MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.jLImjg2vGwOJDkK9gtIeJnEJWVOgoCMeC46OFtfzJ1d8OT0KVvwRppC60QIMfHKoOwLTLYlq8SccrkARwlJ_jOvMAYMGZT-R4qHoEfGmet1cSTC67zaafq5gpf9759x1kNMyckry_PJNSx-9hTFbBMWhY7XVLVlrauppqHXOQr1-BC7u-0InzKjCQTCJj-81Yt8xRKweLbO689oYSRAFYK5LNH8BYoLZFWuWLO-6nxUJu0_XAq9xpZPqZOqj3LxFS4hHVGGmTqnPgR8vBetLXxSLAOBsEyIkeQkOBA03YA6enTNIppmy0XTLgAYmUO_JWOGjjjDQoEojuXtuLRdQHQ';
@@ -33,6 +42,15 @@ export const mockJwtPayload = {
3342
sub: 'user_2GIpXOEpVyJw51rkZn9Kmnc6Sxr',
3443
};
3544

45+
export const mockMachineJwtPayload = {
46+
exp: 2044435812,
47+
iat: 1733866292,
48+
iss: 'https://safe-beagle-32.clerk.accountsstage.dev',
49+
jti: '0650050d79a06e68c565',
50+
nbf: 1733865992,
51+
sub: 'mch_test',
52+
};
53+
3654
export const mockRsaJwkKid = 'ins_2GIoQhbUpy0hX7B2cVkuTMinXoD';
3755

3856
export const mockRsaJwk = {
@@ -48,6 +66,19 @@ export const mockJwks = {
4866
keys: [mockRsaJwk],
4967
};
5068

69+
export const mockMachineRsaJwk = {
70+
use: 'sig',
71+
kty: 'RSA',
72+
kid: 'ins_2oaiWB3PCI6Vl9TJ9lYzg0Q8ryy',
73+
alg: 'RS256',
74+
n: 'wMAr7X1GzgyocS74bYe8uEQ3yLRGb91qdsfd7cRAQ6fiZca7wkOQRhud5EV9JlmDcHqElR2q_ZLFjrtkQo1nSgPhvc70hlha4ScKWrmS_LFcaz-oLBTUUi4k4zbvv6LThLmNGbEO88OttSy4tOMQMsyIQJD32aN1MHQLcS9Jnd70ZD4q6wEUAznyS0QPpLwd3X5TUTan9kUoUHw9t4-FzNFQJ_t_xKMVkw2BIr9n4fEOBl-UjLh1frFVmOWMC5ygpZ9A_19qEKgVDNKgRHoYN4sHH6y8pKPEMgUce2Ee10RMtw9rLpK1JQDb4iXN_Dxd7QUfh62aUFRNIuePWLbHIw',
75+
e: 'AQAB',
76+
};
77+
78+
export const mockMachineJwks = {
79+
keys: [mockMachineRsaJwk],
80+
};
81+
5182
export const mockPEMKey =
5283
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8Z1oLQbaYkakUSIYRvjmOoeXMDFFjynGP2+gVy0mQJHYgVhgo34RsQgZoz7rSNm/EOL+l/mHTqQAhwaf9Ef8X5vsPX8vP3RNRRm3XYpbIGbOcANJaHihJZwnzG9zIGYF8ki+m55zftO7pkOoXDtIqCt+5nIUQjGJK5axFELrnWaz2qcR03A7rYKQc3F1gut2Ru1xfmiJVUlQe0tLevQO/FzfYpWu7+691q+ZRUGxWvGc0ays4ACa7JXElCIKXRv/yb3Vc1iry77HRAQ28J7Fqpj5Cb+sxfFI+Vhf1GB1bNeOLPR10nkSMJ74HB0heHi/SsM83JiGekv0CpZPCC8jcQIDAQAB';
5384

packages/backend/src/internal.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,29 @@ export { debugRequestState } from './tokens/request';
99

1010
export type { AuthenticateRequestOptions, OrganizationSyncOptions } from './tokens/types';
1111

12-
export type { SignedInAuthObjectOptions, SignedInAuthObject, SignedOutAuthObject } from './tokens/authObjects';
13-
export { makeAuthObjectSerializable, signedOutAuthObject, signedInAuthObject } from './tokens/authObjects';
12+
export type {
13+
SignedInAuthObjectOptions,
14+
SignedInAuthObject,
15+
SignedOutAuthObject,
16+
AuthenticatedMachineObject,
17+
UnauthenticatedMachineObject,
18+
} from './tokens/authObjects';
19+
export {
20+
makeAuthObjectSerializable,
21+
signedOutAuthObject,
22+
signedInAuthObject,
23+
unauthenticatedMachineObject,
24+
authenticatedMachineObject,
25+
} from './tokens/authObjects';
1426

1527
export { AuthStatus } from './tokens/authStatus';
16-
export type { RequestState, SignedInState, SignedOutState } from './tokens/authStatus';
28+
export type {
29+
RequestState,
30+
SignedInState,
31+
SignedOutState,
32+
MachineAuthenticatedState,
33+
MachineUnauthenticatedState,
34+
} from './tokens/authStatus';
1735

1836
export { decorateObjectWithResources, stripPrivateDataFromObject } from './util/decorateObjectWithResources';
1937

packages/backend/src/tokens/__tests__/authObjects.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { JwtPayload } from '@clerk/types';
22
import { describe, expect, it } from 'vitest';
33

44
import type { AuthenticateContext } from '../authenticateContext';
5-
import { makeAuthObjectSerializable, signedInAuthObject, signedOutAuthObject } from '../authObjects';
5+
import {
6+
authenticatedMachineObject,
7+
makeAuthObjectSerializable,
8+
signedInAuthObject,
9+
signedOutAuthObject,
10+
} from '../authObjects';
611

712
describe('makeAuthObjectSerializable', () => {
813
it('removes non-serializable props', () => {
@@ -32,3 +37,20 @@ describe('signedInAuthObject', () => {
3237
expect(token).toBe('token');
3338
});
3439
});
40+
41+
describe('authenticatedMachineObject', () => {
42+
it('getToken returns the token passed in', () => {
43+
const authObject = authenticatedMachineObject('token', {
44+
act: null,
45+
sid: null,
46+
org_id: null,
47+
org_role: null,
48+
org_slug: null,
49+
org_permissions: null,
50+
sub: 'mch_id',
51+
} as unknown as JwtPayload);
52+
53+
const token = authObject.getToken();
54+
expect(token).toBe('token');
55+
});
56+
});

packages/backend/src/tokens/__tests__/authStatus.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { handshake, signedIn, signedOut } from '../authStatus';
3+
import { handshake, machineAuthenticated, machineUnauthenticated, signedIn, signedOut } from '../authStatus';
44

55
describe('signed-in', () => {
66
it('does not include debug headers', () => {
@@ -41,6 +41,45 @@ describe('signed-out', () => {
4141
});
4242
});
4343

44+
describe('machine-unauthenticated', () => {
45+
it('includes debug headers', () => {
46+
const headers = new Headers({ 'custom-header': 'value' });
47+
const authObject = machineUnauthenticated({} as any, 'auth-reason', 'auth-message', headers);
48+
49+
expect(authObject.headers.get('custom-header')).toBe('value');
50+
expect(authObject.headers.get('x-clerk-auth-status')).toBe('machine-unauthenticated');
51+
expect(authObject.headers.get('x-clerk-auth-reason')).toBe('auth-reason');
52+
expect(authObject.headers.get('x-clerk-auth-message')).toBe('auth-message');
53+
});
54+
55+
it('handles debug headers containing invalid unicode characters without throwing', () => {
56+
const headers = new Headers({ 'custom-header': 'value' });
57+
const authObject = machineUnauthenticated({} as any, 'auth-reason+RR�56', 'auth-message+RR�56', headers);
58+
59+
expect(authObject.headers.get('custom-header')).toBe('value');
60+
expect(authObject.headers.get('x-clerk-auth-status')).toBe('machine-unauthenticated');
61+
expect(authObject.headers.get('x-clerk-auth-reason')).toBeNull();
62+
expect(authObject.headers.get('x-clerk-auth-message')).toBeNull();
63+
});
64+
});
65+
66+
describe('machine-authenticated', () => {
67+
it('does not include debug headers', () => {
68+
const authObject = machineAuthenticated({} as any, undefined, 'token', {} as any);
69+
70+
expect(authObject.headers.get('x-clerk-auth-status')).toBeNull();
71+
expect(authObject.headers.get('x-clerk-auth-reason')).toBeNull();
72+
expect(authObject.headers.get('x-clerk-auth-message')).toBeNull();
73+
});
74+
75+
it('authObject returned by toAuth() returns the token passed', async () => {
76+
const signedInAuthObject = signedIn({} as any, { sid: 'sid' } as any, undefined, 'token').toAuth();
77+
const token = await signedInAuthObject.getToken();
78+
79+
expect(token).toBe('token');
80+
});
81+
});
82+
4483
describe('handshake', () => {
4584
it('includes debug headers', () => {
4685
const headers = new Headers({ location: '/' });

0 commit comments

Comments
 (0)