From e8745f16039a11d55852adaa684e4fe322215518 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Thu, 18 Jun 2026 14:11:13 -0400 Subject: [PATCH 1/3] feat(sso): hand-own divergent methods via @oagen-ignore Mark deleteConnection, getConnection, getProfileAndToken, and getProfile as hand-owned so oagen preserves their public signatures (positional id args, CustomAttributesType generics, PKCE/confidential-client token exchange) while still owning listConnections and adopting the new logout endpoints. Import deserializeProfileAndToken from its concrete serializer file since the regenerated barrel only re-exports generated serializers, and add the search filter to ListConnectionsOptions for the generated listConnections serializer. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/sso/interfaces/list-connections-options.interface.ts | 3 +++ src/sso/sso.ts | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/sso/interfaces/list-connections-options.interface.ts b/src/sso/interfaces/list-connections-options.interface.ts index 7059d38d3..1c68e539c 100644 --- a/src/sso/interfaces/list-connections-options.interface.ts +++ b/src/sso/interfaces/list-connections-options.interface.ts @@ -8,10 +8,13 @@ export interface ListConnectionsOptions extends PaginationOptions { domain?: string; /** Filter Connections by their associated organization. */ organizationId?: string; + /** Filter Connections by a search term. */ + search?: string; } export interface SerializedListConnectionsOptions extends PaginationOptions { connection_type?: ConnectionType; domain?: string; organization_id?: string; + search?: string; } diff --git a/src/sso/sso.ts b/src/sso/sso.ts index 5ec2e8ebb..906cf5565 100644 --- a/src/sso/sso.ts +++ b/src/sso/sso.ts @@ -20,9 +20,12 @@ import { import { deserializeConnection, deserializeProfile, - deserializeProfileAndToken, serializeListConnectionsOptions, } from './serializers'; +// Imported from the concrete file rather than the serializer barrel: when SSO +// is oagen-owned the regenerated barrel only re-exports serializers for +// generated methods, and `getProfileAndToken` is hand-owned below. +import { deserializeProfileAndToken } from './serializers/profile-and-token.serializer'; export class SSO { constructor(private readonly workos: WorkOS) {} @@ -56,6 +59,7 @@ export class SSO { options ? serializeListConnectionsOptions(options) : undefined, ); } + // @oagen-ignore-start /** * Delete a Connection * @@ -73,7 +77,6 @@ export class SSO { await this.workos.delete(`/connections/${id}`); } - // @oagen-ignore-start getAuthorizationUrl(options: SSOAuthorizationURLOptions): string { const { codeChallenge, @@ -191,6 +194,7 @@ export class SSO { } // @oagen-ignore-end + // @oagen-ignore-start /** * Get a Connection * @@ -293,4 +297,5 @@ export class SSO { return deserializeProfile(data); } + // @oagen-ignore-end } From beb799de952cb2df8a66e979b71243664287d298 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Thu, 18 Jun 2026 15:07:12 -0400 Subject: [PATCH 2/3] Generate SSO from OpenAPI spec --- .oagen-manifest.json | 52 +- src/events/events.spec.ts | 6 +- src/sso/__snapshots__/sso.spec.ts.snap | 88 -- src/sso/fixtures/connection-domain.json | 5 + src/sso/fixtures/connection-option.json | 3 + src/sso/fixtures/connection.json | 21 + src/sso/fixtures/list-connection.json | 29 + src/sso/fixtures/profile.json | 27 + src/sso/fixtures/slim-role.json | 3 + .../fixtures/sso-authorize-url-response.json | 3 + .../sso-logout-authorize-request.json | 3 + .../sso-logout-authorize-response.json | 4 + .../sso-token-response-oauth-token.json | 7 + src/sso/fixtures/sso-token-response.json | 39 + src/sso/fixtures/token-query.json | 6 + .../authorize-logout-options.interface.ts | 6 + .../interfaces/connection-domain.interface.ts | 16 + .../interfaces/connection-option.interface.ts | 11 + .../interfaces/connection-state.interface.ts | 13 + .../interfaces/connection-status.interface.ts | 9 + .../interfaces/connection-type.interface.ts | 55 ++ src/sso/interfaces/connection.interface.ts | 36 +- .../connections-connection-type.interface.ts | 55 ++ .../delete-connection-options.interface.ts | 6 + ...get-authorization-url-options.interface.ts | 39 + .../get-connection-options.interface.ts | 6 + .../get-logout-url-options.interface.ts | 6 + src/sso/interfaces/index.ts | 25 +- .../profile-connection-type.interface.ts | 58 ++ src/sso/interfaces/profile.interface.ts | 48 +- src/sso/interfaces/slim-role.interface.ts | 11 + .../sso-authorize-url-response.interface.ts | 10 + .../sso-logout-authorize-request.interface.ts | 10 + ...sso-logout-authorize-response.interface.ts | 13 + src/sso/interfaces/sso-provider.interface.ts | 19 + ...so-token-response-oauth-token.interface.ts | 23 + .../sso-token-response.interface.ts | 28 + src/sso/interfaces/token-query.interface.ts | 19 + src/sso/serializers.spec.ts | 116 +++ .../connection-domain.serializer.ts | 14 + .../connection-option.serializer.ts | 12 + src/sso/serializers/connection.serializer.ts | 36 +- src/sso/serializers/index.ts | 13 +- src/sso/serializers/profile.serializer.ts | 52 +- src/sso/serializers/slim-role.serializer.ts | 14 + .../sso-authorize-url-response.serializer.ts | 12 + ...sso-logout-authorize-request.serializer.ts | 12 + ...so-logout-authorize-response.serializer.ts | 13 + ...o-token-response-oauth-token.serializer.ts | 16 + .../sso-token-response.serializer.ts | 21 + src/sso/serializers/token-query.serializer.ts | 13 + src/sso/sso.spec.ts | 791 ++---------------- src/sso/sso.ts | 90 +- 53 files changed, 1131 insertions(+), 912 deletions(-) delete mode 100644 src/sso/__snapshots__/sso.spec.ts.snap create mode 100644 src/sso/fixtures/connection-domain.json create mode 100644 src/sso/fixtures/connection-option.json create mode 100644 src/sso/fixtures/connection.json create mode 100644 src/sso/fixtures/list-connection.json create mode 100644 src/sso/fixtures/profile.json create mode 100644 src/sso/fixtures/slim-role.json create mode 100644 src/sso/fixtures/sso-authorize-url-response.json create mode 100644 src/sso/fixtures/sso-logout-authorize-request.json create mode 100644 src/sso/fixtures/sso-logout-authorize-response.json create mode 100644 src/sso/fixtures/sso-token-response-oauth-token.json create mode 100644 src/sso/fixtures/sso-token-response.json create mode 100644 src/sso/fixtures/token-query.json create mode 100644 src/sso/interfaces/authorize-logout-options.interface.ts create mode 100644 src/sso/interfaces/connection-domain.interface.ts create mode 100644 src/sso/interfaces/connection-option.interface.ts create mode 100644 src/sso/interfaces/connection-state.interface.ts create mode 100644 src/sso/interfaces/connection-status.interface.ts create mode 100644 src/sso/interfaces/connection-type.interface.ts create mode 100644 src/sso/interfaces/connections-connection-type.interface.ts create mode 100644 src/sso/interfaces/delete-connection-options.interface.ts create mode 100644 src/sso/interfaces/get-authorization-url-options.interface.ts create mode 100644 src/sso/interfaces/get-connection-options.interface.ts create mode 100644 src/sso/interfaces/get-logout-url-options.interface.ts create mode 100644 src/sso/interfaces/profile-connection-type.interface.ts create mode 100644 src/sso/interfaces/slim-role.interface.ts create mode 100644 src/sso/interfaces/sso-authorize-url-response.interface.ts create mode 100644 src/sso/interfaces/sso-logout-authorize-request.interface.ts create mode 100644 src/sso/interfaces/sso-logout-authorize-response.interface.ts create mode 100644 src/sso/interfaces/sso-provider.interface.ts create mode 100644 src/sso/interfaces/sso-token-response-oauth-token.interface.ts create mode 100644 src/sso/interfaces/sso-token-response.interface.ts create mode 100644 src/sso/interfaces/token-query.interface.ts create mode 100644 src/sso/serializers.spec.ts create mode 100644 src/sso/serializers/connection-domain.serializer.ts create mode 100644 src/sso/serializers/connection-option.serializer.ts create mode 100644 src/sso/serializers/slim-role.serializer.ts create mode 100644 src/sso/serializers/sso-authorize-url-response.serializer.ts create mode 100644 src/sso/serializers/sso-logout-authorize-request.serializer.ts create mode 100644 src/sso/serializers/sso-logout-authorize-response.serializer.ts create mode 100644 src/sso/serializers/sso-token-response-oauth-token.serializer.ts create mode 100644 src/sso/serializers/sso-token-response.serializer.ts create mode 100644 src/sso/serializers/token-query.serializer.ts diff --git a/.oagen-manifest.json b/.oagen-manifest.json index ba29803e3..b73eb66c0 100644 --- a/.oagen-manifest.json +++ b/.oagen-manifest.json @@ -1,7 +1,7 @@ { "version": 2, "language": "node", - "generatedAt": "2026-06-01T17:59:24.986Z", + "generatedAt": "2026-06-18T17:09:23.986Z", "files": [ "src/api-keys/interfaces/create-validation-options.interface.ts", "src/api-keys/interfaces/delete-api-key-options.interface.ts", @@ -116,6 +116,56 @@ "src/radar/serializers/radar-standalone-response.serializer.ts", "src/radar/serializers/radar-standalone-update-radar-attempt-request.serializer.ts", "src/radar/serializers/radar-standalone-update-radar-list-request.serializer.ts", + "src/sso/fixtures/connection-domain.json", + "src/sso/fixtures/connection-option.json", + "src/sso/fixtures/connection.json", + "src/sso/fixtures/list-connection.json", + "src/sso/fixtures/profile.json", + "src/sso/fixtures/slim-role.json", + "src/sso/fixtures/sso-authorize-url-response.json", + "src/sso/fixtures/sso-logout-authorize-request.json", + "src/sso/fixtures/sso-logout-authorize-response.json", + "src/sso/fixtures/sso-token-response-oauth-token.json", + "src/sso/fixtures/sso-token-response.json", + "src/sso/fixtures/token-query.json", + "src/sso/interfaces/authorize-logout-options.interface.ts", + "src/sso/interfaces/connection-domain.interface.ts", + "src/sso/interfaces/connection-option.interface.ts", + "src/sso/interfaces/connection-state.interface.ts", + "src/sso/interfaces/connection-status.interface.ts", + "src/sso/interfaces/connection-type.interface.ts", + "src/sso/interfaces/connection.interface.ts", + "src/sso/interfaces/connections-connection-type.interface.ts", + "src/sso/interfaces/delete-connection-options.interface.ts", + "src/sso/interfaces/get-authorization-url-options.interface.ts", + "src/sso/interfaces/get-connection-options.interface.ts", + "src/sso/interfaces/get-logout-url-options.interface.ts", + "src/sso/interfaces/index.ts", + "src/sso/interfaces/profile-connection-type.interface.ts", + "src/sso/interfaces/profile.interface.ts", + "src/sso/interfaces/slim-role.interface.ts", + "src/sso/interfaces/sso-authorize-url-response.interface.ts", + "src/sso/interfaces/sso-logout-authorize-request.interface.ts", + "src/sso/interfaces/sso-logout-authorize-response.interface.ts", + "src/sso/interfaces/sso-provider.interface.ts", + "src/sso/interfaces/sso-token-response-oauth-token.interface.ts", + "src/sso/interfaces/sso-token-response.interface.ts", + "src/sso/interfaces/token-query.interface.ts", + "src/sso/serializers.spec.ts", + "src/sso/serializers/connection-domain.serializer.ts", + "src/sso/serializers/connection-option.serializer.ts", + "src/sso/serializers/connection.serializer.ts", + "src/sso/serializers/index.ts", + "src/sso/serializers/profile.serializer.ts", + "src/sso/serializers/slim-role.serializer.ts", + "src/sso/serializers/sso-authorize-url-response.serializer.ts", + "src/sso/serializers/sso-logout-authorize-request.serializer.ts", + "src/sso/serializers/sso-logout-authorize-response.serializer.ts", + "src/sso/serializers/sso-token-response-oauth-token.serializer.ts", + "src/sso/serializers/sso-token-response.serializer.ts", + "src/sso/serializers/token-query.serializer.ts", + "src/sso/sso.spec.ts", + "src/sso/sso.ts", "src/vault/fixtures/actor.json", "src/vault/fixtures/create-data-key-request.json", "src/vault/fixtures/create-data-key-response.json", diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index 2c9a16685..93c6329e7 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -63,11 +63,11 @@ describe('Event', () => { id: 'conn_01234ABCD', organizationId: 'org_1234', name: 'Connection', - type: ConnectionType.OktaSAML, + connectionType: ConnectionType.OktaSAML, state: 'active', domains: [], - createdAt: '2020-05-06 04:21:48.649164', - updatedAt: '2020-05-06 04:21:48.649164', + createdAt: new Date('2020-05-06 04:21:48.649164'), + updatedAt: new Date('2020-05-06 04:21:48.649164'), }, }; diff --git a/src/sso/__snapshots__/sso.spec.ts.snap b/src/sso/__snapshots__/sso.spec.ts.snap deleted file mode 100644 index 85caf0b5e..000000000 --- a/src/sso/__snapshots__/sso.spec.ts.snap +++ /dev/null @@ -1,88 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`SSO SSO getAuthorizationUrl with a connection generates an authorize url with the connection 1`] = `"https://api.workos.dev/sso/authorize?client_id=proj_123&connection=connection_123&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with a custom api hostname generates an authorize url with the custom api hostname 1`] = `"https://api.workos.dev/sso/authorize?client_id=proj_123&provider=GoogleOAuth&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with a provider generates an authorize url with the provider 1`] = `"https://api.workos.dev/sso/authorize?client_id=proj_123&provider=GoogleOAuth&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with an \`organization\` generates an authorization URL with the organization 1`] = `"https://api.workos.dev/sso/authorize?client_id=proj_123&organization=organization_123&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with no custom api hostname generates an authorize url with the default api hostname 1`] = `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=GoogleOAuth&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with no domain or provider throws an error for incomplete arguments 1`] = `"Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'."`; - -exports[`SSO SSO getAuthorizationUrl with providerScopes generates an authorize url with the provided provider scopes 1`] = `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&provider_scopes=profile&provider_scopes=email&provider_scopes=calendar&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`; - -exports[`SSO SSO getAuthorizationUrl with state generates an authorize url with the provided state 1`] = `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=GoogleOAuth&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state"`; - -exports[`SSO SSO getProfileAndToken with all information provided sends a request to the WorkOS api for a profile 1`] = `"client_id=proj_123&grant_type=authorization_code&code=authorization_code&client_secret=sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU"`; - -exports[`SSO SSO getProfileAndToken with all information provided sends a request to the WorkOS api for a profile 2`] = ` -{ - "connectionId": "conn_123", - "connectionType": "OktaSAML", - "customAttributes": { - "license": "professional", - }, - "email": "foo@test.com", - "firstName": "foo", - "groups": [ - "Admins", - "Developers", - ], - "id": "prof_123", - "idpId": "123", - "lastName": "bar", - "name": "foo bar", - "organizationId": "org_123", - "rawAttributes": { - "email": "foo@test.com", - "first_name": "foo", - "groups": [ - "Admins", - "Developers", - ], - "last_name": "bar", - "license": "professional", - }, - "role": { - "slug": "admin", - }, - "roles": [ - { - "slug": "admin", - }, - ], -} -`; - -exports[`SSO SSO getProfileAndToken without a groups attribute sends a request to the WorkOS api for a profile 1`] = `"client_id=proj_123&grant_type=authorization_code&code=authorization_code&client_secret=sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU"`; - -exports[`SSO SSO getProfileAndToken without a groups attribute sends a request to the WorkOS api for a profile 2`] = ` -{ - "connectionId": "conn_123", - "connectionType": "OktaSAML", - "customAttributes": {}, - "email": "foo@test.com", - "firstName": "foo", - "id": "prof_123", - "idpId": "123", - "lastName": "bar", - "name": "foo bar", - "organizationId": "org_123", - "rawAttributes": { - "email": "foo@test.com", - "first_name": "foo", - "last_name": "bar", - }, - "role": { - "slug": "admin", - }, - "roles": [ - { - "slug": "admin", - }, - ], -} -`; diff --git a/src/sso/fixtures/connection-domain.json b/src/sso/fixtures/connection-domain.json new file mode 100644 index 000000000..83396c2b7 --- /dev/null +++ b/src/sso/fixtures/connection-domain.json @@ -0,0 +1,5 @@ +{ + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" +} diff --git a/src/sso/fixtures/connection-option.json b/src/sso/fixtures/connection-option.json new file mode 100644 index 000000000..3912dcb4d --- /dev/null +++ b/src/sso/fixtures/connection-option.json @@ -0,0 +1,3 @@ +{ + "signing_cert": null +} diff --git a/src/sso/fixtures/connection.json b/src/sso/fixtures/connection.json new file mode 100644 index 000000000..2caf92734 --- /dev/null +++ b/src/sso/fixtures/connection.json @@ -0,0 +1,21 @@ +{ + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" + } + ], + "options": { + "signing_cert": null + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/src/sso/fixtures/list-connection.json b/src/sso/fixtures/list-connection.json new file mode 100644 index 000000000..ace197231 --- /dev/null +++ b/src/sso/fixtures/list-connection.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" + } + ], + "options": { + "signing_cert": null + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/src/sso/fixtures/profile.json b/src/sso/fixtures/profile.json new file mode 100644 index 000000000..dd9ca7a55 --- /dev/null +++ b/src/sso/fixtures/profile.json @@ -0,0 +1,27 @@ +{ + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "OktaSAML", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "name": "Todd Rundgren", + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "groups": ["Engineering", "Admins"], + "custom_attributes": { + "key": {} + }, + "raw_attributes": { + "key": {} + } +} diff --git a/src/sso/fixtures/slim-role.json b/src/sso/fixtures/slim-role.json new file mode 100644 index 000000000..21f8f0419 --- /dev/null +++ b/src/sso/fixtures/slim-role.json @@ -0,0 +1,3 @@ +{ + "slug": "admin" +} diff --git a/src/sso/fixtures/sso-authorize-url-response.json b/src/sso/fixtures/sso-authorize-url-response.json new file mode 100644 index 000000000..918b2b814 --- /dev/null +++ b/src/sso/fixtures/sso-authorize-url-response.json @@ -0,0 +1,3 @@ +{ + "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=example&redirect_uri=https%3A%2F%2Fapi.workos.com%2Fsso%2Fcallback&response_type=code&scope=openid%20profile%20email" +} diff --git a/src/sso/fixtures/sso-logout-authorize-request.json b/src/sso/fixtures/sso-logout-authorize-request.json new file mode 100644 index 000000000..80a3c6e79 --- /dev/null +++ b/src/sso/fixtures/sso-logout-authorize-request.json @@ -0,0 +1,3 @@ +{ + "profile_id": "prof_01HXYZ123456789ABCDEFGHIJ" +} diff --git a/src/sso/fixtures/sso-logout-authorize-response.json b/src/sso/fixtures/sso-logout-authorize-response.json new file mode 100644 index 000000000..e4b792259 --- /dev/null +++ b/src/sso/fixtures/sso-logout-authorize-response.json @@ -0,0 +1,4 @@ +{ + "logout_url": "https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9", + "logout_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4" +} diff --git a/src/sso/fixtures/sso-token-response-oauth-token.json b/src/sso/fixtures/sso-token-response-oauth-token.json new file mode 100644 index 000000000..c4e6bcf53 --- /dev/null +++ b/src/sso/fixtures/sso-token-response-oauth-token.json @@ -0,0 +1,7 @@ +{ + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": ["profile", "email", "openid"] +} diff --git a/src/sso/fixtures/sso-token-response.json b/src/sso/fixtures/sso-token-response.json new file mode 100644 index 000000000..5457abdf6 --- /dev/null +++ b/src/sso/fixtures/sso-token-response.json @@ -0,0 +1,39 @@ +{ + "token_type": "Bearer", + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby...", + "expires_in": 600, + "profile": { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "OktaSAML", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "name": "Todd Rundgren", + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "groups": ["Engineering", "Admins"], + "custom_attributes": { + "key": {} + }, + "raw_attributes": { + "key": {} + } + }, + "oauth_tokens": { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": ["profile", "email", "openid"] + } +} diff --git a/src/sso/fixtures/token-query.json b/src/sso/fixtures/token-query.json new file mode 100644 index 000000000..1051d0d8a --- /dev/null +++ b/src/sso/fixtures/token-query.json @@ -0,0 +1,6 @@ +{ + "client_id": "client_01HZBC6N1EB1ZY7KG32X", + "client_secret": "sk_example_123456789", + "code": "authorization_code_value", + "grant_type": "authorization_code" +} diff --git a/src/sso/interfaces/authorize-logout-options.interface.ts b/src/sso/interfaces/authorize-logout-options.interface.ts new file mode 100644 index 000000000..48a4554c5 --- /dev/null +++ b/src/sso/interfaces/authorize-logout-options.interface.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface AuthorizeLogoutOptions { + /** The unique ID of the profile to log out. */ + profileId: string; +} diff --git a/src/sso/interfaces/connection-domain.interface.ts b/src/sso/interfaces/connection-domain.interface.ts new file mode 100644 index 000000000..1d631652f --- /dev/null +++ b/src/sso/interfaces/connection-domain.interface.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface ConnectionDomain { + /** Unique identifier for the Connection Domain. */ + id: string; + /** Distinguishes the Connection Domain object. */ + object: 'connection_domain'; + /** The domain value. */ + domain: string; +} + +export interface ConnectionDomainResponse { + id: string; + object: 'connection_domain'; + domain: string; +} diff --git a/src/sso/interfaces/connection-option.interface.ts b/src/sso/interfaces/connection-option.interface.ts new file mode 100644 index 000000000..a82f27dea --- /dev/null +++ b/src/sso/interfaces/connection-option.interface.ts @@ -0,0 +1,11 @@ +// This file is auto-generated by oagen. Do not edit. + +/** Configuration options for SAML connections. Only present for SAML connection types. */ +export interface ConnectionOption { + /** The signing certificate of the SAML connection. */ + signingCert: string | null; +} + +export interface ConnectionOptionResponse { + signing_cert: string | null; +} diff --git a/src/sso/interfaces/connection-state.interface.ts b/src/sso/interfaces/connection-state.interface.ts new file mode 100644 index 000000000..78a96bf88 --- /dev/null +++ b/src/sso/interfaces/connection-state.interface.ts @@ -0,0 +1,13 @@ +// This file is auto-generated by oagen. Do not edit. + +export const ConnectionState = { + RequiresType: 'requires_type', + Draft: 'draft', + Active: 'active', + Validating: 'validating', + Inactive: 'inactive', + Deleting: 'deleting', +} as const; + +export type ConnectionState = + (typeof ConnectionState)[keyof typeof ConnectionState]; diff --git a/src/sso/interfaces/connection-status.interface.ts b/src/sso/interfaces/connection-status.interface.ts new file mode 100644 index 000000000..0c8c86adf --- /dev/null +++ b/src/sso/interfaces/connection-status.interface.ts @@ -0,0 +1,9 @@ +// This file is auto-generated by oagen. Do not edit. + +export const ConnectionStatus = { + Linked: 'linked', + Unlinked: 'unlinked', +} as const; + +export type ConnectionStatus = + (typeof ConnectionStatus)[keyof typeof ConnectionStatus]; diff --git a/src/sso/interfaces/connection-type.interface.ts b/src/sso/interfaces/connection-type.interface.ts new file mode 100644 index 000000000..41c35ca8e --- /dev/null +++ b/src/sso/interfaces/connection-type.interface.ts @@ -0,0 +1,55 @@ +// This file is auto-generated by oagen. Do not edit. + +export enum ConnectionType { + ADFSSAML = 'ADFSSAML', + AdpOidc = 'AdpOidc', + AppleOAuth = 'AppleOAuth', + Auth0SAML = 'Auth0SAML', + AzureSAML = 'AzureSAML', + CasSAML = 'CasSAML', + ClassLinkSAML = 'ClassLinkSAML', + CleverOIDC = 'CleverOIDC', + CloudflareSAML = 'CloudflareSAML', + CyberArkSAML = 'CyberArkSAML', + DuoSAML = 'DuoSAML', + EntraIdOIDC = 'EntraIdOIDC', + GenericOIDC = 'GenericOIDC', + GenericSAML = 'GenericSAML', + GitHubOAuth = 'GitHubOAuth', + GoogleOAuth = 'GoogleOAuth', + GoogleSAML = 'GoogleSAML', + JumpCloudSAML = 'JumpCloudSAML', + KeycloakSAML = 'KeycloakSAML', + LastPassSAML = 'LastPassSAML', + LoginGovOidc = 'LoginGovOidc', + MagicLink = 'MagicLink', + MicrosoftOAuth = 'MicrosoftOAuth', + MiniOrangeSAML = 'MiniOrangeSAML', + NetIqSAML = 'NetIqSAML', + OktaOIDC = 'OktaOIDC', + OktaSAML = 'OktaSAML', + OneLoginSAML = 'OneLoginSAML', + OracleSAML = 'OracleSAML', + PingFederateSAML = 'PingFederateSAML', + PingOneSAML = 'PingOneSAML', + RipplingSAML = 'RipplingSAML', + SalesforceOAuth = 'SalesforceOAuth', + SalesforceSAML = 'SalesforceSAML', + ShibbolethGenericSAML = 'ShibbolethGenericSAML', + ShibbolethSAML = 'ShibbolethSAML', + SimpleSamlPhpSAML = 'SimpleSamlPhpSAML', + VMwareSAML = 'VMwareSAML', + Pending = 'Pending', + Auth0Migration = 'Auth0Migration', + BitbucketOAuth = 'BitbucketOAuth', + DiscordOAuth = 'DiscordOAuth', + GitLabOAuth = 'GitLabOAuth', + GoogleOidc = 'GoogleOIDC', + IntuitOAuth = 'IntuitOAuth', + LinkedInOAuth = 'LinkedInOAuth', + SlackOAuth = 'SlackOAuth', + TestIdp = 'TestIdp', + VercelMarketplaceOAuth = 'VercelMarketplaceOAuth', + VercelOAuth = 'VercelOAuth', + XeroOAuth = 'XeroOAuth', +} diff --git a/src/sso/interfaces/connection.interface.ts b/src/sso/interfaces/connection.interface.ts index 3cdf78c97..711a19eb2 100644 --- a/src/sso/interfaces/connection.interface.ts +++ b/src/sso/interfaces/connection.interface.ts @@ -1,10 +1,12 @@ -import { ConnectionType } from './connection-type.enum'; +// This file is auto-generated by oagen. Do not edit. -export interface ConnectionDomain { - object: 'connection_domain'; - id: string; - domain: string; -} +import type { ConnectionDomain } from './connection-domain.interface'; +import type { + ConnectionOption, + ConnectionOptionResponse, +} from './connection-option.interface'; +import type { ConnectionType } from './connection-type.interface'; +import type { ConnectionStatus } from './connection-status.interface'; export interface Connection { /** Distinguishes the Connection object. */ @@ -13,27 +15,37 @@ export interface Connection { id: string; /** Unique identifier for the Organization in which the Connection resides. */ organizationId?: string; + /** The type of the SSO Connection used to authenticate the user. The Connection type may be used to dynamically generate authorization URLs. */ + connectionType?: ConnectionType; /** A human-readable name for the Connection. This will most commonly be the organization's name. */ name: string; /** Indicates whether a Connection is able to authenticate users. */ - state: 'draft' | 'active' | 'inactive' | 'validating'; + state: 'active' | 'inactive' | 'validating' | 'draft'; + /** + * Deprecated. Use `state` instead. + * @deprecated + */ + status?: ConnectionStatus; /** List of Organization Domains. */ domains: ConnectionDomain[]; - type: ConnectionType; + /** Configuration options for SAML connections. Only present for SAML connection types. */ + options?: ConnectionOption; /** An ISO 8601 timestamp. */ - createdAt: string; + createdAt: Date; /** An ISO 8601 timestamp. */ - updatedAt: string; + updatedAt: Date; } export interface ConnectionResponse { object: 'connection'; id: string; organization_id?: string; - name: string; connection_type: ConnectionType; - state: 'draft' | 'active' | 'inactive' | 'validating'; + name: string; + state: 'active' | 'inactive' | 'validating' | 'draft'; + status?: ConnectionStatus; domains: ConnectionDomain[]; + options?: ConnectionOptionResponse; created_at: string; updated_at: string; } diff --git a/src/sso/interfaces/connections-connection-type.interface.ts b/src/sso/interfaces/connections-connection-type.interface.ts new file mode 100644 index 000000000..cb3f132e5 --- /dev/null +++ b/src/sso/interfaces/connections-connection-type.interface.ts @@ -0,0 +1,55 @@ +// This file is auto-generated by oagen. Do not edit. + +export const ConnectionsConnectionType = { + Adfssaml: 'ADFSSAML', + AdpOidc: 'AdpOidc', + AppleOAuth: 'AppleOAuth', + Auth0SAML: 'Auth0SAML', + AzureSAML: 'AzureSAML', + BitbucketOAuth: 'BitbucketOAuth', + CasSAML: 'CasSAML', + CloudflareSAML: 'CloudflareSAML', + ClassLinkSAML: 'ClassLinkSAML', + CleverOidc: 'CleverOIDC', + CyberArkSAML: 'CyberArkSAML', + DiscordOAuth: 'DiscordOAuth', + DuoSAML: 'DuoSAML', + EntraIdOidc: 'EntraIdOIDC', + GenericOidc: 'GenericOIDC', + GenericSAML: 'GenericSAML', + GithubOAuth: 'GithubOAuth', + GitLabOAuth: 'GitLabOAuth', + GoogleOAuth: 'GoogleOAuth', + GoogleOidc: 'GoogleOIDC', + GoogleSAML: 'GoogleSAML', + IntuitOAuth: 'IntuitOAuth', + JumpCloudSAML: 'JumpCloudSAML', + KeycloakSAML: 'KeycloakSAML', + LastPassSAML: 'LastPassSAML', + LinkedInOAuth: 'LinkedInOAuth', + LoginGovOidc: 'LoginGovOidc', + MagicLink: 'MagicLink', + MicrosoftOAuth: 'MicrosoftOAuth', + MiniOrangeSAML: 'MiniOrangeSAML', + NetIqSAML: 'NetIqSAML', + OktaOidc: 'OktaOIDC', + OktaSAML: 'OktaSAML', + OneLoginSAML: 'OneLoginSAML', + OracleSAML: 'OracleSAML', + PingFederateSAML: 'PingFederateSAML', + PingOneSAML: 'PingOneSAML', + RipplingSAML: 'RipplingSAML', + SalesforceSAML: 'SalesforceSAML', + ShibbolethGenericSAML: 'ShibbolethGenericSAML', + ShibbolethSAML: 'ShibbolethSAML', + SimpleSAMLPhpSAML: 'SimpleSamlPhpSAML', + SalesforceOAuth: 'SalesforceOAuth', + SlackOAuth: 'SlackOAuth', + VercelMarketplaceOAuth: 'VercelMarketplaceOAuth', + VercelOAuth: 'VercelOAuth', + VMwareSAML: 'VMwareSAML', + XeroOAuth: 'XeroOAuth', +} as const; + +export type ConnectionsConnectionType = + (typeof ConnectionsConnectionType)[keyof typeof ConnectionsConnectionType]; diff --git a/src/sso/interfaces/delete-connection-options.interface.ts b/src/sso/interfaces/delete-connection-options.interface.ts new file mode 100644 index 000000000..74011e1cd --- /dev/null +++ b/src/sso/interfaces/delete-connection-options.interface.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface DeleteConnectionOptions { + /** Unique identifier for the Connection. */ + id: string; +} diff --git a/src/sso/interfaces/get-authorization-url-options.interface.ts b/src/sso/interfaces/get-authorization-url-options.interface.ts new file mode 100644 index 000000000..d20da91b8 --- /dev/null +++ b/src/sso/interfaces/get-authorization-url-options.interface.ts @@ -0,0 +1,39 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { SSOProvider } from './sso-provider.interface'; + +export interface GetAuthorizationUrlOptions { + /** Additional scopes to request from the identity provider. Applicable when using OAuth or OpenID Connect connections. */ + providerScopes?: string[]; + /** Key/value pairs of query parameters to pass to the OAuth provider. Only applicable when using OAuth connections. */ + providerQueryParams?: Record; + /** + * Deprecated. Use `connection` or `organization` instead. Used to initiate SSO for a connection by domain. The domain must be associated with a connection in your WorkOS environment. + * @deprecated + */ + domain?: string; + /** Used to initiate OAuth authentication with various providers. */ + provider?: SSOProvider; + /** Where to redirect the user after they complete the authentication process. You must use one of the redirect URIs configured via the [Redirects](https://dashboard.workos.com/redirects) page on the dashboard. */ + redirectUri: string; + /** An optional parameter that can be used to encode arbitrary information to help restore application state between redirects. If included, the redirect URI received from WorkOS will contain the exact `state` that was passed. */ + state?: string; + /** + * Used to initiate SSO for a connection. The value should be a WorkOS connection ID. + * + * You can persist the WorkOS connection ID with application user or team identifiers. WorkOS will use the connection indicated by the connection parameter to direct the user to the corresponding IdP for authentication. + */ + connection?: string; + /** + * Used to initiate SSO for an organization. The value should be a WorkOS organization ID. + * + * You can persist the WorkOS organization ID with application user or team identifiers. WorkOS will use the organization ID to determine the appropriate connection and the IdP to direct the user to for authentication. + */ + organization?: string; + /** Can be used to pre-fill the domain field when initiating authentication with Microsoft OAuth or with a Google SAML connection type. */ + domainHint?: string; + /** Can be used to pre-fill the username/email address field of the IdP sign-in page for the user, if you know their username ahead of time. Currently supported for OAuth, OpenID Connect, Okta, Entra ID, and custom SAML connections. */ + loginHint?: string; + /** A random string generated by the client that is used to mitigate replay attacks. */ + nonce?: string; +} diff --git a/src/sso/interfaces/get-connection-options.interface.ts b/src/sso/interfaces/get-connection-options.interface.ts new file mode 100644 index 000000000..ead995f81 --- /dev/null +++ b/src/sso/interfaces/get-connection-options.interface.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface GetConnectionOptions { + /** Unique identifier for the Connection. */ + id: string; +} diff --git a/src/sso/interfaces/get-logout-url-options.interface.ts b/src/sso/interfaces/get-logout-url-options.interface.ts new file mode 100644 index 000000000..2e62d230e --- /dev/null +++ b/src/sso/interfaces/get-logout-url-options.interface.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface GetLogoutUrlOptions { + /** The logout token returned from the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. */ + token: string; +} diff --git a/src/sso/interfaces/index.ts b/src/sso/interfaces/index.ts index 6de6bc482..0f92ad48d 100644 --- a/src/sso/interfaces/index.ts +++ b/src/sso/interfaces/index.ts @@ -1,9 +1,28 @@ +// This file is auto-generated by oagen. Do not edit. + export * from './authorization-url-options.interface'; -export * from './connection-type.enum'; +export * from './authorize-logout-options.interface'; +export * from './connection-domain.interface'; +export * from './connection-option.interface'; +export * from './connection-state.interface'; +export * from './connection-status.interface'; +export * from './connection-type.interface'; export * from './connection.interface'; -export * from './get-profile-options.interface'; +export * from './connections-connection-type.interface'; +export * from './delete-connection-options.interface'; +export * from './get-connection-options.interface'; +export * from './get-logout-url-options.interface'; export * from './get-profile-and-token-options.interface'; +export * from './get-profile-options.interface'; export * from './list-connections-options.interface'; export * from './profile-and-token.interface'; +export * from './profile-connection-type.interface'; export * from './profile.interface'; -export type { SSOPKCEAuthorizationURLResult } from './authorization-url-options.interface'; +export * from './slim-role.interface'; +export * from './sso-authorize-url-response.interface'; +export * from './sso-logout-authorize-request.interface'; +export * from './sso-logout-authorize-response.interface'; +export * from './sso-provider.interface'; +export * from './sso-token-response-oauth-token.interface'; +export * from './sso-token-response.interface'; +export * from './token-query.interface'; diff --git a/src/sso/interfaces/profile-connection-type.interface.ts b/src/sso/interfaces/profile-connection-type.interface.ts new file mode 100644 index 000000000..b1ae013c2 --- /dev/null +++ b/src/sso/interfaces/profile-connection-type.interface.ts @@ -0,0 +1,58 @@ +// This file is auto-generated by oagen. Do not edit. + +export const ProfileConnectionType = { + Pending: 'Pending', + Adfssaml: 'ADFSSAML', + AdpOidc: 'AdpOidc', + AppleOAuth: 'AppleOAuth', + Auth0Migration: 'Auth0Migration', + Auth0SAML: 'Auth0SAML', + AzureSAML: 'AzureSAML', + BitbucketOAuth: 'BitbucketOAuth', + CasSAML: 'CasSAML', + ClassLinkSAML: 'ClassLinkSAML', + CleverOidc: 'CleverOIDC', + CloudflareSAML: 'CloudflareSAML', + CyberArkSAML: 'CyberArkSAML', + DiscordOAuth: 'DiscordOAuth', + DuoSAML: 'DuoSAML', + EntraIdOidc: 'EntraIdOIDC', + GenericOidc: 'GenericOIDC', + GenericSAML: 'GenericSAML', + GitHubOAuth: 'GitHubOAuth', + GitLabOAuth: 'GitLabOAuth', + GoogleOAuth: 'GoogleOAuth', + GoogleOidc: 'GoogleOIDC', + GoogleSAML: 'GoogleSAML', + IntuitOAuth: 'IntuitOAuth', + JumpCloudSAML: 'JumpCloudSAML', + KeycloakSAML: 'KeycloakSAML', + LastPassSAML: 'LastPassSAML', + LinkedInOAuth: 'LinkedInOAuth', + LoginGovOidc: 'LoginGovOidc', + MagicLink: 'MagicLink', + MicrosoftOAuth: 'MicrosoftOAuth', + MiniOrangeSAML: 'MiniOrangeSAML', + NetIqSAML: 'NetIqSAML', + OktaOidc: 'OktaOIDC', + OktaSAML: 'OktaSAML', + OneLoginSAML: 'OneLoginSAML', + OracleSAML: 'OracleSAML', + PingFederateSAML: 'PingFederateSAML', + PingOneSAML: 'PingOneSAML', + RipplingSAML: 'RipplingSAML', + SalesforceSAML: 'SalesforceSAML', + ShibbolethGenericSAML: 'ShibbolethGenericSAML', + ShibbolethSAML: 'ShibbolethSAML', + SimpleSAMLPhpSAML: 'SimpleSamlPhpSAML', + SalesforceOAuth: 'SalesforceOAuth', + SlackOAuth: 'SlackOAuth', + TestIdp: 'TestIdp', + VercelMarketplaceOAuth: 'VercelMarketplaceOAuth', + VercelOAuth: 'VercelOAuth', + VMwareSAML: 'VMwareSAML', + XeroOAuth: 'XeroOAuth', +} as const; + +export type ProfileConnectionType = + (typeof ProfileConnectionType)[keyof typeof ProfileConnectionType]; diff --git a/src/sso/interfaces/profile.interface.ts b/src/sso/interfaces/profile.interface.ts index 59ce7324d..ed978798b 100644 --- a/src/sso/interfaces/profile.interface.ts +++ b/src/sso/interfaces/profile.interface.ts @@ -1,26 +1,33 @@ -import { UnknownRecord } from '../../common/interfaces/unknown-record.interface'; -import { RoleResponse } from '../../roles/interfaces'; -import { ConnectionType } from './connection-type.enum'; +// This file is auto-generated by oagen. Do not edit. -export interface Profile { +import type { SlimRoleResponse } from './slim-role.interface'; +import type { ConnectionType } from './connection-type.interface'; + +type RoleResponse = SlimRoleResponse; + +export interface Profile< + GenericType extends Record = Record, +> { + /** Distinguishes the profile object. */ + object?: 'profile'; /** Unique identifier of the profile. */ id: string; - /** The user's unique identifier from the identity provider. */ - idpId: string; /** The ID of the organization the user belongs to. */ - organizationId?: string; + organizationId?: string | null; /** The ID of the SSO connection used for authentication. */ connectionId: string; /** The type of SSO connection. */ connectionType: ConnectionType; + /** The user's unique identifier from the identity provider. */ + idpId: string; /** The user's email address. */ email: string; - /** The user's full name. */ - name?: string; /** The user's first name. */ - firstName?: string; + firstName?: string | null; /** The user's last name. */ - lastName?: string; + lastName?: string | null; + /** The user's full name. */ + name?: string | null; /** The role assigned to the user within the organization, if applicable. */ role?: RoleResponse; /** The roles assigned to the user within the organization, if applicable. */ @@ -28,24 +35,27 @@ export interface Profile { /** The groups the user belongs to, as returned by the identity provider. */ groups?: string[]; /** Custom attribute mappings defined for the connection, returned as key-value pairs. */ - customAttributes?: CustomAttributesType; + customAttributes?: GenericType; /** The complete set of raw attributes returned by the identity provider. */ rawAttributes?: { [key: string]: any }; } -export interface ProfileResponse { +export interface ProfileResponse< + GenericType extends Record = Record, +> { + object?: 'profile'; id: string; - idp_id: string; - organization_id?: string; + organization_id?: string | null; connection_id: string; connection_type: ConnectionType; + idp_id: string; email: string; - name?: string; - first_name?: string; - last_name?: string; + first_name?: string | null; + last_name?: string | null; + name?: string | null; role?: RoleResponse; roles?: RoleResponse[]; groups?: string[]; - custom_attributes?: CustomAttributesType; + custom_attributes?: GenericType; raw_attributes?: { [key: string]: any }; } diff --git a/src/sso/interfaces/slim-role.interface.ts b/src/sso/interfaces/slim-role.interface.ts new file mode 100644 index 000000000..72dc7ceb3 --- /dev/null +++ b/src/sso/interfaces/slim-role.interface.ts @@ -0,0 +1,11 @@ +// This file is auto-generated by oagen. Do not edit. + +/** The primary role assigned to the user. */ +export interface SlimRole { + /** The slug of the assigned role. */ + slug: string; +} + +export interface SlimRoleResponse { + slug: string; +} diff --git a/src/sso/interfaces/sso-authorize-url-response.interface.ts b/src/sso/interfaces/sso-authorize-url-response.interface.ts new file mode 100644 index 000000000..bea244a6d --- /dev/null +++ b/src/sso/interfaces/sso-authorize-url-response.interface.ts @@ -0,0 +1,10 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface SSOAuthorizeUrlResponse { + /** An OAuth 2.0 authorization URL. */ + url: string; +} + +export interface SSOAuthorizeUrlResponseWire { + url: string; +} diff --git a/src/sso/interfaces/sso-logout-authorize-request.interface.ts b/src/sso/interfaces/sso-logout-authorize-request.interface.ts new file mode 100644 index 000000000..97409e7a7 --- /dev/null +++ b/src/sso/interfaces/sso-logout-authorize-request.interface.ts @@ -0,0 +1,10 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface SSOLogoutAuthorizeRequest { + /** The unique ID of the profile to log out. */ + profileId: string; +} + +export interface SSOLogoutAuthorizeRequestResponse { + profile_id: string; +} diff --git a/src/sso/interfaces/sso-logout-authorize-response.interface.ts b/src/sso/interfaces/sso-logout-authorize-response.interface.ts new file mode 100644 index 000000000..3bed6a517 --- /dev/null +++ b/src/sso/interfaces/sso-logout-authorize-response.interface.ts @@ -0,0 +1,13 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface SSOLogoutAuthorizeResponse { + /** The URL to redirect the user to in order to log out ([Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint ready to use). */ + logoutUrl: string; + /** The logout token to be used in the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint. */ + logoutToken: string; +} + +export interface SSOLogoutAuthorizeResponseWire { + logout_url: string; + logout_token: string; +} diff --git a/src/sso/interfaces/sso-provider.interface.ts b/src/sso/interfaces/sso-provider.interface.ts new file mode 100644 index 000000000..f53872f36 --- /dev/null +++ b/src/sso/interfaces/sso-provider.interface.ts @@ -0,0 +1,19 @@ +// This file is auto-generated by oagen. Do not edit. + +export const SSOProvider = { + AppleOAuth: 'AppleOAuth', + BitbucketOAuth: 'BitbucketOAuth', + GitHubOAuth: 'GitHubOAuth', + GitLabOAuth: 'GitLabOAuth', + GoogleOAuth: 'GoogleOAuth', + IntuitOAuth: 'IntuitOAuth', + LinkedInOAuth: 'LinkedInOAuth', + MicrosoftOAuth: 'MicrosoftOAuth', + SalesforceOAuth: 'SalesforceOAuth', + SlackOAuth: 'SlackOAuth', + VercelMarketplaceOAuth: 'VercelMarketplaceOAuth', + VercelOAuth: 'VercelOAuth', + XeroOAuth: 'XeroOAuth', +} as const; + +export type SSOProvider = (typeof SSOProvider)[keyof typeof SSOProvider]; diff --git a/src/sso/interfaces/sso-token-response-oauth-token.interface.ts b/src/sso/interfaces/sso-token-response-oauth-token.interface.ts new file mode 100644 index 000000000..2a8aff58d --- /dev/null +++ b/src/sso/interfaces/sso-token-response-oauth-token.interface.ts @@ -0,0 +1,23 @@ +// This file is auto-generated by oagen. Do not edit. + +/** OAuth tokens issued by the identity provider, if available. */ +export interface SSOTokenResponseOAuthToken { + /** The OAuth provider used for authentication. */ + provider: string; + /** The refresh token from the OAuth provider. */ + refreshToken: string; + /** The access token from the OAuth provider. */ + accessToken: string; + /** The timestamp at which the access token expires. */ + expiresAt: number; + /** A list of OAuth scopes for which the access token is authorized. */ + scopes: string[]; +} + +export interface SSOTokenResponseOAuthTokenResponse { + provider: string; + refresh_token: string; + access_token: string; + expires_at: number; + scopes: string[]; +} diff --git a/src/sso/interfaces/sso-token-response.interface.ts b/src/sso/interfaces/sso-token-response.interface.ts new file mode 100644 index 000000000..7375d537d --- /dev/null +++ b/src/sso/interfaces/sso-token-response.interface.ts @@ -0,0 +1,28 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { Profile, ProfileResponse } from './profile.interface'; +import type { + SSOTokenResponseOAuthToken, + SSOTokenResponseOAuthTokenResponse, +} from './sso-token-response-oauth-token.interface'; + +export interface SSOTokenResponse { + /** The type of token issued. */ + tokenType: 'Bearer'; + /** An access token that can be exchanged for a user profile. Access tokens are short-lived — see the `expires_in` field for the exact lifetime. */ + accessToken: string; + /** The lifetime of the access token in seconds. */ + expiresIn: number; + /** The user profile returned by the identity provider. */ + profile: Profile>; + /** OAuth tokens issued by the identity provider, if available. */ + oauthTokens?: SSOTokenResponseOAuthToken; +} + +export interface SSOTokenResponseWire { + token_type: 'Bearer'; + access_token: string; + expires_in: number; + profile: ProfileResponse>; + oauth_tokens?: SSOTokenResponseOAuthTokenResponse; +} diff --git a/src/sso/interfaces/token-query.interface.ts b/src/sso/interfaces/token-query.interface.ts new file mode 100644 index 000000000..18d9b3e9d --- /dev/null +++ b/src/sso/interfaces/token-query.interface.ts @@ -0,0 +1,19 @@ +// This file is auto-generated by oagen. Do not edit. + +export interface TokenQuery { + /** The client ID of the WorkOS environment. */ + clientId: string; + /** The client secret of the WorkOS environment. */ + clientSecret: string; + /** The authorization code received from the authorization callback. */ + code: string; + /** The grant type for the token request. */ + grantType: 'authorization_code'; +} + +export interface TokenQueryResponse { + client_id: string; + client_secret: string; + code: string; + grant_type: 'authorization_code'; +} diff --git a/src/sso/serializers.spec.ts b/src/sso/serializers.spec.ts new file mode 100644 index 000000000..d14f74130 --- /dev/null +++ b/src/sso/serializers.spec.ts @@ -0,0 +1,116 @@ +// This file is auto-generated by oagen. Do not edit. + +import { serializeTokenQuery } from './serializers/token-query.serializer'; +import { deserializeConnection } from './serializers/connection.serializer'; +import { deserializeSSOAuthorizeUrlResponse } from './serializers/sso-authorize-url-response.serializer'; +import { deserializeProfile } from './serializers/profile.serializer'; +import { deserializeSSOTokenResponse } from './serializers/sso-token-response.serializer'; +import { deserializeSSOLogoutAuthorizeResponse } from './serializers/sso-logout-authorize-response.serializer'; +import { deserializeSSOTokenResponseOAuthToken } from './serializers/sso-token-response-oauth-token.serializer'; +import { deserializeConnectionDomain } from './serializers/connection-domain.serializer'; +import { deserializeConnectionOption } from './serializers/connection-option.serializer'; +import { serializeSSOLogoutAuthorizeRequest } from './serializers/sso-logout-authorize-request.serializer'; +import type { TokenQueryResponse } from './interfaces/token-query.interface'; +import type { ConnectionResponse } from './interfaces/connection.interface'; +import type { SSOAuthorizeUrlResponseWire } from './interfaces/sso-authorize-url-response.interface'; +import type { ProfileResponse } from './interfaces/profile.interface'; +import type { SSOTokenResponseWire } from './interfaces/sso-token-response.interface'; +import type { SSOLogoutAuthorizeResponseWire } from './interfaces/sso-logout-authorize-response.interface'; +import type { SSOTokenResponseOAuthTokenResponse } from './interfaces/sso-token-response-oauth-token.interface'; +import type { ConnectionDomainResponse } from './interfaces/connection-domain.interface'; +import type { ConnectionOptionResponse } from './interfaces/connection-option.interface'; +import type { SSOLogoutAuthorizeRequestResponse } from './interfaces/sso-logout-authorize-request.interface'; +import tokenQueryFixture from './fixtures/token-query.json'; +import connectionFixture from './fixtures/connection.json'; +import ssoAuthorizeUrlResponseFixture from './fixtures/sso-authorize-url-response.json'; +import profileFixture from './fixtures/profile.json'; +import ssoTokenResponseFixture from './fixtures/sso-token-response.json'; +import ssoLogoutAuthorizeResponseFixture from './fixtures/sso-logout-authorize-response.json'; +import ssoTokenResponseOAuthTokenFixture from './fixtures/sso-token-response-oauth-token.json'; +import connectionDomainFixture from './fixtures/connection-domain.json'; +import connectionOptionFixture from './fixtures/connection-option.json'; +import ssoLogoutAuthorizeRequestFixture from './fixtures/sso-logout-authorize-request.json'; + +describe('TokenQuerySerializer', () => { + it('serializes correctly', () => { + const fixture = tokenQueryFixture as TokenQueryResponse; + const serialized = serializeTokenQuery(fixture as any); + expect(serialized).toBeDefined(); + }); +}); + +describe('ConnectionSerializer', () => { + it('deserializes correctly', () => { + const fixture = connectionFixture as ConnectionResponse; + const deserialized = deserializeConnection(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('SSOAuthorizeUrlResponseSerializer', () => { + it('deserializes correctly', () => { + const fixture = + ssoAuthorizeUrlResponseFixture as SSOAuthorizeUrlResponseWire; + const deserialized = deserializeSSOAuthorizeUrlResponse(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('ProfileSerializer', () => { + it('deserializes correctly', () => { + const fixture = profileFixture as ProfileResponse; + const deserialized = deserializeProfile(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('SSOTokenResponseSerializer', () => { + it('deserializes correctly', () => { + const fixture = ssoTokenResponseFixture as SSOTokenResponseWire; + const deserialized = deserializeSSOTokenResponse(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('SSOLogoutAuthorizeResponseSerializer', () => { + it('deserializes correctly', () => { + const fixture = + ssoLogoutAuthorizeResponseFixture as SSOLogoutAuthorizeResponseWire; + const deserialized = deserializeSSOLogoutAuthorizeResponse(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('SSOTokenResponseOAuthTokenSerializer', () => { + it('deserializes correctly', () => { + const fixture = + ssoTokenResponseOAuthTokenFixture as SSOTokenResponseOAuthTokenResponse; + const deserialized = deserializeSSOTokenResponseOAuthToken(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('ConnectionDomainSerializer', () => { + it('deserializes correctly', () => { + const fixture = connectionDomainFixture as ConnectionDomainResponse; + const deserialized = deserializeConnectionDomain(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('ConnectionOptionSerializer', () => { + it('deserializes correctly', () => { + const fixture = connectionOptionFixture as ConnectionOptionResponse; + const deserialized = deserializeConnectionOption(fixture); + expect(deserialized).toBeDefined(); + }); +}); + +describe('SSOLogoutAuthorizeRequestSerializer', () => { + it('serializes correctly', () => { + const fixture = + ssoLogoutAuthorizeRequestFixture as SSOLogoutAuthorizeRequestResponse; + const serialized = serializeSSOLogoutAuthorizeRequest(fixture as any); + expect(serialized).toBeDefined(); + }); +}); diff --git a/src/sso/serializers/connection-domain.serializer.ts b/src/sso/serializers/connection-domain.serializer.ts new file mode 100644 index 000000000..4112a3221 --- /dev/null +++ b/src/sso/serializers/connection-domain.serializer.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + ConnectionDomain, + ConnectionDomainResponse, +} from '../interfaces/connection-domain.interface'; + +export const deserializeConnectionDomain = ( + response: ConnectionDomainResponse, +): ConnectionDomain => ({ + id: response.id, + object: response.object, + domain: response.domain, +}); diff --git a/src/sso/serializers/connection-option.serializer.ts b/src/sso/serializers/connection-option.serializer.ts new file mode 100644 index 000000000..b7943145c --- /dev/null +++ b/src/sso/serializers/connection-option.serializer.ts @@ -0,0 +1,12 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + ConnectionOption, + ConnectionOptionResponse, +} from '../interfaces/connection-option.interface'; + +export const deserializeConnectionOption = ( + response: ConnectionOptionResponse, +): ConnectionOption => ({ + signingCert: response.signing_cert ?? null, +}); diff --git a/src/sso/serializers/connection.serializer.ts b/src/sso/serializers/connection.serializer.ts index 9848ec06d..884be9b29 100644 --- a/src/sso/serializers/connection.serializer.ts +++ b/src/sso/serializers/connection.serializer.ts @@ -1,17 +1,27 @@ -import { Connection, ConnectionResponse } from '../interfaces'; +// This file is auto-generated by oagen. Do not edit. + +import type { + Connection, + ConnectionResponse, +} from '../interfaces/connection.interface'; +import { deserializeConnectionDomain } from './connection-domain.serializer'; +import { deserializeConnectionOption } from './connection-option.serializer'; export const deserializeConnection = ( - connection: ConnectionResponse, + response: ConnectionResponse, ): Connection => ({ - object: connection.object, - id: connection.id, - ...(connection.organization_id !== undefined && { - organizationId: connection.organization_id, - }), - name: connection.name, - type: connection.connection_type, - state: connection.state, - domains: connection.domains, - createdAt: connection.created_at, - updatedAt: connection.updated_at, + object: response.object, + id: response.id, + organizationId: response.organization_id, + connectionType: response.connection_type, + name: response.name, + state: response.state, + status: response.status, + domains: response.domains.map(deserializeConnectionDomain), + options: + response.options != null + ? deserializeConnectionOption(response.options) + : undefined, + createdAt: new Date(response.created_at), + updatedAt: new Date(response.updated_at), }); diff --git a/src/sso/serializers/index.ts b/src/sso/serializers/index.ts index 259ade3ec..90ab19400 100644 --- a/src/sso/serializers/index.ts +++ b/src/sso/serializers/index.ts @@ -1,4 +1,13 @@ +// This file is auto-generated by oagen. Do not edit. + export * from './connection.serializer'; -export * from './list-connections-options.serializer'; -export * from './profile-and-token.serializer'; +export * from './connection-domain.serializer'; +export * from './connection-option.serializer'; export * from './profile.serializer'; +export * from './slim-role.serializer'; +export * from './sso-authorize-url-response.serializer'; +export * from './sso-logout-authorize-request.serializer'; +export * from './sso-logout-authorize-response.serializer'; +export * from './sso-token-response.serializer'; +export * from './sso-token-response-oauth-token.serializer'; +export * from './token-query.serializer'; diff --git a/src/sso/serializers/profile.serializer.ts b/src/sso/serializers/profile.serializer.ts index 1e0f41074..caa9f1eea 100644 --- a/src/sso/serializers/profile.serializer.ts +++ b/src/sso/serializers/profile.serializer.ts @@ -1,27 +1,29 @@ -import { UnknownRecord } from '../../common/interfaces/unknown-record.interface'; -import { Profile, ProfileResponse } from '../interfaces'; +// This file is auto-generated by oagen. Do not edit. -export const deserializeProfile = ( - profile: ProfileResponse, -): Profile => ({ - id: profile.id, - idpId: profile.idp_id, - ...(profile.organization_id !== undefined && { - organizationId: profile.organization_id, - }), - connectionId: profile.connection_id, - connectionType: profile.connection_type, - email: profile.email, - ...(profile.name !== undefined && { name: profile.name }), - ...(profile.first_name !== undefined && { firstName: profile.first_name }), - ...(profile.last_name !== undefined && { lastName: profile.last_name }), - ...(profile.role !== undefined && { role: profile.role }), - ...(profile.roles !== undefined && { roles: profile.roles }), - ...(profile.groups !== undefined && { groups: profile.groups }), - ...(profile.custom_attributes !== undefined && { - customAttributes: profile.custom_attributes, - }), - ...(profile.raw_attributes !== undefined && { - rawAttributes: profile.raw_attributes, - }), +import type { Profile, ProfileResponse } from '../interfaces/profile.interface'; +import { deserializeSlimRole } from './slim-role.serializer'; + +export const deserializeProfile = < + GenericType extends Record = Record, +>( + response: ProfileResponse, +): Profile => ({ + object: response.object ?? 'profile', + id: response.id, + organizationId: response.organization_id ?? undefined, + connectionId: response.connection_id, + connectionType: response.connection_type, + idpId: response.idp_id, + email: response.email, + firstName: response.first_name ?? undefined, + lastName: response.last_name ?? undefined, + name: response.name ?? undefined, + role: response.role != null ? deserializeSlimRole(response.role) : undefined, + roles: + response.roles != null + ? response.roles.map(deserializeSlimRole) + : undefined, + groups: response.groups, + customAttributes: response.custom_attributes, + rawAttributes: response.raw_attributes ?? {}, }); diff --git a/src/sso/serializers/slim-role.serializer.ts b/src/sso/serializers/slim-role.serializer.ts new file mode 100644 index 000000000..6281acac9 --- /dev/null +++ b/src/sso/serializers/slim-role.serializer.ts @@ -0,0 +1,14 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SlimRole, + SlimRoleResponse, +} from '../interfaces/slim-role.interface'; + +export const deserializeSlimRole = (response: SlimRoleResponse): SlimRole => ({ + slug: response.slug, +}); + +export const serializeSlimRole = (model: SlimRole): SlimRoleResponse => ({ + slug: model.slug, +}); diff --git a/src/sso/serializers/sso-authorize-url-response.serializer.ts b/src/sso/serializers/sso-authorize-url-response.serializer.ts new file mode 100644 index 000000000..8251cd8b8 --- /dev/null +++ b/src/sso/serializers/sso-authorize-url-response.serializer.ts @@ -0,0 +1,12 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SSOAuthorizeUrlResponse, + SSOAuthorizeUrlResponseWire, +} from '../interfaces/sso-authorize-url-response.interface'; + +export const deserializeSSOAuthorizeUrlResponse = ( + response: SSOAuthorizeUrlResponseWire, +): SSOAuthorizeUrlResponse => ({ + url: response.url, +}); diff --git a/src/sso/serializers/sso-logout-authorize-request.serializer.ts b/src/sso/serializers/sso-logout-authorize-request.serializer.ts new file mode 100644 index 000000000..8c3200727 --- /dev/null +++ b/src/sso/serializers/sso-logout-authorize-request.serializer.ts @@ -0,0 +1,12 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SSOLogoutAuthorizeRequest, + SSOLogoutAuthorizeRequestResponse, +} from '../interfaces/sso-logout-authorize-request.interface'; + +export const serializeSSOLogoutAuthorizeRequest = ( + model: SSOLogoutAuthorizeRequest, +): SSOLogoutAuthorizeRequestResponse => ({ + profile_id: model.profileId, +}); diff --git a/src/sso/serializers/sso-logout-authorize-response.serializer.ts b/src/sso/serializers/sso-logout-authorize-response.serializer.ts new file mode 100644 index 000000000..56b294aca --- /dev/null +++ b/src/sso/serializers/sso-logout-authorize-response.serializer.ts @@ -0,0 +1,13 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SSOLogoutAuthorizeResponse, + SSOLogoutAuthorizeResponseWire, +} from '../interfaces/sso-logout-authorize-response.interface'; + +export const deserializeSSOLogoutAuthorizeResponse = ( + response: SSOLogoutAuthorizeResponseWire, +): SSOLogoutAuthorizeResponse => ({ + logoutUrl: response.logout_url, + logoutToken: response.logout_token, +}); diff --git a/src/sso/serializers/sso-token-response-oauth-token.serializer.ts b/src/sso/serializers/sso-token-response-oauth-token.serializer.ts new file mode 100644 index 000000000..1fc43ac1e --- /dev/null +++ b/src/sso/serializers/sso-token-response-oauth-token.serializer.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SSOTokenResponseOAuthToken, + SSOTokenResponseOAuthTokenResponse, +} from '../interfaces/sso-token-response-oauth-token.interface'; + +export const deserializeSSOTokenResponseOAuthToken = ( + response: SSOTokenResponseOAuthTokenResponse, +): SSOTokenResponseOAuthToken => ({ + provider: response.provider, + refreshToken: response.refresh_token, + accessToken: response.access_token, + expiresAt: response.expires_at, + scopes: response.scopes, +}); diff --git a/src/sso/serializers/sso-token-response.serializer.ts b/src/sso/serializers/sso-token-response.serializer.ts new file mode 100644 index 000000000..bc4870d08 --- /dev/null +++ b/src/sso/serializers/sso-token-response.serializer.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + SSOTokenResponse, + SSOTokenResponseWire, +} from '../interfaces/sso-token-response.interface'; +import { deserializeProfile } from './profile.serializer'; +import { deserializeSSOTokenResponseOAuthToken } from './sso-token-response-oauth-token.serializer'; + +export const deserializeSSOTokenResponse = ( + response: SSOTokenResponseWire, +): SSOTokenResponse => ({ + tokenType: response.token_type, + accessToken: response.access_token, + expiresIn: response.expires_in, + profile: deserializeProfile(response.profile), + oauthTokens: + response.oauth_tokens != null + ? deserializeSSOTokenResponseOAuthToken(response.oauth_tokens) + : undefined, +}); diff --git a/src/sso/serializers/token-query.serializer.ts b/src/sso/serializers/token-query.serializer.ts new file mode 100644 index 000000000..7aa08bfad --- /dev/null +++ b/src/sso/serializers/token-query.serializer.ts @@ -0,0 +1,13 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + TokenQuery, + TokenQueryResponse, +} from '../interfaces/token-query.interface'; + +export const serializeTokenQuery = (model: TokenQuery): TokenQueryResponse => ({ + client_id: model.clientId, + client_secret: model.clientSecret, + code: model.code, + grant_type: model.grantType, +}); diff --git a/src/sso/sso.spec.ts b/src/sso/sso.spec.ts index c4cad3bd1..5987b0419 100644 --- a/src/sso/sso.spec.ts +++ b/src/sso/sso.spec.ts @@ -1,747 +1,78 @@ +// This file is auto-generated by oagen. Do not edit. + import fetch from 'jest-fetch-mock'; import { fetchOnce, fetchURL, - fetchHeaders, - fetchBody, + fetchMethod, fetchSearchParams, + fetchBody, } from '../common/utils/test-utils'; - import { WorkOS } from '../workos'; -import { ConnectionResponse, ConnectionType } from './interfaces'; -import { ListResponse } from '../common/interfaces'; - -describe('SSO', () => { - beforeEach(() => fetch.resetMocks()); - - const connectionResponse: ConnectionResponse = { - object: 'connection', - id: 'conn_123', - organization_id: 'org_123', - name: 'Connection', - connection_type: ConnectionType.OktaSAML, - state: 'active', - domains: [], - created_at: '2023-07-17T20:07:20.055Z', - updated_at: '2023-07-17T20:07:20.055Z', - }; - - describe('SSO', () => { - describe('with options', () => { - it('requests Connections with query parameters', async () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const listConnectionsResponse: ListResponse = { - object: 'list', - data: [connectionResponse], - list_metadata: {}, - }; - - fetchOnce(listConnectionsResponse); - - await workos.sso.listConnections({ - connectionType: ConnectionType.OktaSAML, - organizationId: 'org_123', - }); - - expect(fetchSearchParams()).toMatchObject({ - connection_type: ConnectionType.OktaSAML, - organization_id: 'org_123', - }); - }); - }); - - describe('getAuthorizationUrl', () => { - describe('with no custom api hostname', () => { - it('generates an authorize url with the default api hostname', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'GoogleOAuth', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchSnapshot(); - }); - }); - - describe('with no domain or provider', () => { - it('throws an error for incomplete arguments', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const urlFn = () => - // @ts-expect-error Testing runtime validation with invalid input - workos.sso.getAuthorizationUrl({ - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(urlFn).toThrowErrorMatchingSnapshot(); - }); - }); - - describe('with a provider', () => { - it('generates an authorize url with the provider', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', { - apiHostname: 'api.workos.dev', - }); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'GoogleOAuth', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchSnapshot(); - }); - }); - - describe('with a connection', () => { - it('generates an authorize url with the connection', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', { - apiHostname: 'api.workos.dev', - }); - - const url = workos.sso.getAuthorizationUrl({ - connection: 'connection_123', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchSnapshot(); - }); - }); - - describe('with an `organization`', () => { - it('generates an authorization URL with the organization', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', { - apiHostname: 'api.workos.dev', - }); - - const url = workos.sso.getAuthorizationUrl({ - organization: 'organization_123', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchSnapshot(); - }); - }); - - describe('with a custom api hostname', () => { - it('generates an authorize url with the custom api hostname', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', { - apiHostname: 'api.workos.dev', - }); +import { ConnectionType } from './interfaces/connection-type.enum'; - const url = workos.sso.getAuthorizationUrl({ - provider: 'GoogleOAuth', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); +import listConnectionFixture from './fixtures/list-connection.json'; +import ssoLogoutAuthorizeResponseFixture from './fixtures/sso-logout-authorize-response.json'; - expect(url).toMatchSnapshot(); - }); - }); - - describe('with state', () => { - it('generates an authorize url with the provided state', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'GoogleOAuth', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - state: 'custom state', - }); - - expect(url).toMatchSnapshot(); - }); - }); - - describe('with domainHint', () => { - it('generates an authorize url with the provided domain hint', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - domainHint: 'lyft.com', - connection: 'connection_123', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - state: 'custom state', - }); - - expect(url).toMatchInlineSnapshot( - `"https://api.workos.com/sso/authorize?client_id=proj_123&connection=connection_123&domain_hint=lyft.com&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state"`, - ); - }); - }); - - describe('with loginHint', () => { - it('generates an authorize url with the provided login hint', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - loginHint: 'foo@workos.com', - connection: 'connection_123', - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - state: 'custom state', - }); - - expect(url).toMatchInlineSnapshot( - `"https://api.workos.com/sso/authorize?client_id=proj_123&connection=connection_123&login_hint=foo%40workos.com&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code&state=custom+state"`, - ); - }); - }); - - describe('with providerScopes', () => { - it('generates an authorize url with the provided provider scopes', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'Google', - providerScopes: ['profile', 'email', 'calendar'], - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchSnapshot(); - }); - - it('handles empty provider scopes array', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'Google', - providerScopes: [], - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); +const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - expect(url).toMatchInlineSnapshot( - `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`, - ); - }); - }); - - describe('with providerQueryParams', () => { - it('generates an authorize url with the provided provider query params', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'Google', - providerQueryParams: { - custom_param: 'custom_value', - another_param: 123, - bool_param: true, - }, - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchInlineSnapshot( - `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&provider_query_params%5Banother_param%5D=123&provider_query_params%5Bbool_param%5D=true&provider_query_params%5Bcustom_param%5D=custom_value&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`, - ); - }); - - it('handles empty provider query params', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - provider: 'Google', - providerQueryParams: {}, - clientId: 'proj_123', - redirectUri: 'example.com/sso/workos/callback', - }); - - expect(url).toMatchInlineSnapshot( - `"https://api.workos.com/sso/authorize?client_id=proj_123&provider=Google&redirect_uri=example.com%2Fsso%2Fworkos%2Fcallback&response_type=code"`, - ); - }); - }); - - describe('with PKCE parameters', () => { - it('includes codeChallenge and codeChallengeMethod in URL', () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const url = workos.sso.getAuthorizationUrl({ - connection: 'conn_123', - clientId: 'proj_123', - redirectUri: 'myapp://callback', - codeChallenge: 'test-challenge', - codeChallengeMethod: 'S256', - }); - - expect(url).toContain('code_challenge=test-challenge'); - expect(url).toContain('code_challenge_method=S256'); - }); - }); - }); - - describe('getAuthorizationUrlWithPKCE', () => { - it('generates PKCE parameters and returns codeVerifier', async () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const result = await workos.sso.getAuthorizationUrlWithPKCE({ - connection: 'conn_123', - clientId: 'proj_123', - redirectUri: 'myapp://callback', - }); - - expect(result.codeVerifier).toBeDefined(); - expect(result.codeVerifier.length).toBeGreaterThanOrEqual(43); - expect(result.url).toContain('code_challenge='); - expect(result.url).toContain('code_challenge_method=S256'); - expect(result.state).toBeDefined(); - expect(result.state.length).toBeGreaterThanOrEqual(43); - }); +function expectConnection(result: any) { + expect(result.object).toBe('connection'); + expect(result.id).toBe('conn_01E4ZCR3C56J083X43JQXF3JK5'); + expect(result.connectionType).toBe('OktaSAML'); + expect(result.name).toBe('Foo Corp'); + expect(result.state).toBe('active'); + expect(result.status).toBe('linked'); + expect(result.createdAt.toISOString()).toBe('2026-01-15T12:00:00.000Z'); + expect(result.updatedAt.toISOString()).toBe('2026-01-15T12:00:00.000Z'); +} - it('includes all provided options in the URL', async () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const result = await workos.sso.getAuthorizationUrlWithPKCE({ - connection: 'conn_123', - clientId: 'proj_123', - redirectUri: 'myapp://callback', - domainHint: 'example.com', - loginHint: 'user@example.com', - }); - - expect(result.url).toContain('connection=conn_123'); - expect(result.url).toContain('client_id=proj_123'); - expect(result.url).toContain('domain_hint=example.com'); - expect(result.url).toContain('login_hint=user%40example.com'); - }); - - it('throws error when no connection, organization, or provider is specified', async () => { - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - await expect( - workos.sso.getAuthorizationUrlWithPKCE({ - clientId: 'proj_123', - redirectUri: 'myapp://callback', - } as Parameters[0]), - ).rejects.toThrow( - `Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'.`, - ); - }); - }); - - describe('getProfileAndToken', () => { - describe('with all information provided', () => { - it('sends a request to the WorkOS api for a profile', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { - slug: 'admin', - }, - roles: [{ slug: 'admin' }], - groups: ['Admins', 'Developers'], - raw_attributes: { - email: 'foo@test.com', - first_name: 'foo', - last_name: 'bar', - groups: ['Admins', 'Developers'], - license: 'professional', - }, - custom_attributes: { - license: 'professional', - }, - }, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const { accessToken, profile } = await workos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - }); - - expect(fetch.mock.calls.length).toEqual(1); - - expect(fetchBody()).toMatchSnapshot(); - - const headers = fetchHeaders() as Record; - expect(headers['Accept']).toBe('application/json, text/plain, */*'); - expect(headers['Authorization']).toBe( - 'Bearer sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', - ); - expect(headers['Content-Type']).toBe( - 'application/x-www-form-urlencoded;charset=utf-8', - ); - expect(headers['User-Agent']).toMatch( - /^workos-node\/\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/, - ); - - expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q'); - expect(profile).toMatchSnapshot(); - }); - }); - - describe('without a groups attribute', () => { - it('sends a request to the WorkOS api for a profile', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { - slug: 'admin', - }, - roles: [{ slug: 'admin' }], - raw_attributes: { - email: 'foo@test.com', - first_name: 'foo', - last_name: 'bar', - }, - custom_attributes: {}, - }, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const { accessToken, profile } = await workos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - }); - - expect(fetch.mock.calls.length).toEqual(1); - - expect(fetchBody()).toMatchSnapshot(); - - const headers = fetchHeaders() as Record; - expect(headers['Accept']).toBe('application/json, text/plain, */*'); - expect(headers['Authorization']).toBe( - 'Bearer sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', - ); - expect(headers['Content-Type']).toBe( - 'application/x-www-form-urlencoded;charset=utf-8', - ); - expect(headers['User-Agent']).toMatch( - /^workos-node\/\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/, - ); - - expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q'); - expect(profile).toMatchSnapshot(); - }); - }); - - describe('with oauth tokens in the response', () => { - it('returns the oauth tokens from the profile and token response', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { - slug: 'admin', - }, - roles: [{ slug: 'admin' }], - groups: ['Admins', 'Developers'], - raw_attributes: { - email: 'foo@test.com', - first_name: 'foo', - last_name: 'bar', - groups: ['Admins', 'Developers'], - }, - custom_attributes: {}, - }, - oauth_tokens: { - access_token: 'oauth_access_token', - refresh_token: 'oauth_refresh_token', - expires_at: 1640995200, - scopes: ['profile', 'email'], - }, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const { accessToken, profile, oauthTokens } = - await workos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - }); - - expect(fetch.mock.calls.length).toEqual(1); - expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q'); - expect(profile).toBeDefined(); - expect(oauthTokens).toEqual({ - accessToken: 'oauth_access_token', - refreshToken: 'oauth_refresh_token', - expiresAt: 1640995200, - scopes: ['profile', 'email'], - }); - }); - }); - - describe('without oauth tokens in the response', () => { - it('returns undefined for oauth tokens when not present in response', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { - slug: 'admin', - }, - roles: [{ slug: 'admin' }], - raw_attributes: { - email: 'foo@test.com', - first_name: 'foo', - last_name: 'bar', - }, - custom_attributes: {}, - }, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const { accessToken, profile, oauthTokens } = - await workos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - }); - - expect(fetch.mock.calls.length).toEqual(1); - expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q'); - expect(profile).toBeDefined(); - expect(oauthTokens).toBeUndefined(); - }); - }); - - describe('confidential client with PKCE (API key + codeVerifier)', () => { - it('sends both client_secret and code_verifier for defense in depth', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { slug: 'admin' }, - roles: [{ slug: 'admin' }], - raw_attributes: {}, - custom_attributes: {}, - }, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - await workos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - codeVerifier: 'test_code_verifier_value', - }); - - const body = fetchBody(); - expect(body).toContain('code_verifier=test_code_verifier_value'); - expect(body).toContain( - 'client_secret=sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU', - ); - }); - }); - - describe('public client mode (codeVerifier without API key)', () => { - let publicWorkos: WorkOS; - let originalApiKey: string | undefined; - - beforeEach(() => { - originalApiKey = process.env.WORKOS_API_KEY; - delete process.env.WORKOS_API_KEY; - publicWorkos = new WorkOS({ clientId: 'proj_123' }); - }); - - afterEach(() => { - process.env.WORKOS_API_KEY = originalApiKey; - }); - - it('sends code_verifier without client_secret', async () => { - fetchOnce({ - access_token: '01DMEK0J53CVMC32CK5SE0KZ8Q', - profile: { - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { slug: 'admin' }, - roles: [{ slug: 'admin' }], - raw_attributes: {}, - custom_attributes: {}, - }, - }); - - const { accessToken } = await publicWorkos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - codeVerifier: 'test_code_verifier_value', - }); - - expect(accessToken).toBe('01DMEK0J53CVMC32CK5SE0KZ8Q'); - const body = fetchBody(); - expect(body).toContain('code_verifier=test_code_verifier_value'); - expect(body).not.toContain('client_secret'); - }); - - it('throws error when neither codeVerifier nor API key is provided', async () => { - await expect( - publicWorkos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - }), - ).rejects.toThrow( - 'getProfileAndToken requires either a codeVerifier (for public clients) ' + - 'or an API key configured on the WorkOS instance (for confidential clients).', - ); - }); - - it('throws error when codeVerifier is an empty string', async () => { - await expect( - publicWorkos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - codeVerifier: '', - }), - ).rejects.toThrow( - 'codeVerifier cannot be an empty string. ' + - 'Generate a valid PKCE pair using workos.pkce.generate().', - ); - }); - - it('throws error when codeVerifier is whitespace only', async () => { - await expect( - publicWorkos.sso.getProfileAndToken({ - code: 'authorization_code', - clientId: 'proj_123', - codeVerifier: ' ', - }), - ).rejects.toThrow( - 'codeVerifier cannot be an empty string. ' + - 'Generate a valid PKCE pair using workos.pkce.generate().', - ); - }); - }); - }); - - describe('getProfile', () => { - it('calls the `/sso/profile` endpoint with the provided access token', async () => { - fetchOnce({ - id: 'prof_123', - idp_id: '123', - organization_id: 'org_123', - connection_id: 'conn_123', - connection_type: 'OktaSAML', - email: 'foo@test.com', - name: 'foo bar', - first_name: 'foo', - last_name: 'bar', - role: { - slug: 'admin', - }, - roles: [{ slug: 'admin' }], - groups: ['Admins', 'Developers'], - raw_attributes: { - email: 'foo@test.com', - first_name: 'foo', - last_name: 'bar', - groups: ['Admins', 'Developers'], - }, - custom_attributes: {}, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - const profile = await workos.sso.getProfile({ - accessToken: 'access_token', - }); - - expect(fetch.mock.calls.length).toEqual(1); - expect(fetchHeaders()).toMatchObject({ - Authorization: 'Bearer access_token', - }); - - expect(profile.id).toBe('prof_123'); - }); - }); - - describe('deleteConnection', () => { - it('sends request to delete a Connection', async () => { - fetchOnce(); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - await workos.sso.deleteConnection('conn_123'); - - expect(fetchURL()).toContain('/connections/conn_123'); - }); - }); - - describe('getConnection', () => { - it(`requests a Connection`, async () => { - fetchOnce(connectionResponse); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const subject = await workos.sso.getConnection('conn_123'); - - expect(fetchURL()).toContain('/connections/conn_123'); +describe('SSO', () => { + beforeEach(() => fetch.resetMocks()); - expect(subject.type).toEqual('OktaSAML'); - }); + describe('listConnections', () => { + it('returns paginated results', async () => { + fetchOnce(listConnectionFixture); + + const { data, listMetadata } = await workos.sso.listConnections({ + order: 'desc', + connectionType: ConnectionType.ADFSSAML, + domain: 'foo-corp.com', + organizationId: 'org_01EHWNCE74X7JSDV0X3SZ3KJNY', + search: 'Foo Corp', + }); + + expect(fetchMethod()).toBe('GET'); + expect(new URL(String(fetchURL())).pathname).toBe('/connections'); + expect(fetchSearchParams()).toHaveProperty('order'); + expect(Array.isArray(data)).toBe(true); + expect(listMetadata).toBeDefined(); + expect(data.length).toBeGreaterThan(0); + expectConnection(data[0]); }); + }); - describe('listConnections', () => { - it(`requests a list of Connections`, async () => { - fetchOnce({ - data: [connectionResponse], - list_metadata: {}, - }); - - const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); - - const subject = await workos.sso.listConnections({ - organizationId: 'org_1234', - }); - - expect(fetchURL()).toContain('/connections'); - - expect(subject.data).toHaveLength(1); - }); + describe('authorizeLogout', () => { + it('sends the correct request and returns result', async () => { + fetchOnce(ssoLogoutAuthorizeResponseFixture); + + const result = await workos.sso.authorizeLogout({ + profileId: 'profile_id_01234', + }); + + expect(fetchMethod()).toBe('POST'); + expect(new URL(String(fetchURL())).pathname).toBe( + '/sso/logout/authorize', + ); + expect(fetchBody()).toEqual( + expect.objectContaining({ profile_id: 'profile_id_01234' }), + ); + expect(result.logoutUrl).toBe( + 'https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9', + ); + expect(result.logoutToken).toBe( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4', + ); }); }); }); diff --git a/src/sso/sso.ts b/src/sso/sso.ts index 906cf5565..569ca0c23 100644 --- a/src/sso/sso.ts +++ b/src/sso/sso.ts @@ -1,32 +1,57 @@ -import { UnknownRecord } from '../common/interfaces/unknown-record.interface'; -import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; +// This file is auto-generated by oagen. Do not edit. + +import type { WorkOS } from '../workos'; +import type { PaginationOptions } from '../common/interfaces/pagination-options.interface'; import { AutoPaginatable } from '../common/utils/pagination'; +import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; import { toQueryString } from '../common/utils/query-string'; -import { WorkOS } from '../workos'; -import { +import type { ListConnectionsOptions } from './interfaces/list-connections-options.interface'; +import type { GetLogoutUrlOptions } from './interfaces/get-logout-url-options.interface'; +import type { AuthorizeLogoutOptions } from './interfaces/authorize-logout-options.interface'; +import type { + SSOLogoutAuthorizeResponse, + SSOLogoutAuthorizeResponseWire, +} from './interfaces/sso-logout-authorize-response.interface'; +import type { SSOLogoutAuthorizeRequestResponse } from './interfaces/sso-logout-authorize-request.interface'; +import type { Connection, ConnectionResponse, +} from './interfaces/connection.interface'; +import { deserializeSSOLogoutAuthorizeResponse } from './serializers/sso-logout-authorize-response.serializer'; +import { deserializeConnection } from './serializers/connection.serializer'; +import { serializeSSOLogoutAuthorizeRequest } from './serializers/sso-logout-authorize-request.serializer'; +import { UnknownRecord } from '../common/interfaces/unknown-record.interface'; +import { GetProfileAndTokenOptions, GetProfileOptions, - ListConnectionsOptions, Profile, ProfileAndToken, ProfileAndTokenResponse, ProfileResponse, SSOAuthorizationURLOptions, SSOPKCEAuthorizationURLResult, - SerializedListConnectionsOptions, } from './interfaces'; -import { - deserializeConnection, - deserializeProfile, - serializeListConnectionsOptions, -} from './serializers'; -// Imported from the concrete file rather than the serializer barrel: when SSO -// is oagen-owned the regenerated barrel only re-exports serializers for -// generated methods, and `getProfileAndToken` is hand-owned below. +import { deserializeProfile } from './serializers'; import { deserializeProfileAndToken } from './serializers/profile-and-token.serializer'; +const serializeListConnectionsOptions = ( + options: ListConnectionsOptions, +): PaginationOptions => { + const wire: Record = { + limit: options.limit, + before: options.before, + after: options.after, + order: options.order, + }; + if (options.connectionType !== undefined) + wire.connection_type = options.connectionType; + if (options.domain !== undefined) wire.domain = options.domain; + if (options.organizationId !== undefined) + wire.organization_id = options.organizationId; + if (options.search !== undefined) wire.search = options.search; + return wire as PaginationOptions; +}; + export class SSO { constructor(private readonly workos: WorkOS) {} @@ -36,12 +61,12 @@ export class SSO { * Get a list of all of your existing connections matching the criteria specified. * @param options - Pagination and filter options. * @returns {Promise>} - * @throws 403 response from the API. + * @throws {AuthorizationException} 403 * @throws {UnprocessableEntityException} 422 */ async listConnections( options?: ListConnectionsOptions, - ): Promise> { + ): Promise> { return new AutoPaginatable( await fetchAndDeserialize( this.workos, @@ -59,6 +84,39 @@ export class SSO { options ? serializeListConnectionsOptions(options) : undefined, ); } + + /** + * Logout Redirect + * + * Logout allows to sign out a user from your application by triggering the identity provider sign out flow. This `GET` endpoint should be a redirection, since the identity provider user will be identified in the browser session. + * + * Before redirecting to this endpoint, you need to generate a short-lived logout token using the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + * @returns {string} The constructed URL. + */ + getLogoutUrl(options: GetLogoutUrlOptions): string { + const query = toQueryString({ token: options.token }); + return `${this.workos.baseURL}/sso/logout?${query}`; + } + + /** + * Logout Authorize + * + * You should call this endpoint from your server to generate a logout token which is required for the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint. + * @returns {Promise} + * @throws {BadRequestException} 400 + * @throws {NotFoundException} 404 + */ + async authorizeLogout( + options: AuthorizeLogoutOptions, + ): Promise { + const payload = options; + const { data } = await this.workos.post< + SSOLogoutAuthorizeResponseWire, + SSOLogoutAuthorizeRequestResponse + >('/sso/logout/authorize', serializeSSOLogoutAuthorizeRequest(payload)); + return deserializeSSOLogoutAuthorizeResponse(data); + } + // @oagen-ignore-start /** * Delete a Connection From 0a9c6d15abc9b93c7f5ec4e196d1d55d3f2d98bf Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Thu, 18 Jun 2026 15:49:05 -0400 Subject: [PATCH 3/3] feat(sso): expose Connection.type and consolidate ConnectionType Regenerate SSO with the connection_type wire field surfaced under the domain name `type` (via the new fieldHints), preserving the long-standing public SDK surface; wire serialization still uses connection_type. Drop the redundant hand-written connection-type.enum and point ListConnectionsOptions at the generated connection-type.interface so there is a single ConnectionType. Update the events spec's connection fixture to match. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/events/events.spec.ts | 2 +- src/sso/interfaces/connection-type.enum.ts | 40 ------------------- .../interfaces/connection-type.interface.ts | 18 ++++----- src/sso/interfaces/connection.interface.ts | 2 +- .../list-connections-options.interface.ts | 2 +- src/sso/interfaces/profile.interface.ts | 10 ++--- src/sso/serializers/connection.serializer.ts | 2 +- src/sso/serializers/profile.serializer.ts | 8 ++-- src/sso/sso.spec.ts | 4 +- src/sso/sso.ts | 5 ++- 10 files changed, 27 insertions(+), 66 deletions(-) delete mode 100644 src/sso/interfaces/connection-type.enum.ts diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index 93c6329e7..d72f1e469 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -63,7 +63,7 @@ describe('Event', () => { id: 'conn_01234ABCD', organizationId: 'org_1234', name: 'Connection', - connectionType: ConnectionType.OktaSAML, + type: ConnectionType.OktaSAML, state: 'active', domains: [], createdAt: new Date('2020-05-06 04:21:48.649164'), diff --git a/src/sso/interfaces/connection-type.enum.ts b/src/sso/interfaces/connection-type.enum.ts deleted file mode 100644 index 188895f3d..000000000 --- a/src/sso/interfaces/connection-type.enum.ts +++ /dev/null @@ -1,40 +0,0 @@ -export enum ConnectionType { - ADFSSAML = 'ADFSSAML', - AdpOidc = 'AdpOidc', - AppleOAuth = 'AppleOAuth', - Auth0SAML = 'Auth0SAML', - AzureSAML = 'AzureSAML', - CasSAML = 'CasSAML', - CleverOIDC = 'CleverOIDC', - ClassLinkSAML = 'ClassLinkSAML', - CloudflareSAML = 'CloudflareSAML', - CyberArkSAML = 'CyberArkSAML', - DuoSAML = 'DuoSAML', - EntraIdOIDC = 'EntraIdOIDC', - GenericOIDC = 'GenericOIDC', - GenericSAML = 'GenericSAML', - GitHubOAuth = 'GitHubOAuth', - GoogleOAuth = 'GoogleOAuth', - GoogleSAML = 'GoogleSAML', - JumpCloudSAML = 'JumpCloudSAML', - KeycloakSAML = 'KeycloakSAML', - LastPassSAML = 'LastPassSAML', - LoginGovOidc = 'LoginGovOidc', - MagicLink = 'MagicLink', - MicrosoftOAuth = 'MicrosoftOAuth', - MiniOrangeSAML = 'MiniOrangeSAML', - NetIqSAML = 'NetIqSAML', - OktaOIDC = 'OktaOIDC', - OktaSAML = 'OktaSAML', - OneLoginSAML = 'OneLoginSAML', - OracleSAML = 'OracleSAML', - PingFederateSAML = 'PingFederateSAML', - PingOneSAML = 'PingOneSAML', - RipplingSAML = 'RipplingSAML', - SalesforceOAuth = 'SalesforceOAuth', - SalesforceSAML = 'SalesforceSAML', - ShibbolethGenericSAML = 'ShibbolethGenericSAML', - ShibbolethSAML = 'ShibbolethSAML', - SimpleSamlPhpSAML = 'SimpleSamlPhpSAML', - VMwareSAML = 'VMwareSAML', -} diff --git a/src/sso/interfaces/connection-type.interface.ts b/src/sso/interfaces/connection-type.interface.ts index 41c35ca8e..85085c43b 100644 --- a/src/sso/interfaces/connection-type.interface.ts +++ b/src/sso/interfaces/connection-type.interface.ts @@ -4,23 +4,30 @@ export enum ConnectionType { ADFSSAML = 'ADFSSAML', AdpOidc = 'AdpOidc', AppleOAuth = 'AppleOAuth', + Auth0Migration = 'Auth0Migration', Auth0SAML = 'Auth0SAML', AzureSAML = 'AzureSAML', + BitbucketOAuth = 'BitbucketOAuth', CasSAML = 'CasSAML', ClassLinkSAML = 'ClassLinkSAML', CleverOIDC = 'CleverOIDC', CloudflareSAML = 'CloudflareSAML', CyberArkSAML = 'CyberArkSAML', + DiscordOAuth = 'DiscordOAuth', DuoSAML = 'DuoSAML', EntraIdOIDC = 'EntraIdOIDC', GenericOIDC = 'GenericOIDC', GenericSAML = 'GenericSAML', GitHubOAuth = 'GitHubOAuth', + GitLabOAuth = 'GitLabOAuth', GoogleOAuth = 'GoogleOAuth', + GoogleOidc = 'GoogleOIDC', GoogleSAML = 'GoogleSAML', + IntuitOAuth = 'IntuitOAuth', JumpCloudSAML = 'JumpCloudSAML', KeycloakSAML = 'KeycloakSAML', LastPassSAML = 'LastPassSAML', + LinkedInOAuth = 'LinkedInOAuth', LoginGovOidc = 'LoginGovOidc', MagicLink = 'MagicLink', MicrosoftOAuth = 'MicrosoftOAuth', @@ -30,6 +37,7 @@ export enum ConnectionType { OktaSAML = 'OktaSAML', OneLoginSAML = 'OneLoginSAML', OracleSAML = 'OracleSAML', + Pending = 'Pending', PingFederateSAML = 'PingFederateSAML', PingOneSAML = 'PingOneSAML', RipplingSAML = 'RipplingSAML', @@ -38,17 +46,9 @@ export enum ConnectionType { ShibbolethGenericSAML = 'ShibbolethGenericSAML', ShibbolethSAML = 'ShibbolethSAML', SimpleSamlPhpSAML = 'SimpleSamlPhpSAML', - VMwareSAML = 'VMwareSAML', - Pending = 'Pending', - Auth0Migration = 'Auth0Migration', - BitbucketOAuth = 'BitbucketOAuth', - DiscordOAuth = 'DiscordOAuth', - GitLabOAuth = 'GitLabOAuth', - GoogleOidc = 'GoogleOIDC', - IntuitOAuth = 'IntuitOAuth', - LinkedInOAuth = 'LinkedInOAuth', SlackOAuth = 'SlackOAuth', TestIdp = 'TestIdp', + VMwareSAML = 'VMwareSAML', VercelMarketplaceOAuth = 'VercelMarketplaceOAuth', VercelOAuth = 'VercelOAuth', XeroOAuth = 'XeroOAuth', diff --git a/src/sso/interfaces/connection.interface.ts b/src/sso/interfaces/connection.interface.ts index 711a19eb2..b7bffcfe5 100644 --- a/src/sso/interfaces/connection.interface.ts +++ b/src/sso/interfaces/connection.interface.ts @@ -16,7 +16,7 @@ export interface Connection { /** Unique identifier for the Organization in which the Connection resides. */ organizationId?: string; /** The type of the SSO Connection used to authenticate the user. The Connection type may be used to dynamically generate authorization URLs. */ - connectionType?: ConnectionType; + type?: ConnectionType; /** A human-readable name for the Connection. This will most commonly be the organization's name. */ name: string; /** Indicates whether a Connection is able to authenticate users. */ diff --git a/src/sso/interfaces/list-connections-options.interface.ts b/src/sso/interfaces/list-connections-options.interface.ts index 1c68e539c..cb8a3cc28 100644 --- a/src/sso/interfaces/list-connections-options.interface.ts +++ b/src/sso/interfaces/list-connections-options.interface.ts @@ -1,5 +1,5 @@ import { PaginationOptions } from '../../common/interfaces/pagination-options.interface'; -import { ConnectionType } from './connection-type.enum'; +import { ConnectionType } from './connection-type.interface'; export interface ListConnectionsOptions extends PaginationOptions { /** Filter Connections by their type. */ diff --git a/src/sso/interfaces/profile.interface.ts b/src/sso/interfaces/profile.interface.ts index ed978798b..b5f0f83fc 100644 --- a/src/sso/interfaces/profile.interface.ts +++ b/src/sso/interfaces/profile.interface.ts @@ -3,8 +3,6 @@ import type { SlimRoleResponse } from './slim-role.interface'; import type { ConnectionType } from './connection-type.interface'; -type RoleResponse = SlimRoleResponse; - export interface Profile< GenericType extends Record = Record, > { @@ -29,9 +27,9 @@ export interface Profile< /** The user's full name. */ name?: string | null; /** The role assigned to the user within the organization, if applicable. */ - role?: RoleResponse; + role?: SlimRoleResponse; /** The roles assigned to the user within the organization, if applicable. */ - roles?: RoleResponse[]; + roles?: SlimRoleResponse[]; /** The groups the user belongs to, as returned by the identity provider. */ groups?: string[]; /** Custom attribute mappings defined for the connection, returned as key-value pairs. */ @@ -53,8 +51,8 @@ export interface ProfileResponse< first_name?: string | null; last_name?: string | null; name?: string | null; - role?: RoleResponse; - roles?: RoleResponse[]; + role?: SlimRoleResponse; + roles?: SlimRoleResponse[]; groups?: string[]; custom_attributes?: GenericType; raw_attributes?: { [key: string]: any }; diff --git a/src/sso/serializers/connection.serializer.ts b/src/sso/serializers/connection.serializer.ts index 884be9b29..8f37552e5 100644 --- a/src/sso/serializers/connection.serializer.ts +++ b/src/sso/serializers/connection.serializer.ts @@ -13,7 +13,7 @@ export const deserializeConnection = ( object: response.object, id: response.id, organizationId: response.organization_id, - connectionType: response.connection_type, + type: response.connection_type, name: response.name, state: response.state, status: response.status, diff --git a/src/sso/serializers/profile.serializer.ts b/src/sso/serializers/profile.serializer.ts index caa9f1eea..8433ddbd2 100644 --- a/src/sso/serializers/profile.serializer.ts +++ b/src/sso/serializers/profile.serializer.ts @@ -10,14 +10,14 @@ export const deserializeProfile = < ): Profile => ({ object: response.object ?? 'profile', id: response.id, - organizationId: response.organization_id ?? undefined, + organizationId: response.organization_id ?? null, connectionId: response.connection_id, connectionType: response.connection_type, idpId: response.idp_id, email: response.email, - firstName: response.first_name ?? undefined, - lastName: response.last_name ?? undefined, - name: response.name ?? undefined, + firstName: response.first_name ?? null, + lastName: response.last_name ?? null, + name: response.name ?? null, role: response.role != null ? deserializeSlimRole(response.role) : undefined, roles: response.roles != null diff --git a/src/sso/sso.spec.ts b/src/sso/sso.spec.ts index 5987b0419..5772a54c8 100644 --- a/src/sso/sso.spec.ts +++ b/src/sso/sso.spec.ts @@ -9,7 +9,7 @@ import { fetchBody, } from '../common/utils/test-utils'; import { WorkOS } from '../workos'; -import { ConnectionType } from './interfaces/connection-type.enum'; +import { ConnectionType } from './interfaces/connection-type.interface'; import listConnectionFixture from './fixtures/list-connection.json'; import ssoLogoutAuthorizeResponseFixture from './fixtures/sso-logout-authorize-response.json'; @@ -19,7 +19,7 @@ const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); function expectConnection(result: any) { expect(result.object).toBe('connection'); expect(result.id).toBe('conn_01E4ZCR3C56J083X43JQXF3JK5'); - expect(result.connectionType).toBe('OktaSAML'); + expect(result.type).toBe('OktaSAML'); expect(result.name).toBe('Foo Corp'); expect(result.state).toBe('active'); expect(result.status).toBe('linked'); diff --git a/src/sso/sso.ts b/src/sso/sso.ts index 569ca0c23..7ab5c602c 100644 --- a/src/sso/sso.ts +++ b/src/sso/sso.ts @@ -60,7 +60,7 @@ export class SSO { * * Get a list of all of your existing connections matching the criteria specified. * @param options - Pagination and filter options. - * @returns {Promise>} + * @returns {Promise>} * @throws {AuthorizationException} 403 * @throws {UnprocessableEntityException} 422 */ @@ -102,6 +102,9 @@ export class SSO { * Logout Authorize * * You should call this endpoint from your server to generate a logout token which is required for the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint. + * @param options - Object containing profileId. + * @param options.profileId - The unique ID of the profile to log out. + * @example "prof_01HXYZ123456789ABCDEFGHIJ" * @returns {Promise} * @throws {BadRequestException} 400 * @throws {NotFoundException} 404