Skip to content

Commit dddca9a

Browse files
authored
[v8] feat: add PKCE support for public clients; remove /client entry point (#1435)
## Description Enable PKCE authentication for both public and confidential clients. ### Changes - **API key now optional**: Initialize with just `clientId` for PKCE mode: `new WorkOS({ clientId: 'client_...' })` - **New helper method**: `getAuthorizationUrlWithPKCE()` - generates PKCE internally, returns `{ url, state, codeVerifier }` - **Enhanced exchange**: `authenticateWithCode()` auto-detects client mode based on available credentials - **Manual PKCE option**: `workos.pkce.generate()` + `getAuthorizationUrl()` for advanced use cases - **Non-breaking**: Existing `getAuthorizationUrl()` unchanged, still returns URL string ### PKCE with Confidential Clients (OAuth 2.1 Best Practice) Server-side apps can use PKCE alongside the client secret for defense in depth: ```ts const workos = new WorkOS('sk_...'); // With API key const { url, codeVerifier } = await workos.userManagement.getAuthorizationUrlWithPKCE({ provider: 'authkit', redirectUri: 'https://example.com/callback', clientId: 'client_...', }); // Both client_secret AND code_verifier will be sent const { accessToken } = await workos.userManagement.authenticateWithCode({ code: authorizationCode, codeVerifier, clientId: 'client_...', }); ``` The auto-detection logic: | API Key | codeVerifier | Behavior | |---------|--------------|----------| | ✅ | ✅ | Send both `client_secret` AND `code_verifier` (confidential + PKCE) | | ✅ | ❌ | Send `client_secret` only (traditional confidential client) | | ❌ | ✅ | Send `code_verifier` only (public client) | | ❌ | ❌ | Error | ### Removed: `@workos-inc/node/client` export The separate `/client` entry point has been removed. Instead of: ```ts // Old approach - standalone functions import { userManagement } from '@workos-inc/node/client'; const url = userManagement.getAuthorizationUrl({ ... }); ``` Use the standard SDK without an API key: ```ts // New approach - consistent with rest of SDK import { WorkOS } from '@workos-inc/node'; const workos = new WorkOS({ clientId: 'client_...' }); const url = workos.userManagement.getAuthorizationUrl({ ... }); ``` This provides a single, consistent API surface rather than two parallel approaches. ### Public Client Usage ```ts import { WorkOS } from '@workos-inc/node'; const workos = new WorkOS({ clientId: 'client_...' }); // Step 1: Get authorization URL with auto-generated PKCE const { url, state, codeVerifier } = await workos.userManagement.getAuthorizationUrlWithPKCE({ redirectUri: 'myapp://callback', provider: 'authkit', }); // Store codeVerifier securely, then redirect user to url // Step 2: Exchange code for tokens const { accessToken, refreshToken, user } = await workos.userManagement.authenticateWithCode({ code: authCode, codeVerifier, }); ``` ### Methods Available Without API Key | Method | Description | |--------|-------------| | `userManagement.getAuthorizationUrlWithPKCE()` | Build OAuth URL with auto-generated PKCE | | `userManagement.getAuthorizationUrl()` | Build OAuth URL (with manual PKCE params) | | `userManagement.authenticateWithCode()` | Exchange code + verifier for tokens | | `userManagement.authenticateWithCodeAndVerifier()` | Exchange code + verifier for tokens (explicit) | | `userManagement.authenticateWithRefreshToken()` | Refresh tokens | | `userManagement.getLogoutUrl()` | Build logout redirect URL | | `userManagement.getJwksUrl()` | Get JWKS URL for local JWT validation | | `workos.pkce.generate()` | Generate PKCE code verifier and challenge |
1 parent 4e90607 commit dddca9a

File tree

49 files changed

+1838
-820
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1838
-820
lines changed

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,60 @@ import { WorkOS } from '@workos-inc/node';
3737
const workos = new WorkOS('sk_1234');
3838
```
3939

40+
## Public Client Mode (Browser/Mobile/CLI)
41+
42+
For apps that can't securely store secrets, initialize with just a client ID:
43+
44+
```ts
45+
import { WorkOS } from '@workos-inc/node';
46+
47+
const workos = new WorkOS({ clientId: 'client_...' }); // No API key needed
48+
49+
// Generate auth URL with automatic PKCE
50+
const { url, codeVerifier } =
51+
await workos.userManagement.getAuthorizationUrlWithPKCE({
52+
provider: 'authkit',
53+
redirectUri: 'myapp://callback',
54+
clientId: 'client_...',
55+
});
56+
57+
// After user authenticates, exchange code for tokens
58+
const { accessToken, refreshToken } =
59+
await workos.userManagement.authenticateWithCode({
60+
code: authorizationCode,
61+
codeVerifier,
62+
clientId: 'client_...',
63+
});
64+
```
65+
66+
> [!IMPORTANT]
67+
> Store `codeVerifier` securely on-device between generating the auth URL and handling the callback. For mobile apps, use platform secure storage (iOS Keychain, Android Keystore). For CLI apps, consider OS credential storage. The verifier must survive app restarts during the auth flow.
68+
69+
See the [AuthKit documentation](https://workos.com/docs/authkit) for details on PKCE authentication.
70+
71+
### PKCE with Confidential Clients
72+
73+
Server-side apps can also use PKCE alongside the client secret for defense in depth (recommended by OAuth 2.1):
74+
75+
```ts
76+
const workos = new WorkOS('sk_...'); // With API key
77+
78+
// Use PKCE even with API key for additional security
79+
const { url, codeVerifier } =
80+
await workos.userManagement.getAuthorizationUrlWithPKCE({
81+
provider: 'authkit',
82+
redirectUri: 'https://example.com/callback',
83+
clientId: 'client_...',
84+
});
85+
86+
// Both client_secret AND code_verifier will be sent
87+
const { accessToken } = await workos.userManagement.authenticateWithCode({
88+
code: authorizationCode,
89+
codeVerifier,
90+
clientId: 'client_...',
91+
});
92+
```
93+
4094
## SDK Versioning
4195

4296
For our SDKs WorkOS follows a Semantic Versioning ([SemVer](https://semver.org/)) process where all releases will have a version X.Y.Z (like 1.0.0) pattern wherein Z would be a bug fix (e.g., 1.0.1), Y would be a minor release (1.1.0) and X would be a major release (2.0.0). We permit any breaking changes to only be released in major versions and strongly recommend reading changelogs before making any major version upgrades.

package-lock.json

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@types/qs": "^6.14.0",
5858
"@typescript-eslint/parser": "^8.46.0",
5959
"babel-jest": "^30.2.0",
60+
"baseline-browser-mapping": "^2.9.11",
6061
"eslint": "^9.37.0",
6162
"eslint-plugin-jest": "^29.0.1",
6263
"eslint-plugin-n": "^17.23.1",
@@ -110,17 +111,6 @@
110111
},
111112
"default": "./lib/index.js"
112113
},
113-
"./client": {
114-
"import": {
115-
"types": "./lib/index.client.d.ts",
116-
"default": "./lib/index.client.js"
117-
},
118-
"require": {
119-
"types": "./lib/index.client.d.cts",
120-
"default": "./lib/index.client.cjs"
121-
},
122-
"default": "./lib/index.client.js"
123-
},
124114
"./worker": {
125115
"import": {
126116
"types": "./lib/index.worker.d.ts",

src/client/index.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/client/sso.spec.ts

Lines changed: 0 additions & 115 deletions
This file was deleted.

src/client/sso.ts

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)