Skip to content

Commit 52b93c7

Browse files
authored
Add authenticateWithCodeAndVerifier method for strict PKCE enforcement (#1297)
1 parent 2823968 commit 52b93c7

File tree

7 files changed

+164
-0
lines changed

7 files changed

+164
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {
2+
AuthenticateWithOptionsBase,
3+
SerializedAuthenticateWithPKCEBase,
4+
} from './authenticate-with-options-base.interface';
5+
6+
export interface AuthenticateWithCodeAndVerifierOptions
7+
extends AuthenticateWithOptionsBase {
8+
codeVerifier: string;
9+
code: string;
10+
invitationToken?: string;
11+
}
12+
13+
export interface SerializedAuthenticateWithCodeAndVerifierOptions
14+
extends SerializedAuthenticateWithPKCEBase {
15+
grant_type: 'authorization_code';
16+
code_verifier: string;
17+
code: string;
18+
invitation_token?: string;
19+
}

src/user-management/interfaces/authenticate-with-options-base.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ export interface SerializedAuthenticateWithOptionsBase {
1616
ip_address?: string;
1717
user_agent?: string;
1818
}
19+
20+
export interface SerializedAuthenticateWithPKCEBase {
21+
client_id: string;
22+
ip_address?: string;
23+
user_agent?: string;
24+
}

src/user-management/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './authenticate-with-code-options.interface';
2+
export * from './authenticate-with-code-and-verifier-options.interface';
23
export * from './authenticate-with-email-verification-options.interface';
34
export * from './authenticate-with-magic-auth-options.interface';
45
export * from './authenticate-with-options-base.interface';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
AuthenticateWithCodeAndVerifierOptions,
3+
SerializedAuthenticateWithCodeAndVerifierOptions,
4+
} from '../interfaces';
5+
6+
export const serializeAuthenticateWithCodeAndVerifierOptions = (
7+
options: AuthenticateWithCodeAndVerifierOptions,
8+
): SerializedAuthenticateWithCodeAndVerifierOptions => ({
9+
grant_type: 'authorization_code',
10+
client_id: options.clientId,
11+
code: options.code,
12+
code_verifier: options.codeVerifier,
13+
invitation_token: options.invitationToken,
14+
ip_address: options.ipAddress,
15+
user_agent: options.userAgent,
16+
});

src/user-management/serializers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './authenticate-with-code-options.serializer';
2+
export * from './authenticate-with-code-and-verifier-options.serializer';
23
export * from './authenticate-with-magic-auth-options.serializer';
34
export * from './authenticate-with-password-options.serializer';
45
export * from './authenticate-with-refresh-token.options.serializer';

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,105 @@ describe('UserManagement', () => {
465465
});
466466
});
467467

