Skip to content

Commit 8d3f693

Browse files
committed
fix(pkce): improve validation and type safety
- Add empty codeVerifier validation to prevent silent bypass of PKCE flow - Add PKCE field coupling types (codeChallenge + codeChallengeMethod must be set together) - Standardize error types to TypeError for argument validation - Add tests for empty/whitespace codeVerifier edge cases
1 parent a6ede43 commit 8d3f693

File tree

6 files changed

+110
-34
lines changed

6 files changed

+110
-34
lines changed

src/sso/interfaces/authorization-url-options.interface.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
interface SSOAuthorizationURLBase {
1+
/**
2+
* PKCE fields must be provided together or not at all.
3+
* Use workos.pkce.generate() to create a valid pair.
4+
*/
5+
type PKCEFields =
6+
| { codeChallenge?: never; codeChallengeMethod?: never }
7+
| { codeChallenge: string; codeChallengeMethod: 'S256' };
8+
9+
interface SSOAuthorizationURLBaseFields {
210
clientId: string;
3-
/**
4-
* PKCE code challenge for public clients.
5-
* Generate using workos.pkce.generate() and pass the codeChallenge here.
6-
*/
7-
codeChallenge?: string;
8-
/** PKCE code challenge method. Use 'S256' (recommended). */
9-
codeChallengeMethod?: 'S256';
1011
domainHint?: string;
1112
loginHint?: string;
1213
providerQueryParams?: Record<string, string | boolean | number>;
@@ -31,23 +32,26 @@ export interface SSOPKCEAuthorizationURLResult {
3132
codeVerifier: string;
3233
}
3334

34-
interface SSOWithConnection extends SSOAuthorizationURLBase {
35-
connection: string;
36-
organization?: never;
37-
provider?: never;
38-
}
35+
type SSOWithConnection = SSOAuthorizationURLBaseFields &
36+
PKCEFields & {
37+
connection: string;
38+
organization?: never;
39+
provider?: never;
40+
};
3941

40-
interface SSOWithOrganization extends SSOAuthorizationURLBase {
41-
organization: string;
42-
connection?: never;
43-
provider?: never;
44-
}
42+
type SSOWithOrganization = SSOAuthorizationURLBaseFields &
43+
PKCEFields & {
44+
organization: string;
45+
connection?: never;
46+
provider?: never;
47+
};
4548

46-
interface SSOWithProvider extends SSOAuthorizationURLBase {
47-
provider: string;
48-
connection?: never;
49-
organization?: never;
50-
}
49+
type SSOWithProvider = SSOAuthorizationURLBaseFields &
50+
PKCEFields & {
51+
provider: string;
52+
connection?: never;
53+
organization?: never;
54+
};
5155

5256
export type SSOAuthorizationURLOptions =
5357
| SSOWithConnection

src/sso/sso.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,32 @@ describe('SSO', () => {
620620
'or an API key configured on the WorkOS instance (for confidential clients).',
621621
);
622622
});
623+
624+
it('throws error when codeVerifier is an empty string', async () => {
625+
await expect(
626+
publicWorkos.sso.getProfileAndToken({
627+
code: 'authorization_code',
628+
clientId: 'proj_123',
629+
codeVerifier: '',
630+
}),
631+
).rejects.toThrow(
632+
'codeVerifier cannot be an empty string. ' +
633+
'Generate a valid PKCE pair using workos.pkce.generate().',
634+
);
635+
});
636+
637+
it('throws error when codeVerifier is whitespace only', async () => {
638+
await expect(
639+
publicWorkos.sso.getProfileAndToken({
640+
code: 'authorization_code',
641+
clientId: 'proj_123',
642+
codeVerifier: ' ',
643+
}),
644+
).rejects.toThrow(
645+
'codeVerifier cannot be an empty string. ' +
646+
'Generate a valid PKCE pair using workos.pkce.generate().',
647+
);
648+
});
623649
});
624650
});
625651
});

src/sso/sso.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class SSO {
6868
} = options;
6969

