diff --git a/src/admin-portal/admin-portal.spec.ts b/src/admin-portal/admin-portal.spec.ts index 3e777c231..57fc75ec2 100644 --- a/src/admin-portal/admin-portal.spec.ts +++ b/src/admin-portal/admin-portal.spec.ts @@ -1,5 +1,5 @@ import fetch from 'jest-fetch-mock'; -import { fetchBody, fetchOnce } from '../common/utils/test-utils'; +import { fetchBody, fetchOnce, fetchURL } from '../common/utils/test-utils'; import { WorkOS } from '../workos'; import generateLinkInvalid from './fixtures/generate-link-invalid.json'; import portalLinkResponse from './fixtures/portal-link-response.json'; @@ -193,6 +193,24 @@ describe('AdminPortal', () => { }); }); + describe('with an externalUserId', () => { + it('sends it as external_user_id', async () => { + fetchOnce(portalLinkResponse, { status: 201 }); + + await workos.adminPortal.generateLink({ + intent: GenerateLinkIntent.SSO, + organization: 'org_01EHQMYV6MBK39QC5PZXHY59C3', + externalUserId: 'user_12345', + }); + + expect(fetchBody()).toEqual({ + intent: GenerateLinkIntent.SSO, + organization: 'org_01EHQMYV6MBK39QC5PZXHY59C3', + external_user_id: 'user_12345', + }); + }); + }); + describe('with an invalid organization', () => { it('throws an error', async () => { fetchOnce(generateLinkInvalid, { @@ -241,4 +259,22 @@ describe('AdminPortal', () => { }); }); }); + + describe('revokeLink', () => { + it('sends a revoke_link request', async () => { + fetchOnce(); + + const resp = await workos.adminPortal.revokeLink({ + organization: 'org_01EHQMYV6MBK39QC5PZXHY59C3', + externalUserId: 'user_12345', + }); + + expect(fetchURL()).toContain('/portal/revoke_link'); + expect(fetchBody()).toEqual({ + organization: 'org_01EHQMYV6MBK39QC5PZXHY59C3', + external_user_id: 'user_12345', + }); + expect(resp).toBeUndefined(); + }); + }); }); diff --git a/src/admin-portal/admin-portal.ts b/src/admin-portal/admin-portal.ts index aa242d72b..984ef950f 100644 --- a/src/admin-portal/admin-portal.ts +++ b/src/admin-portal/admin-portal.ts @@ -10,7 +10,9 @@ export class AdminPortal { * Generate a Portal Link * * Generate a Portal Link scoped to an Organization. - * @param payload - Object containing organization. + * @param payload - Object containing organization. Pass `externalUserId` + * for every admin you may later need to deprovision; only links generated + * with one can be revoked via `revokeLink`. * @returns {Promise<{ link: string; }>} * @throws {BadRequestException} 400 * @throws 403 response from the API. @@ -24,6 +26,7 @@ export class AdminPortal { successUrl, intentOptions, itContactEmails, + externalUserId, }: { intent?: GenerateLinkIntent; organization: string; @@ -36,6 +39,7 @@ export class AdminPortal { }; }; itContactEmails?: string[]; + externalUserId?: string; }): Promise<{ link: string }> { const { data } = await this.workos.post('/portal/generate_link', { intent, @@ -51,8 +55,36 @@ export class AdminPortal { } : undefined, it_contact_emails: itContactEmails, + external_user_id: externalUserId, }); return data; } + + /** + * Revoke Portal Links + * + * Revokes every Portal Link generated for the given organization with the + * given `externalUserId`, and immediately ends any Admin Portal sessions that + * were started from those links. Use this when an admin is deprovisioned from + * your application: unopened links stop working, and their already-open + * portal sessions are ended. Only links that were generated with an + * `externalUserId` can be revoked, so pass one to `generateLink` for every + * admin you may later need to deprovision. + * @param payload - Object containing organization and externalUserId. + * @throws {NotFoundException} 404 + * @throws {UnprocessableEntityException} 422 + */ + async revokeLink({ + organization, + externalUserId, + }: { + organization: string; + externalUserId: string; + }): Promise { + await this.workos.post('/portal/revoke_link', { + organization, + external_user_id: externalUserId, + }); + } } diff --git a/src/admin-portal/fixtures/generate-link.json b/src/admin-portal/fixtures/generate-link.json index 8fe99551a..7385a4446 100644 --- a/src/admin-portal/fixtures/generate-link.json +++ b/src/admin-portal/fixtures/generate-link.json @@ -9,5 +9,6 @@ "provider_type": "GoogleSAML" } }, - "it_contact_emails": ["it-contact@example.com"] + "it_contact_emails": ["it-contact@example.com"], + "external_user_id": "user_12345" } diff --git a/src/admin-portal/interfaces/generate-link.interface.ts b/src/admin-portal/interfaces/generate-link.interface.ts index 2a9ce229b..e52c69bf7 100644 --- a/src/admin-portal/interfaces/generate-link.interface.ts +++ b/src/admin-portal/interfaces/generate-link.interface.ts @@ -29,6 +29,8 @@ export interface GenerateLink { intentOptions?: IntentOptions; /** The email addresses of the IT contacts to grant access to the Admin Portal for the given organization. Accepts up to 20 emails. */ itContactEmails?: string[]; + /** The ID of the user in your application the link is generated for. The link and any Admin Portal sessions started from it can later be revoked by this ID, for example when that user is deprovisioned from your application. */ + externalUserId?: string; } export interface GenerateLinkResponse { @@ -38,4 +40,5 @@ export interface GenerateLinkResponse { intent?: GenerateLinkIntent; intent_options?: IntentOptionsResponse; it_contact_emails?: string[]; + external_user_id?: string; } diff --git a/src/admin-portal/serializers/generate-link.serializer.ts b/src/admin-portal/serializers/generate-link.serializer.ts index 7a11d6b40..a58cede85 100644 --- a/src/admin-portal/serializers/generate-link.serializer.ts +++ b/src/admin-portal/serializers/generate-link.serializer.ts @@ -21,6 +21,7 @@ export const deserializeGenerateLink = ( ? deserializeIntentOptions(response.intent_options) : undefined, itContactEmails: response.it_contact_emails, + externalUserId: response.external_user_id, }); export const serializeGenerateLink = ( @@ -35,4 +36,5 @@ export const serializeGenerateLink = ( ? serializeIntentOptions(model.intentOptions) : undefined, it_contact_emails: model.itContactEmails, + external_user_id: model.externalUserId, });