Skip to content

Commit 4699466

Browse files
committed
[V8] create client API for methods that don't require an API key (#1354)
This pull request introduces a new set of client-only methods for building OAuth and SSO URLs that do not require a WorkOS API key, making them suitable for browser-based PKCE flows and similar use cases. The changes include the addition of new client modules for user management and SSO, comprehensive tests for these modules, a shared query string utility, and updates to the main SDK exports. Additionally, the server-side SDK now delegates URL generation to the new client modules for consistency and maintainability. **Client Module Additions and Exports:** * Added new `src/client/user-management.ts` and `src/client/sso.ts` modules, providing functions to build authorization, logout, and JWKS URLs for OAuth and SSO flows without requiring an API key. These modules include robust argument validation and support for custom query parameters and PKCE. [[1]](diffhunk://#diff-b5a04503adce4aaadee02b4511ee9bd11ec26a46927bde7c07d85ad31786e4bbR1-R147) [[2]](diffhunk://#diff-aba556dc64a77e993f9ce2de8ffd20b276128d1f6f4ba69bf2967e05dc1f7676R1-R62) * Updated `src/index.client.ts` and `src/client/index.ts` to export the new client modules and their types, making them easily accessible for browser environments. [[1]](diffhunk://#diff-0311bcbdf2e78d79d36c4417c3c6d6409834421d79d111e294dbacf47e487984R1-R19) [[2]](diffhunk://#diff-24f1ac19d22913269d99a8a5afa4c13aa7fd2c8b9858529046424841ca6421daR1-R18) * Updated the package export map in `package.json` to include the new client entry points for both ESM and CJS consumers. **Testing and Utilities:** * Added comprehensive test suites for the new client modules in `src/client/user-management.spec.ts`, `src/client/sso.spec.ts`, and integration tests in `src/index.client.spec.ts`, ensuring correct URL generation and error handling for all supported options. [[1]](diffhunk://#diff-01061808fc40490ae09053b5553be015aa94e42cb7efcf8dbeba57a23c699e80R1-R219) [[2]](diffhunk://#diff-28da04c6058919471ee96a352c58c206146b8ac07454e7ab25613a59a20fc7bdR1-R114) [[3]](diffhunk://#diff-da4fa2fd6101519fd777b8803892a919b567b988b1bb48fc941671e20beb23ecR1-R47) * Introduced a shared `toQueryString` utility in `src/client/utils.ts` for consistent and backwards-compatible query string generation across client modules. **Server SDK Refactoring:** * Updated server-side SDK modules (`src/sso/sso.ts` and `src/user-management/user-management.ts`) to delegate authorization URL generation to the new client modules, ensuring consistency and reducing code duplication. [[1]](diffhunk://#diff-40f88259248b65c47b88e6460bbb0e52cd4f70fc298c96319889051c19bcd3a6L1-R1) [[2]](diffhunk://#diff-40f88259248b65c47b88e6460bbb0e52cd4f70fc298c96319889051c19bcd3a6L26-L40) [[3]](diffhunk://#diff-40f88259248b65c47b88e6460bbb0e52cd4f70fc298c96319889051c19bcd3a6L68-L100) [[4]](diffhunk://#diff-5df813524b03843b7a76359f2c080a36e82f2c065c578794071eb6ffcc0efa7cL3-R3) These changes collectively improve the SDK's usability for client-side OAuth flows, enhance maintainability, and ensure robust, well-tested URL generation logic.
1 parent b4f703c commit 4699466

File tree

15 files changed

+726
-152
lines changed

15 files changed

+726
-152
lines changed

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@
103103
"require": "./lib/cjs/index.cjs",
104104
"default": "./lib/esm/index.js"
105105
},
106+
"./client": {
107+
"types": {
108+
"require": "./lib/cjs/index.client.d.cts",
109+
"import": "./lib/esm/index.client.d.ts"
110+
},
111+
"import": "./lib/esm/index.client.js",
112+
"require": "./lib/cjs/index.client.cjs",
113+
"default": "./lib/esm/index.client.js"
114+
},
106115
"./worker": {
107116
"types": {
108117
"require": "./lib/cjs/index.worker.d.cts",

src/client/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Client methods that can be used without a WorkOS API key.
3+
* These are OAuth client operations suitable for PKCE flows.
4+
*/
5+
6+
// User Management client methods and types
7+
export * as userManagement from './user-management';
8+
9+
// SSO client methods and types
10+
export * as sso from './sso';
11+
12+
// Re-export specific types for convenience
13+
export type {
14+
AuthorizationURLOptions as UserManagementAuthorizationURLOptions,
15+
LogoutURLOptions,
16+
} from './user-management';
17+
18+
export type { SSOAuthorizationURLOptions } from './sso';

src/client/sso.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { getAuthorizationUrl } from './sso';
2+
3+
describe('Public SSO Methods', () => {
4+
describe('getAuthorizationUrl', () => {
5+
it('builds correct URL with provider', () => {
6+
const url = getAuthorizationUrl({
7+
clientId: 'client_123',
8+
redirectUri: 'https://app.com/callback',
9+
provider: 'GoogleOAuth',
10+
});
11+
12+
expect(url).toContain('https://api.workos.com/sso/authorize');
13+
expect(url).toContain('client_id=client_123');
14+
expect(url).toContain('redirect_uri=https%3A%2F%2Fapp.com%2Fcallback');
15+
expect(url).toContain('provider=GoogleOAuth');
16+
expect(url).toContain('response_type=code');
17+
});
18+
19+
it('builds URL with organization', () => {
20+
const url = getAuthorizationUrl({
21+
clientId: 'client_123',
22+
redirectUri: 'https://app.com/callback',
23+
organization: 'org_123',
24+
});
25+
26+
expect(url).toContain('organization=org_123');
27+
});
28+
29+
it('builds URL with connection', () => {
30+
const url = getAuthorizationUrl({
31+
clientId: 'client_123',
32+
redirectUri: 'https://app.com/callback',
33+
connection: 'conn_123',
34+
});
35+
36+
expect(url).toContain('connection=conn_123');
37+
});
38+
39+
it('includes state parameter', () => {
40+
const url = getAuthorizationUrl({
41+
clientId: 'client_123',
42+
redirectUri: 'https://app.com/callback',
43+
provider: 'GoogleOAuth',
44+
state: 'custom state',
45+
});
46+
47+
expect(url).toContain('state=custom+state');
48+
});
49+
50+
it('includes domainHint and loginHint', () => {
51+
const url = getAuthorizationUrl({
52+
clientId: 'client_123',
53+
redirectUri: 'https://app.com/callback',
54+
provider: 'GoogleOAuth',
55+
domainHint: 'example.com',
56+
loginHint: '[email protected]',
57+
});
58+
59+
expect(url).toContain('domain_hint=example.com');
60+
expect(url).toContain('login_hint=user%40example.com');
61+
});
62+
63+
it('includes provider scopes', () => {
64+
const url = getAuthorizationUrl({
65+
clientId: 'client_123',
66+
redirectUri: 'https://app.com/callback',
67+
provider: 'GoogleOAuth',
68+
providerScopes: ['read_api', 'read_repository'],
69+
});
70+
71+
expect(url).toContain('provider_scopes=read_api');
72+
expect(url).toContain('provider_scopes=read_repository');
73+
});
74+
75+
it('includes provider query params', () => {
76+
const url = getAuthorizationUrl({
77+
clientId: 'client_123',
78+
redirectUri: 'https://app.com/callback',
79+
provider: 'GoogleOAuth',
80+
providerQueryParams: {
81+
foo: 'bar',
82+
baz: 123,
83+
bool: true,
84+
},
85+
});
86+
87+
expect(url).toContain('provider_query_params%5Bfoo%5D=bar');
88+
expect(url).toContain('provider_query_params%5Bbaz%5D=123');
89+
expect(url).toContain('provider_query_params%5Bbool%5D=true');
90+
});
91+
92+
it('uses custom baseURL when provided', () => {
93+
const url = getAuthorizationUrl({
94+
clientId: 'client_123',
95+
redirectUri: 'https://app.com/callback',
96+
provider: 'GoogleOAuth',
97+
baseURL: 'https://api.workos.dev',
98+
});
99+
100+
expect(url).toContain('https://api.workos.dev/sso/authorize');
101+
});
102+
103+
it('throws error when no provider, connection, or organization specified', () => {
104+
expect(() => {
105+
// @ts-expect-error Testing runtime validation with invalid input
106+
getAuthorizationUrl({
107+
clientId: 'client_123',
108+
redirectUri: 'https://app.com/callback',
109+
});
110+
}).toThrow(
111+
"Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'.",
112+
);
113+
});
114+
});
115+
});

src/client/sso.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { toQueryString } from './utils';
2+
import type { SSOAuthorizationURLOptions as BaseSSOAuthorizationURLOptions } from '../sso/interfaces';
3+
4+
// Extend the base options to include baseURL for internal use
5+
export type SSOAuthorizationURLOptions = BaseSSOAuthorizationURLOptions & {
6+
baseURL?: string;
7+
};
8+
9+
/**
10+
* Generates the authorization URL for SSO authentication.
11+
* Does not require an API key, suitable for OAuth client operations.
12+
*
13+
* @param options - SSO authorization URL options
14+
* @returns The authorization URL as a string
15+
* @throws Error if required arguments are missing
16+
*/
17+
export function getAuthorizationUrl(
18+
options: SSOAuthorizationURLOptions,
19+
): string {
20+
const {
21+
connection,
22+
clientId,
23+
domainHint,
24+
loginHint,
25+
organization,
26+
provider,
27+
providerQueryParams,
28+
providerScopes,
29+
redirectUri,
30+
state,
31+
baseURL = 'https://api.workos.com',
32+
} = options;
33+
34+
if (!provider && !connection && !organization) {
35+
throw new Error(
36+
`Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'.`,
37+
);
38+
}
39+
40+
const query = toQueryString({
41+
connection,
42+
organization,
43+
domain_hint: domainHint,
44+
login_hint: loginHint,
45+
provider,
46+
provider_query_params: providerQueryParams,
47+
provider_scopes: providerScopes,
48+
client_id: clientId,
49+
redirect_uri: redirectUri,
50+
response_type: 'code',
51+
state,
52+
});
53+
54+
return `${baseURL}/sso/authorize?${query}`;
55+
}

0 commit comments

Comments
 (0)