7070
if (!provider && !connection && !organization) {
71-
throw new Error(
71+
throw new TypeError(
7272
`Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'.`,
7373
);
7474
}
@@ -193,11 +193,19 @@ export class SSO {
193193
}: GetProfileAndTokenOptions): Promise<
194194
ProfileAndToken<CustomAttributesType>
195195
> {
196+
// Validate codeVerifier is not an empty string (common mistake)
197+
if (codeVerifier !== undefined && codeVerifier.trim() === '') {
198+
throw new TypeError(
199+
'codeVerifier cannot be an empty string. ' +
200+
'Generate a valid PKCE pair using workos.pkce.generate().',
201+
);
202+
}
203+
196204
const usePublicClientFlow = !!codeVerifier;
197205
const hasApiKey = !!this.workos.key;
198206

199207
if (!usePublicClientFlow && !hasApiKey) {
200-
throw new Error(
208+
throw new TypeError(
201209
'getProfileAndToken requires either a codeVerifier (for public clients) ' +
202210
'or an API key configured on the WorkOS instance (for confidential clients).',
203211
);

src/user-management/interfaces/authorization-url-options.interface.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
export interface UserManagementAuthorizationURLOptions {
1+
/**
2+
* PKCE fields must be provided together or not at all.
3+
* Use workos.pkce.generate() to create a valid pair.
4+
*/
5+
type PKCEFields =
6+
| { codeChallenge?: never; codeChallengeMethod?: never }
7+
| { codeChallenge: string; codeChallengeMethod: 'S256' };
8+
9+
interface UserManagementAuthorizationURLBaseOptions {
210
clientId: string;
3-
/**
4-
* PKCE code challenge for public clients.
5-
* Generate using workos.pkce.generate() and pass the codeChallenge here.
6-
*/
7-
codeChallenge?: string;
8-
/** PKCE code challenge method. Use 'S256' (recommended). */
9-
codeChallengeMethod?: 'S256';
1011
connectionId?: string;
1112
organizationId?: string;
1213
domainHint?: string;
@@ -20,6 +21,9 @@ export interface UserManagementAuthorizationURLOptions {
2021
screenHint?: 'sign-up' | 'sign-in';
2122
}
2223

24+
export type UserManagementAuthorizationURLOptions =
25+
UserManagementAuthorizationURLBaseOptions & PKCEFields;
26+
2327
/**
2428
* Result of getAuthorizationUrlWithPKCE() containing the URL,
2529
* state, and PKCE code verifier.

src/user-management/user-management.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,32 @@ describe('UserManagement', () => {
382382
'or an API key configured on the WorkOS instance (for confidential clients).',
383383
);
384384
});
385+
386+
it('throws error when codeVerifier is an empty string', async () => {
387+
await expect(
388+
publicWorkos.userManagement.authenticateWithCode({
389+
clientId: 'proj_whatever',
390+
code: 'some_code',
391+
codeVerifier: '',
392+
}),
393+
).rejects.toThrow(
394+
'codeVerifier cannot be an empty string. ' +
395+
'Generate a valid PKCE pair using workos.pkce.generate().',
396+
);
397+
});
398+
399+
it('throws error when codeVerifier is whitespace only', async () => {
400+
await expect(
401+
publicWorkos.userManagement.authenticateWithCode({
402+
clientId: 'proj_whatever',
403+
code: 'some_code',
404+
codeVerifier: ' ',
405+
}),
406+
).rejects.toThrow(
407+
'codeVerifier cannot be an empty string. ' +
408+
'Generate a valid PKCE pair using workos.pkce.generate().',
409+
);
410+
});
385411
});
386412

387413
it('deserializes authentication_method', async () => {

src/user-management/user-management.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,19 @@ export class UserManagement {
306306
): Promise<AuthenticationResponse> {
307307
const { session, codeVerifier, ...remainingPayload } = payload;
308308

309+
// Validate codeVerifier is not an empty string (common mistake)
310+
if (codeVerifier !== undefined && codeVerifier.trim() === '') {
311+
throw new TypeError(
312+
'codeVerifier cannot be an empty string. ' +
313+
'Generate a valid PKCE pair using workos.pkce.generate().',
314+
);
315+
}
316+
309317
const usePublicClientFlow = !!codeVerifier;
310318
const hasApiKey = !!this.workos.key;
311319

312320
if (!usePublicClientFlow && !hasApiKey) {
313-
throw new Error(
321+
throw new TypeError(
314322
'authenticateWithCode requires either a codeVerifier (for public clients) ' +
315323
'or an API key configured on the WorkOS instance (for confidential clients).',
316324
);

0 commit comments

Comments
 (0)