Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
89949d4
machine to machine v1
jakobevangelista Jan 13, 2025
ea44d14
added message to catch machine token to user auth and added changeset
jakobevangelista Jan 14, 2025
aa444ce
ready for snapshot
jakobevangelista Jan 22, 2025
95ce1be
removed unnecessary comments
jakobevangelista Jan 23, 2025
a2e74fe
added any to auth
jakobevangelista Jan 29, 2025
7d5abb8
refactor, handle empty authStatus
brkalow Jan 29, 2025
914d92c
export EntityTypes
brkalow Jan 29, 2025
779f7c6
handle case where token isn't present
brkalow Jan 29, 2025
ed1c5c9
Delete packages/react/tsconfig.vitest-temp.json
brkalow Jan 29, 2025
28b9bda
More changes for entity types
jakobevangelista Jan 31, 2025
d8937d6
fixed merge conflicts
jakobevangelista Feb 3, 2025
29b622b
Merge branch 'jakob/m2m-fix-nextjs' of github.com:clerk/javascript in…
jakobevangelista Feb 3, 2025
0c5af6e
Merge branch 'main' of github.com:clerk/javascript into jakob/m2m-fix…
jakobevangelista Feb 4, 2025
3517c5c
fixed error messages and finished all tests
jakobevangelista Feb 4, 2025
02d907b
added comments and removed unneccessary state
jakobevangelista Feb 4, 2025
7e8be65
Merge branch 'main' of github.com:clerk/javascript into jakob/m2m-fix…
jakobevangelista Feb 4, 2025
1cf7f05
Merge branch 'main' of github.com:clerk/javascript into jakob/m2m-fix…
jakobevangelista Feb 6, 2025
1c3be0b
Merge branch 'main' of github.com:clerk/javascript into jakob/m2m-fix…
jakobevangelista Feb 12, 2025
0de9806
changed claims to machineClaims
jakobevangelista Feb 12, 2025
d0d7d0b
middleware changes for all the fullstack frameworks
jakobevangelista Feb 13, 2025
2810822
Merge branch 'main' of github.com:clerk/javascript into jakob/m2m-fix…
jakobevangelista Feb 18, 2025
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/thin-stingrays-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
---

Added machine to machine auth
2 changes: 1 addition & 1 deletion packages/astro/src/server/clerk-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {

const requestState = await clerkClient(context).authenticateRequest(
clerkRequest,
createAuthenticateRequestOptions(clerkRequest, options, context),
createAuthenticateRequestOptions(clerkRequest, { ...options, entity: 'any' }, context),
);

const locationHeader = requestState.headers.get(constants.Headers.Location);
Expand Down
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 }),
),
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 @@ -20,6 +20,7 @@ export const ObjectType = {
FacebookAccount: 'facebook_account',
GoogleAccount: 'google_account',
Invitation: 'invitation',
MachineToken: 'machine_token',
OauthAccessToken: 'oauth_access_token',
Organization: 'organization',
OrganizationDomain: 'organization_domain',
Expand Down Expand Up @@ -124,6 +125,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 jwt: 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 @@ -20,6 +20,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
2 changes: 2 additions & 0 deletions packages/backend/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const TokenVerificationErrorReason = {
RemoteJWKMissing: 'jwk-remote-missing',
JWKFailedToResolve: 'jwk-failed-to-resolve',
JWKKidMismatch: 'jwk-kid-mismatch',
MachineTokenUsedForUserRequest: 'machine-token-used-for-user-request',
UserTokenUsedForMachineRequest: 'user-token-used-for-machine-request',
};

export type TokenVerificationErrorReason =
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
28 changes: 23 additions & 5 deletions packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@ export { createAuthenticateRequest } from './tokens/factory';

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 { AuthenticateRequestOptions, OrganizationSyncOptions, EntityTypes } from './tokens/types';

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