468+
describe('authenticateWithCodeAndVerifier', () => {
469+
it('sends a token authentication request with required code_verifier', async () => {
470+
fetchOnce({ user: userFixture });
471+
const resp = await workos.userManagement.authenticateWithCodeAndVerifier({
472+
clientId: 'proj_whatever',
473+
code: 'auth_code_123',
474+
codeVerifier: 'required_code_verifier',
475+
});
476+
477+
expect(fetchURL()).toContain('/user_management/authenticate');
478+
expect(fetchBody()).toEqual({
479+
client_id: 'proj_whatever',
480+
code: 'auth_code_123',
481+
code_verifier: 'required_code_verifier',
482+
grant_type: 'authorization_code',
483+
});
484+
485+
expect(resp).toMatchObject({
486+
user: {
487+
488+
},
489+
});
490+
});
491+
492+
it('sends a token authentication request with invitation token', async () => {
493+
fetchOnce({ user: userFixture });
494+
const resp = await workos.userManagement.authenticateWithCodeAndVerifier({
495+
clientId: 'proj_whatever',
496+
code: 'auth_code_123',
497+
codeVerifier: 'required_code_verifier',
498+
invitationToken: 'invitation_123',
499+
});
500+
501+
expect(fetchURL()).toContain('/user_management/authenticate');
502+
expect(fetchBody()).toEqual({
503+
client_id: 'proj_whatever',
504+
code: 'auth_code_123',
505+
code_verifier: 'required_code_verifier',
506+
invitation_token: 'invitation_123',
507+
grant_type: 'authorization_code',
508+
});
509+
510+
expect(resp).toMatchObject({
511+
user: {
512+
513+
},
514+
});
515+
});
516+
517+
describe('when sealSession = true', () => {
518+
beforeEach(() => {
519+
fetchOnce({
520+
user: userFixture,
521+
access_token:
522+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJpYXQiOiAxNTE2MjM5MDIyLAogICJzaWQiOiAic2Vzc2lvbl8xMjMiLAogICJvcmdfaWQiOiAib3JnXzEyMyIsCiAgInJvbGUiOiAibWVtYmVyIiwKICAicGVybWlzc2lvbnMiOiBbInBvc3RzOmNyZWF0ZSIsICJwb3N0czpkZWxldGUiXQp9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
523+
});
524+
});
525+
526+
describe('when the cookie password is undefined', () => {
527+
it('throws an error', async () => {
528+
await expect(
529+
workos.userManagement.authenticateWithCodeAndVerifier({
530+
clientId: 'proj_whatever',
531+
code: 'auth_code_123',
532+
codeVerifier: 'required_code_verifier',
533+
session: { sealSession: true },
534+
}),
535+
).rejects.toThrow('Cookie password is required');
536+
});
537+
});
538+
539+
describe('when successfully authenticated', () => {
540+
it('returns the sealed session data', async () => {
541+
const cookiePassword = 'alongcookiesecretmadefortestingsessions';
542+
543+
const response =
544+
await workos.userManagement.authenticateWithCodeAndVerifier({
545+
clientId: 'proj_whatever',
546+
code: 'auth_code_123',
547+
codeVerifier: 'required_code_verifier',
548+
session: { sealSession: true, cookiePassword },
549+
});
550+
551+
expect(response).toEqual({
552+
sealedSession: expect.any(String),
553+
accessToken: expect.any(String),
554+
authenticationMethod: undefined,
555+
impersonator: undefined,
556+
organizationId: undefined,
557+
refreshToken: undefined,
558+
user: expect.objectContaining({
559+
560+
}),
561+
});
562+
});
563+
});
564+
});
565+
});
566+
468567
describe('authenticateWithRefreshToken', () => {
469568
it('sends a refresh_token authentication request', async () => {
470569
fetchOnce({

src/user-management/user-management.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { deserializeChallenge } from '../mfa/serializers';
88
import { WorkOS } from '../workos';
99
import {
1010
AuthenticateWithCodeOptions,
11+
AuthenticateWithCodeAndVerifierOptions,
1112
AuthenticateWithMagicAuthOptions,
1213
AuthenticateWithPasswordOptions,
1314
AuthenticateWithRefreshTokenOptions,
@@ -32,6 +33,7 @@ import {
3233
SendPasswordResetEmailOptions,
3334
SendVerificationEmailOptions,
3435
SerializedAuthenticateWithCodeOptions,
36+
SerializedAuthenticateWithCodeAndVerifierOptions,
3537
SerializedAuthenticateWithMagicAuthOptions,
3638
SerializedAuthenticateWithPasswordOptions,
3739
SerializedAuthenticateWithRefreshTokenOptions,
@@ -112,6 +114,7 @@ import {
112114
deserializePasswordReset,
113115
deserializeUser,
114116
serializeAuthenticateWithCodeOptions,
117+
serializeAuthenticateWithCodeAndVerifierOptions,
115118
serializeAuthenticateWithMagicAuthOptions,
116119
serializeAuthenticateWithPasswordOptions,
117120
serializeAuthenticateWithRefreshTokenOptions,
@@ -315,6 +318,25 @@ export class UserManagement {
315318
});
316319
}
317320

321+
async authenticateWithCodeAndVerifier(
322+
payload: AuthenticateWithCodeAndVerifierOptions,
323+
): Promise<AuthenticationResponse> {
324+
const { session, ...remainingPayload } = payload;
325+
326+
const { data } = await this.workos.post<
327+
AuthenticationResponseResponse,
328+
SerializedAuthenticateWithCodeAndVerifierOptions
329+
>(
330+
'/user_management/authenticate',
331+
serializeAuthenticateWithCodeAndVerifierOptions(remainingPayload),
332+
);
333+
334+
return this.prepareAuthenticationResponse({
335+
authenticationResponse: deserializeAuthenticationResponse(data),
336+
session,
337+
});
338+
}
339+
318340
async authenticateWithRefreshToken(
319341
payload: AuthenticateWithRefreshTokenOptions,
320342
): Promise<AuthenticationResponse> {

0 commit comments

Comments
 (0)