Skip to content

Commit

Permalink
machine to machine v1
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobevangelista committed Jan 13, 2025
1 parent 84867be commit 89949d4
Show file tree
Hide file tree
Showing 23 changed files with 701 additions and 30 deletions.
2 changes: 2 additions & 0 deletions packages/backend/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('subpath /internal exports', () => {
expect(Object.keys(internalExports).sort()).toMatchInlineSnapshot(`
[
"AuthStatus",
"authenticatedMachineObject",
"constants",
"createAuthenticateRequest",
"createClerkRequest",
Expand All @@ -49,6 +50,7 @@ describe('subpath /internal exports', () => {
"signedInAuthObject",
"signedOutAuthObject",
"stripPrivateDataFromObject",
"unauthenticatedMachineObject",
]
`);
});
Expand Down
24 changes: 24 additions & 0 deletions packages/backend/src/api/endpoints/MachineTokensApi.ts
Original file line number Diff line number Diff line change
@@ -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<MachineToken>({
method: 'POST',
path: basePath,
bodyParams: params,
});
}
}
1 change: 1 addition & 0 deletions packages/backend/src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DomainAPI,
EmailAddressAPI,
InvitationAPI,
MachineTokensAPI,
OrganizationAPI,
PhoneNumberAPI,
RedirectUrlAPI,
Expand All @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/api/resources/MachineToken.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/api/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
31 changes: 31 additions & 0 deletions packages/backend/src/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = {
Expand All @@ -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';

Expand Down
24 changes: 21 additions & 3 deletions packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
24 changes: 23 additions & 1 deletion packages/backend/src/tokens/__tests__/authObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
41 changes: 40 additions & 1 deletion packages/backend/src/tokens/__tests__/authStatus.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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: '/' });
Expand Down
Loading

0 comments on commit 89949d4

Please sign in to comment.