Skip to content

feat(backend): *WIP* Add machine authentication support #5689

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/agent-toolkit/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AuthObject, ClerkClient } from '@clerk/backend';
import type { ClerkClient } from '@clerk/backend';
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';

import type { ClerkTool } from './clerk-tool';

Expand All @@ -12,7 +13,7 @@ export type ToolkitParams = {
* @default {}
*/
authContext?: Pick<
AuthObject,
SignedInAuthObject | SignedOutAuthObject,
'userId' | 'sessionId' | 'sessionClaims' | 'orgId' | 'orgRole' | 'orgSlug' | 'orgPermissions' | 'actor'
>;
/**
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ describe('subpath /errors exports', () => {
it('should not include a breaking change', () => {
expect(Object.keys(errorExports).sort()).toMatchInlineSnapshot(`
[
"MachineTokenVerificationError",
"MachineTokenVerificationErrorCode",
"SignJWTError",
"TokenVerificationError",
"TokenVerificationErrorAction",
Expand All @@ -37,18 +39,24 @@ describe('subpath /internal exports', () => {
expect(Object.keys(internalExports).sort()).toMatchInlineSnapshot(`
[
"AuthStatus",
"authenticatedMachineObject",
"constants",
"createAuthenticateRequest",
"createClerkRequest",
"createRedirect",
"debugRequestState",
"decorateObjectWithResources",
"getMachineTokenType",
"isMachineToken",
"isTokenTypeAccepted",
"makeAuthObjectSerializable",
"reverificationError",
"reverificationErrorResponse",
"signedInAuthObject",
"signedOutAuthObject",
"stripPrivateDataFromObject",
"unauthenticatedMachineObject",
"verifyMachineAuthToken",
]
`);
});
Expand Down
15 changes: 15 additions & 0 deletions packages/backend/src/api/endpoints/APIKeysApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { joinPaths } from '../../util/path';
import type { APIKey } from '../resources/APIKey';
import { AbstractAPI } from './AbstractApi';

const basePath = '/api_keys';

export class APIKeysAPI extends AbstractAPI {
async verifySecret(secret: string) {
return this.request<APIKey>({
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
});
}
}
15 changes: 15 additions & 0 deletions packages/backend/src/api/endpoints/IdPOAuthAccessTokenApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { joinPaths } from '../../util/path';
import type { IdPOAuthAccessToken } from '../resources';
import { AbstractAPI } from './AbstractApi';

const basePath = '/oauth_applications/access_tokens';

export class IdPOAuthAccessTokenApi extends AbstractAPI {
async verifySecret(secret: string) {
return this.request<IdPOAuthAccessToken>({
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
});
}
}
15 changes: 15 additions & 0 deletions packages/backend/src/api/endpoints/MachineTokensApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { joinPaths } from '../../util/path';
import type { MachineToken } from '../resources/MachineToken';
import { AbstractAPI } from './AbstractApi';

const basePath = '/m2m_tokens';

export class MachineTokensApi extends AbstractAPI {
async verifySecret(secret: string) {
return this.request<MachineToken>({
method: 'POST',
path: joinPaths(basePath, 'verify'),
bodyParams: { secret },
});
}
}
3 changes: 3 additions & 0 deletions packages/backend/src/api/endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ export * from './ActorTokenApi';
export * from './AccountlessApplicationsAPI';
export * from './AbstractApi';
export * from './AllowlistIdentifierApi';
export * from './APIKeysApi';
export * from './BetaFeaturesApi';
export * from './BlocklistIdentifierApi';
export * from './ClientApi';
export * from './DomainApi';
export * from './EmailAddressApi';
export * from './IdPOAuthAccessTokenApi';
export * from './InstanceApi';
export * from './InvitationApi';
export * from './MachineTokensApi';
export * from './JwksApi';
export * from './JwtTemplatesApi';
export * from './OrganizationApi';
Expand Down
22 changes: 22 additions & 0 deletions packages/backend/src/api/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import {
AccountlessApplicationAPI,
ActorTokenAPI,
AllowlistIdentifierAPI,
APIKeysAPI,
BetaFeaturesAPI,
BlocklistIdentifierAPI,
ClientAPI,
DomainAPI,
EmailAddressAPI,
IdPOAuthAccessTokenApi,
InstanceAPI,
InvitationAPI,
JwksAPI,
JwtTemplatesApi,
MachineTokensApi,
OAuthApplicationsApi,
OrganizationAPI,
PhoneNumberAPI,
Expand Down Expand Up @@ -47,6 +50,25 @@ export function createBackendApiClient(options: CreateBackendApiOptions) {
emailAddresses: new EmailAddressAPI(request),
instance: new InstanceAPI(request),
invitations: new InvitationAPI(request),
// TODO: Remove this once we add a version to bapi-proxy
machineTokens: new MachineTokensApi(
buildRequest({
...options,
apiVersion: '/',
}),
),
idPOAuthAccessToken: new IdPOAuthAccessTokenApi(
buildRequest({
...options,
apiVersion: '/',
}),
),
apiKeys: new APIKeysAPI(
buildRequest({
...options,
apiVersion: '/',
}),
),
jwks: new JwksAPI(request),
jwtTemplates: new JwtTemplatesApi(request),
oauthApplications: new OAuthApplicationsApi(request),
Expand Down
41 changes: 41 additions & 0 deletions packages/backend/src/api/resources/APIKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { APIKeyJSON } from './JSON';

export class APIKey {
constructor(
readonly id: string,
readonly type: string,
readonly name: string,
readonly subject: string,
readonly scopes: string[],
readonly claims: Record<string, string> | null,
readonly revoked: boolean,
readonly revocationReason: string | null,
readonly expired: boolean,
readonly expiration: number | null,
readonly createdBy: string | null,
readonly creationReason: string | null,
readonly secondsUntilExpiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
) {}

static fromJSON(data: APIKeyJSON) {
return new APIKey(
data.id,
data.type,
data.name,
data.subject,
data.scopes,
data.claims,
data.revoked,
data.revocation_reason,
data.expired,
data.expiration,
data.created_by,
data.creation_reason,
data.seconds_until_expiration,
data.created_at,
data.updated_at,
);
}
}
9 changes: 9 additions & 0 deletions packages/backend/src/api/resources/Deserializer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {
ActorToken,
AllowlistIdentifier,
APIKey,
BlocklistIdentifier,
Client,
Cookies,
DeletedObject,
Domain,
Email,
EmailAddress,
IdPOAuthAccessToken,
Instance,
InstanceRestrictions,
InstanceSettings,
Invitation,
JwtTemplate,
MachineToken,
OauthAccessToken,
OAuthApplication,
Organization,
Expand Down Expand Up @@ -85,6 +88,8 @@ function jsonToObject(item: any): any {
return ActorToken.fromJSON(item);
case ObjectType.AllowlistIdentifier:
return AllowlistIdentifier.fromJSON(item);
case ObjectType.ApiKey:
return APIKey.fromJSON(item);
case ObjectType.BlocklistIdentifier:
return BlocklistIdentifier.fromJSON(item);
case ObjectType.Client:
Expand All @@ -97,6 +102,8 @@ function jsonToObject(item: any): any {
return EmailAddress.fromJSON(item);
case ObjectType.Email:
return Email.fromJSON(item);
case ObjectType.IdpOAuthAccessToken:
return IdPOAuthAccessToken.fromJSON(item);
case ObjectType.Instance:
return Instance.fromJSON(item);
case ObjectType.InstanceRestrictions:
Expand All @@ -107,6 +114,8 @@ function jsonToObject(item: any): any {
return Invitation.fromJSON(item);
case ObjectType.JwtTemplate:
return JwtTemplate.fromJSON(item);
case ObjectType.MachineToken:
return MachineToken.fromJSON(item);
case ObjectType.OauthAccessToken:
return OauthAccessToken.fromJSON(item);
case ObjectType.OAuthApplication:
Expand Down
35 changes: 35 additions & 0 deletions packages/backend/src/api/resources/IdPOAuthAccessToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { IdPOAuthAccessTokenJSON } from './JSON';

export class IdPOAuthAccessToken {
constructor(
readonly id: string,
readonly clientId: string,
readonly type: string,
readonly name: string,
readonly subject: string,
readonly scopes: string[],
readonly revoked: boolean,
readonly revocationReason: string | null,
readonly expired: boolean,
readonly expiration: number | null,
readonly createdAt: number,
readonly updatedAt: number,
) {}

static fromJSON(data: IdPOAuthAccessTokenJSON) {
return new IdPOAuthAccessToken(
data.id,
data.client_id,
data.type,
data.name,
data.subject,
data.scopes,
data.revoked,
data.revocation_reason,
data.expired,
data.expiration,
data.created_at,
data.updated_at,
);
}
}
52 changes: 52 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 = {
AccountlessApplication: 'accountless_application',
ActorToken: 'actor_token',
AllowlistIdentifier: 'allowlist_identifier',
ApiKey: 'api_key',
BlocklistIdentifier: 'blocklist_identifier',
Client: 'client',
Cookies: 'cookies',
Expand All @@ -33,8 +34,10 @@ export const ObjectType = {
InstanceRestrictions: 'instance_restrictions',
InstanceSettings: 'instance_settings',
Invitation: 'invitation',
MachineToken: 'machine_token',
JwtTemplate: 'jwt_template',
OauthAccessToken: 'oauth_access_token',
IdpOAuthAccessToken: 'clerk_idp_oauth_access_token',
OAuthApplication: 'oauth_application',
Organization: 'organization',
OrganizationDomain: 'organization_domain',
Expand Down Expand Up @@ -672,6 +675,55 @@ export interface SamlAccountConnectionJSON extends ClerkResourceJSON {
updated_at: number;
}

export interface MachineTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.MachineToken;
name: string;
subject: string;
scopes: string[];
claims: Record<string, string> | null;
revoked: boolean;
revocation_reason: string | null;
expired: boolean;
expiration: number | null;
created_by: string | null;
creation_reason: string | null;
created_at: number;
updated_at: number;
}

export interface APIKeyJSON extends ClerkResourceJSON {
object: typeof ObjectType.ApiKey;
type: string;
name: string;
subject: string;
scopes: string[];
claims: Record<string, string> | null;
revoked: boolean;
revocation_reason: string | null;
expired: boolean;
expiration: number | null;
created_by: string | null;
creation_reason: string | null;
seconds_until_expiration: number | null;
created_at: number;
updated_at: number;
}

export interface IdPOAuthAccessTokenJSON extends ClerkResourceJSON {
object: typeof ObjectType.IdpOAuthAccessToken;
client_id: string;
type: string;
name: string;
subject: string;
scopes: string[];
revoked: boolean;
revocation_reason: string | null;
expired: boolean;
expiration: number | null;
created_at: number;
updated_at: number;
}

export interface WebhooksSvixJSON {
svix_url: string;
}
37 changes: 37 additions & 0 deletions packages/backend/src/api/resources/MachineToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { MachineTokenJSON } from './JSON';

export class MachineToken {
constructor(
readonly id: string,
readonly name: string,
readonly subject: string,
readonly scopes: string[],
readonly claims: Record<string, string> | null,
readonly revoked: boolean,
readonly revocationReason: string | null,
readonly expired: boolean,
readonly expiration: number | null,
readonly createdBy: string | null,
readonly creationReason: string | null,
readonly createdAt: number,
readonly updatedAt: number,
) {}

static fromJSON(data: MachineTokenJSON) {
return new MachineToken(
data.id,
data.name,
data.subject,
data.scopes,
data.claims,
data.revoked,
data.revocation_reason,
data.expired,
data.expiration,
data.created_by,
data.creation_reason,
data.created_at,
data.updated_at,
);
}
}
Loading
Loading