From 1f6a3d0431e935eaa016fc716d46a723cc34d9d6 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 21 Apr 2026 12:36:42 -0400 Subject: [PATCH 1/3] fix: Normalize `GithubOAuth` to `GitHubOAuth` in identity deserialization The API returns `GithubOAuth` from getUserIdentities but getAuthorizationUrl expects `GitHubOAuth`. Normalize during deserialization so the provider value can be passed directly. Closes #1227 Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/V9_MIGRATION_GUIDE.md | 28 +++++++++++++++++++ .../interfaces/identity.interface.ts | 3 +- .../serializers/identity.serializer.spec.ts | 25 +++++++++++++++++ .../serializers/identity.serializer.ts | 14 +++++++++- src/user-management/user-management.spec.ts | 21 +++++++++++++- 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/user-management/serializers/identity.serializer.spec.ts diff --git a/docs/V9_MIGRATION_GUIDE.md b/docs/V9_MIGRATION_GUIDE.md index 6797967e2..e484ef1b8 100644 --- a/docs/V9_MIGRATION_GUIDE.md +++ b/docs/V9_MIGRATION_GUIDE.md @@ -363,3 +363,31 @@ This is intended to make strict TypeScript error handling easier, especially if The v9 update also tightens webhook/event deserialization behavior for unknown event types. If your integration assumed unknown event types would be ignored or treated loosely, re-test webhook handling on the latest v9 release. + +### User Management + +#### `getUserIdentities` now normalizes `GithubOAuth` to `GitHubOAuth` + +The WorkOS API returns `GithubOAuth` (lowercase 'h') as the provider in identity responses, but `getAuthorizationUrl` expects `GitHubOAuth` (uppercase 'H'). The SDK now normalizes the casing during deserialization so that identity provider values can be passed directly to `getAuthorizationUrl`. + +If your code was comparing against the raw API value `'GithubOAuth'`, update it to `'GitHubOAuth'`: + +**Before (v8):** + +```typescript +const identities = await workos.userManagement.getUserIdentities(userId); +const github = identities.find((i) => i.provider === 'GithubOAuth'); +``` + +**After (v9):** + +```typescript +const identities = await workos.userManagement.getUserIdentities(userId); +const github = identities.find((i) => i.provider === 'GitHubOAuth'); + +// The provider value can now be passed directly to getAuthorizationUrl +const url = workos.userManagement.getAuthorizationUrl({ + provider: github.provider, + redirectUri: 'https://example.com/callback', +}); +``` diff --git a/src/user-management/interfaces/identity.interface.ts b/src/user-management/interfaces/identity.interface.ts index 00fe07178..21c48c9ad 100644 --- a/src/user-management/interfaces/identity.interface.ts +++ b/src/user-management/interfaces/identity.interface.ts @@ -14,8 +14,9 @@ export interface IdentityResponse { type: 'OAuth'; provider: | 'AppleOAuth' - | 'GoogleOAuth' + | 'GithubOAuth' | 'GitHubOAuth' + | 'GoogleOAuth' | 'MicrosoftOAuth' | 'SalesforceOAuth'; } diff --git a/src/user-management/serializers/identity.serializer.spec.ts b/src/user-management/serializers/identity.serializer.spec.ts new file mode 100644 index 000000000..65c029a22 --- /dev/null +++ b/src/user-management/serializers/identity.serializer.spec.ts @@ -0,0 +1,25 @@ +import { deserializeIdentities } from './identity.serializer'; + +describe('deserializeIdentities', () => { + it('normalizes GithubOAuth to GitHubOAuth', () => { + const result = deserializeIdentities([ + { idp_id: '123', type: 'OAuth', provider: 'GithubOAuth' }, + ]); + + expect(result).toEqual([ + { idpId: '123', type: 'OAuth', provider: 'GitHubOAuth' }, + ]); + }); + + it('leaves other providers unchanged', () => { + const result = deserializeIdentities([ + { idp_id: '456', type: 'OAuth', provider: 'GoogleOAuth' }, + { idp_id: '789', type: 'OAuth', provider: 'AppleOAuth' }, + ]); + + expect(result).toEqual([ + { idpId: '456', type: 'OAuth', provider: 'GoogleOAuth' }, + { idpId: '789', type: 'OAuth', provider: 'AppleOAuth' }, + ]); + }); +}); diff --git a/src/user-management/serializers/identity.serializer.ts b/src/user-management/serializers/identity.serializer.ts index 04df64498..3cce1e1c8 100644 --- a/src/user-management/serializers/identity.serializer.ts +++ b/src/user-management/serializers/identity.serializer.ts @@ -1,5 +1,17 @@ import { Identity, IdentityResponse } from '../interfaces/identity.interface'; +// The API returns 'GithubOAuth' but getAuthorizationUrl expects 'GitHubOAuth'. +// Normalize here so callers can pass identity.provider directly. +// See: https://github.com/workos/workos-node/issues/1227 +const normalizeProvider = ( + provider: IdentityResponse['provider'], +): Identity['provider'] => { + if (provider === 'GithubOAuth') { + return 'GitHubOAuth'; + } + return provider; +}; + export const deserializeIdentities = ( identities: IdentityResponse[], ): Identity[] => { @@ -7,7 +19,7 @@ export const deserializeIdentities = ( return { idpId: identity.idp_id, type: identity.type, - provider: identity.provider, + provider: normalizeProvider(identity.provider), }; }); }; diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index bf4073d1d..88bf2faa6 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -1732,7 +1732,7 @@ describe('UserManagement', () => { { idpId: '108872335', type: 'OAuth', - provider: 'GithubOAuth', + provider: 'GitHubOAuth', }, { idpId: '111966195055680542408', @@ -1741,6 +1741,25 @@ describe('UserManagement', () => { }, ]); }); + + it('normalizes GithubOAuth to GitHubOAuth so it can be passed to getAuthorizationUrl', async () => { + fetchOnce(identityFixture); + + const identities = + await workos.userManagement.getUserIdentities(userId); + + const githubIdentity = identities.find( + (i) => i.provider === 'GitHubOAuth', + ); + expect(githubIdentity).toBeDefined(); + + const url = workos.userManagement.getAuthorizationUrl({ + provider: githubIdentity!.provider, + redirectUri: 'https://example.com/callback', + }); + + expect(url).toContain('provider=GitHubOAuth'); + }); }); describe('getOrganizationMembership', () => { From e71e74b766683f7bbd77de4259b3861f18c05f74 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 21 Apr 2026 12:49:14 -0400 Subject: [PATCH 2/3] lint --- src/user-management/user-management.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/user-management/user-management.spec.ts b/src/user-management/user-management.spec.ts index 88bf2faa6..66894530c 100644 --- a/src/user-management/user-management.spec.ts +++ b/src/user-management/user-management.spec.ts @@ -1745,8 +1745,7 @@ describe('UserManagement', () => { it('normalizes GithubOAuth to GitHubOAuth so it can be passed to getAuthorizationUrl', async () => { fetchOnce(identityFixture); - const identities = - await workos.userManagement.getUserIdentities(userId); + const identities = await workos.userManagement.getUserIdentities(userId); const githubIdentity = identities.find( (i) => i.provider === 'GitHubOAuth', From e1b9ff93cbbfe886fc9afa8fc96d566822c72903 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 21 Apr 2026 12:54:51 -0400 Subject: [PATCH 3/3] hide bad casing in a non-reexported raw type instead of the public identity interface --- .../interfaces/identity-response.interface.ts | 13 +++++++++++++ .../interfaces/identity.interface.ts | 12 ------------ .../serializers/identity.serializer.ts | 7 ++++--- src/user-management/user-management.ts | 5 +++-- 4 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 src/user-management/interfaces/identity-response.interface.ts diff --git a/src/user-management/interfaces/identity-response.interface.ts b/src/user-management/interfaces/identity-response.interface.ts new file mode 100644 index 000000000..0d7042625 --- /dev/null +++ b/src/user-management/interfaces/identity-response.interface.ts @@ -0,0 +1,13 @@ +interface RawIdentityResponse { + idp_id: string; + type: 'OAuth'; + provider: + | 'AppleOAuth' + | 'GithubOAuth' + | 'GitHubOAuth' + | 'GoogleOAuth' + | 'MicrosoftOAuth' + | 'SalesforceOAuth'; +} + +export type { RawIdentityResponse }; diff --git a/src/user-management/interfaces/identity.interface.ts b/src/user-management/interfaces/identity.interface.ts index 21c48c9ad..c7b51724c 100644 --- a/src/user-management/interfaces/identity.interface.ts +++ b/src/user-management/interfaces/identity.interface.ts @@ -8,15 +8,3 @@ export interface Identity { | 'MicrosoftOAuth' | 'SalesforceOAuth'; } - -export interface IdentityResponse { - idp_id: string; - type: 'OAuth'; - provider: - | 'AppleOAuth' - | 'GithubOAuth' - | 'GitHubOAuth' - | 'GoogleOAuth' - | 'MicrosoftOAuth' - | 'SalesforceOAuth'; -} diff --git a/src/user-management/serializers/identity.serializer.ts b/src/user-management/serializers/identity.serializer.ts index 3cce1e1c8..135fc6aca 100644 --- a/src/user-management/serializers/identity.serializer.ts +++ b/src/user-management/serializers/identity.serializer.ts @@ -1,10 +1,11 @@ -import { Identity, IdentityResponse } from '../interfaces/identity.interface'; +import { Identity } from '../interfaces/identity.interface'; +import { RawIdentityResponse } from '../interfaces/identity-response.interface'; // The API returns 'GithubOAuth' but getAuthorizationUrl expects 'GitHubOAuth'. // Normalize here so callers can pass identity.provider directly. // See: https://github.com/workos/workos-node/issues/1227 const normalizeProvider = ( - provider: IdentityResponse['provider'], + provider: RawIdentityResponse['provider'], ): Identity['provider'] => { if (provider === 'GithubOAuth') { return 'GitHubOAuth'; @@ -13,7 +14,7 @@ const normalizeProvider = ( }; export const deserializeIdentities = ( - identities: IdentityResponse[], + identities: RawIdentityResponse[], ): Identity[] => { return identities.map((identity) => { return { diff --git a/src/user-management/user-management.ts b/src/user-management/user-management.ts index cff94a5ce..ada58b696 100644 --- a/src/user-management/user-management.ts +++ b/src/user-management/user-management.ts @@ -73,7 +73,8 @@ import { CreateOrganizationMembershipOptions, SerializedCreateOrganizationMembershipOptions, } from './interfaces/create-organization-membership-options.interface'; -import { Identity, IdentityResponse } from './interfaces/identity.interface'; +import { Identity } from './interfaces/identity.interface'; +import { RawIdentityResponse } from './interfaces/identity-response.interface'; import { Invitation, InvitationResponse, @@ -812,7 +813,7 @@ export class UserManagement { throw new TypeError(`Incomplete arguments. Need to specify 'userId'.`); } - const { data } = await this.workos.get( + const { data } = await this.workos.get( `/user_management/users/${userId}/identities`, );