@memberjunction/auth-providers 5.43.0 → 5.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +38 -0
- package/README.md +17 -1
- package/WORKOS.md +256 -0
- package/dist/AuthProviderFactory.d.ts +1 -0
- package/dist/AuthProviderFactory.d.ts.map +1 -1
- package/dist/AuthProviderFactory.js +1 -0
- package/dist/AuthProviderFactory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/HostIdentityProvider.d.ts +59 -0
- package/dist/providers/HostIdentityProvider.d.ts.map +1 -0
- package/dist/providers/HostIdentityProvider.js +118 -0
- package/dist/providers/HostIdentityProvider.js.map +1 -0
- package/dist/providers/WorkOSProvider.d.ts +48 -0
- package/dist/providers/WorkOSProvider.d.ts.map +1 -0
- package/dist/providers/WorkOSProvider.js +84 -0
- package/dist/providers/WorkOSProvider.js.map +1 -0
- package/package.json +3 -3
- package/src/AuthProviderFactory.ts +1 -0
- package/src/__tests__/WorkOSProvider.test.ts +169 -0
- package/src/index.ts +5 -0
- package/src/providers/HostIdentityProvider.ts +125 -0
- package/src/providers/WorkOSProvider.ts +78 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @memberjunction/auth-providers
|
|
2
2
|
|
|
3
|
+
## 5.45.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [45d121b]
|
|
8
|
+
- Updated dependencies [21e33fe]
|
|
9
|
+
- Updated dependencies [b7cf50f]
|
|
10
|
+
- Updated dependencies [f4f11fa]
|
|
11
|
+
- Updated dependencies [e370816]
|
|
12
|
+
- Updated dependencies [fbee64c]
|
|
13
|
+
- Updated dependencies [b2927f1]
|
|
14
|
+
- Updated dependencies [c1f2d3d]
|
|
15
|
+
- Updated dependencies [0b1e009]
|
|
16
|
+
- @memberjunction/core@5.45.0
|
|
17
|
+
- @memberjunction/global@5.45.0
|
|
18
|
+
|
|
19
|
+
## 5.44.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 2f9b863: Add WorkOS (AuthKit) as a first-class authentication provider — end to end, server-side JWT validation and browser-side login. A deployment can now set `type: 'workos'` (server) / `AUTH_TYPE: 'workos'` (browser) and authenticate users through WorkOS just like Auth0, Okta, MSAL, Cognito, or Google.
|
|
24
|
+
- **Server** (`@memberjunction/auth-providers`): `WorkOSProvider` extends `BaseAuthProvider`, registered via `@RegisterClass(BaseAuthProvider, 'workos')`. Maps AuthKit JWT claims to `AuthUserInfo` (with graceful fallbacks) and validates `clientId`; issuer matching, JWKS caching, and retry/backoff are inherited. Wired into `AuthProviderFactory`.
|
|
25
|
+
- **Client** (`@memberjunction/ng-auth-services`): `MJWorkOSProvider` extends `MJAuthBase`, registered via `@RegisterClass(MJAuthBase, 'workos')`. Wraps the `@workos-inc/authkit-js` SDK (`createClient`/`signIn`/`signOut`/`getUser`/`getAccessToken`) behind the standardized provider contract with semantic error classification.
|
|
26
|
+
- **Core** (`@memberjunction/core`): `AUTH_PROVIDER_TYPES` gains `WORKOS: 'workos'`.
|
|
27
|
+
- **Env typing** (`@memberjunction/ng-bootstrap`): the `AUTH_TYPE` union gains `'workos'`, plus `WORKOS_CLIENTID` / `WORKOS_REDIRECT_URI` / `WORKOS_API_HOSTNAME` / `WORKOS_DEV_MODE` keys.
|
|
28
|
+
|
|
29
|
+
Includes a full end-to-end integration guide (`packages/AuthProviders/WORKOS.md`) covering the two WorkOS-specific gotchas: the required `email` JWT Template (AuthKit access tokens omit email, which MJ keys users on) and matching the enforced `aud` claim. Additive only.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [5396d90]
|
|
34
|
+
- Updated dependencies [7279819]
|
|
35
|
+
- Updated dependencies [d44e430]
|
|
36
|
+
- Updated dependencies [6f74b17]
|
|
37
|
+
- Updated dependencies [2f9b863]
|
|
38
|
+
- @memberjunction/core@5.44.0
|
|
39
|
+
- @memberjunction/global@5.44.0
|
|
40
|
+
|
|
3
41
|
## 5.43.0
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@ This package gives the MJ server (and any other Node.js consumer) a uniform, plu
|
|
|
9
9
|
- Extract a normalized `AuthUserInfo` from provider-specific claim shapes
|
|
10
10
|
- Register additional providers at runtime via the MJ class-factory system
|
|
11
11
|
|
|
12
|
-
It ships with first-class support for **Auth0**, **Microsoft Entra ID / MSAL**, **Okta**, **AWS Cognito**,
|
|
12
|
+
It ships with first-class support for **Auth0**, **Microsoft Entra ID / MSAL**, **Okta**, **AWS Cognito**, **Google Identity Platform**, and **WorkOS (AuthKit)**, and is the extension point used to plug custom providers into [`@memberjunction/server`](../MJServer/README.md).
|
|
13
|
+
|
|
14
|
+
> **Integrating WorkOS?** See the dedicated end-to-end guide: **[WORKOS.md](WORKOS.md)** — it covers the browser + server setup and the two WorkOS-specific gotchas (the required `email` JWT Template and matching the `aud` claim).
|
|
13
15
|
|
|
14
16
|
## When to use this package
|
|
15
17
|
|
|
@@ -67,6 +69,7 @@ This package is a Node.js / server-side package. It depends on:
|
|
|
67
69
|
│ ├─ @RegisterClass(BaseAuthProvider, 'okta') │
|
|
68
70
|
│ ├─ @RegisterClass(BaseAuthProvider, 'cognito') │
|
|
69
71
|
│ ├─ @RegisterClass(BaseAuthProvider, 'google') │
|
|
72
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'workos') │
|
|
70
73
|
│ └─ @RegisterClass(BaseAuthProvider, 'your-custom') │
|
|
71
74
|
└──────────────────────────────────────────────────────────────────┘
|
|
72
75
|
```
|
|
@@ -95,6 +98,7 @@ Each built-in provider is registered with the MJ class factory under a lowercase
|
|
|
95
98
|
| `okta` | `OktaProvider` | `clientId`, `domain` |
|
|
96
99
|
| `cognito` | `CognitoProvider` | `clientId`, `region`, `userPoolId` |
|
|
97
100
|
| `google` | `GoogleProvider` | `clientId` |
|
|
101
|
+
| `workos` | `WorkOSProvider` | `clientId` (see [WORKOS.md](WORKOS.md) for the required `email` JWT Template + `aud`) |
|
|
98
102
|
|
|
99
103
|
Every provider also requires the base fields: `name`, `type`, `issuer`, `audience`, `jwksUri`. See [`AuthProviderConfig`](../MJCore/src/generic/authTypes.ts) for the full shape.
|
|
100
104
|
|
|
@@ -124,11 +128,23 @@ module.exports = {
|
|
|
124
128
|
audience: 'https://api.example.com',
|
|
125
129
|
jwksUri: 'https://tenant.auth0.com/.well-known/jwks.json',
|
|
126
130
|
},
|
|
131
|
+
{
|
|
132
|
+
name: 'workos-prod',
|
|
133
|
+
type: 'workos',
|
|
134
|
+
clientId: process.env.WORKOS_CLIENT_ID, // client_01H...
|
|
135
|
+
issuer: `https://api.workos.com/user_management/${process.env.WORKOS_CLIENT_ID}`,
|
|
136
|
+
jwksUri: `https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
|
|
137
|
+
audience: process.env.WORKOS_CLIENT_ID, // must match the token's `aud` — see WORKOS.md
|
|
138
|
+
},
|
|
127
139
|
// ...add more providers here
|
|
128
140
|
],
|
|
129
141
|
};
|
|
130
142
|
```
|
|
131
143
|
|
|
144
|
+
> **WorkOS needs two extra steps** beyond this config — an `email` JWT Template (its access tokens
|
|
145
|
+
> omit email, which MJ keys users on) and matching the `aud` claim. The full walkthrough is in
|
|
146
|
+
> **[WORKOS.md](WORKOS.md)**.
|
|
147
|
+
|
|
132
148
|
> **Multiple audiences on the same issuer.** When two MJ apps share an Auth0 domain but use different client IDs, register both as separate entries — `AuthProviderFactory.getAllByIssuer()` returns every match so the validator can try each audience.
|
|
133
149
|
|
|
134
150
|
## Usage
|
package/WORKOS.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# WorkOS (AuthKit) Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide walks you through wiring **WorkOS AuthKit** into MemberJunction end to end —
|
|
4
|
+
browser login and server-side token validation — so users sign in through WorkOS exactly like
|
|
5
|
+
they would through Auth0, Okta, MSAL, Cognito, or Google.
|
|
6
|
+
|
|
7
|
+
WorkOS is consumed as an OIDC/JWT identity provider. MemberJunction does **not** replace WorkOS;
|
|
8
|
+
WorkOS handles authentication (hosted login, SSO, MFA, Directory Sync) and MJ handles everything
|
|
9
|
+
after the user is identified (entities, permissions, agents, audit).
|
|
10
|
+
|
|
11
|
+
> **TL;DR — the one thing that trips everyone up:** a WorkOS AuthKit **access token does not
|
|
12
|
+
> include the user's email by default**, and MemberJunction resolves users by email. You must add
|
|
13
|
+
> an `email` claim with a [WorkOS **JWT Template**](#step-3--add-an-email-claim-jwt-template).
|
|
14
|
+
> Skip this and every login will fail user lookup on the server. See
|
|
15
|
+
> [Step 3](#step-3--add-an-email-claim-jwt-template).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
1. [How it fits together](#how-it-fits-together)
|
|
22
|
+
2. [What's in this package](#whats-in-this-package)
|
|
23
|
+
3. [Prerequisites](#prerequisites)
|
|
24
|
+
4. [Step 1 — Configure the WorkOS dashboard](#step-1--configure-the-workos-dashboard)
|
|
25
|
+
5. [Step 2 — Configure the MJ server (`mj.config.cjs`)](#step-2--configure-the-mj-server-mjconfigcjs)
|
|
26
|
+
6. [Step 3 — Add an `email` claim (JWT Template)](#step-3--add-an-email-claim-jwt-template)
|
|
27
|
+
7. [Step 4 — Configure the browser (MJExplorer)](#step-4--configure-the-browser-mjexplorer)
|
|
28
|
+
8. [Token claims reference](#token-claims-reference)
|
|
29
|
+
9. [How validation works under the hood](#how-validation-works-under-the-hood)
|
|
30
|
+
10. [Troubleshooting](#troubleshooting)
|
|
31
|
+
11. [Scope & non-goals](#scope--non-goals)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## How it fits together
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─ Browser (MJExplorer) ─────────────────┐ ┌─ MJ GraphQL Server ─────────────────────┐
|
|
39
|
+
│ │ │ │
|
|
40
|
+
│ MJWorkOSProvider (extends MJAuthBase) │ │ WorkOSProvider (extends BaseAuthProvider)│
|
|
41
|
+
│ @RegisterClass(MJAuthBase,'workos') │ │ @RegisterClass(BaseAuthProvider,'workos')│
|
|
42
|
+
│ │ │ │ ▲ │
|
|
43
|
+
│ @workos-inc/authkit-js │ │ │ jwks-rsa + jsonwebtoken │
|
|
44
|
+
│ │ signIn → hosted login │ │ │ (verify signature, iss, aud) │
|
|
45
|
+
│ │ getUser() (email for display)│ │ │ │
|
|
46
|
+
│ └─ getAccessToken() ── Bearer ───┼────────┼──▶ AuthProviderFactory.getByIssuer(iss) │
|
|
47
|
+
│ │ │ → WorkOSProvider.extractUserInfo() │
|
|
48
|
+
│ │ │ → verifyUserRecord(email) │
|
|
49
|
+
└─────────────────────────────────────────┘ └──────────────────────────────────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Both ends register under the same lowercase key, `workos`, through MJ's class factory. The server
|
|
53
|
+
trusts the issuer/JWKS to verify the signature; the browser SDK manages the session and silent
|
|
54
|
+
token refresh.
|
|
55
|
+
|
|
56
|
+
## What's in this package
|
|
57
|
+
|
|
58
|
+
| Side | Class | File | Registers as |
|
|
59
|
+
|---|---|---|---|
|
|
60
|
+
| Server | `WorkOSProvider` | [`src/providers/WorkOSProvider.ts`](src/providers/WorkOSProvider.ts) | `@RegisterClass(BaseAuthProvider, 'workos')` |
|
|
61
|
+
| Browser | `MJWorkOSProvider` | `@memberjunction/ng-auth-services` → `src/lib/providers/mjexplorer-workos-provider.service.ts` | `@RegisterClass(MJAuthBase, 'workos')` |
|
|
62
|
+
|
|
63
|
+
The server provider is registered automatically (it's imported by
|
|
64
|
+
[`AuthProviderFactory.ts`](src/AuthProviderFactory.ts)); you only need to add a config entry.
|
|
65
|
+
|
|
66
|
+
## Prerequisites
|
|
67
|
+
|
|
68
|
+
- A [WorkOS](https://workos.com) account with **AuthKit / User Management** enabled.
|
|
69
|
+
- Your **Client ID** (looks like `client_01H...`) from the WorkOS dashboard.
|
|
70
|
+
- The browser app must depend on `@workos-inc/authkit-js` (already declared as a peer dependency
|
|
71
|
+
of `@memberjunction/ng-auth-services`, and a direct dependency of MJExplorer).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Step 1 — Configure the WorkOS dashboard
|
|
76
|
+
|
|
77
|
+
1. In the WorkOS dashboard, go to **Authentication → AuthKit** and enable it.
|
|
78
|
+
2. Under **Redirects**, add your app's redirect URI (e.g. `http://localhost:4200` for local
|
|
79
|
+
development, plus your production origin). This is where WorkOS returns the user after login.
|
|
80
|
+
3. Set a **Logout URI** (the page WorkOS returns to after sign-out — typically the same origin).
|
|
81
|
+
4. Note your **Client ID**. You'll use it in three places: the server `issuer`/`jwksUri`, the
|
|
82
|
+
server `clientId`, and the browser `WORKOS_CLIENTID`.
|
|
83
|
+
|
|
84
|
+
## Step 2 — Configure the MJ server (`mj.config.cjs`)
|
|
85
|
+
|
|
86
|
+
Add a `workos` entry to `authProviders`. The `issuer` and `jwksUri` are derived from your Client
|
|
87
|
+
ID:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// mj.config.cjs
|
|
91
|
+
module.exports = {
|
|
92
|
+
authProviders: [
|
|
93
|
+
{
|
|
94
|
+
name: 'workos-prod',
|
|
95
|
+
type: 'workos',
|
|
96
|
+
clientId: process.env.WORKOS_CLIENT_ID, // client_01H...
|
|
97
|
+
issuer: `https://api.workos.com/user_management/${process.env.WORKOS_CLIENT_ID}`,
|
|
98
|
+
jwksUri: `https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
|
|
99
|
+
// The audience MUST match the token's `aud` claim — see the note below.
|
|
100
|
+
audience: process.env.WORKOS_CLIENT_ID,
|
|
101
|
+
},
|
|
102
|
+
// ...other providers can coexist; the factory routes by the token's `iss` claim
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Getting `audience` right
|
|
108
|
+
|
|
109
|
+
MemberJunction **always enforces the `aud` claim** during verification. WorkOS sets `aud`
|
|
110
|
+
automatically (you cannot override it via a JWT Template — it's a reserved claim):
|
|
111
|
+
|
|
112
|
+
- **No Resource Indicators configured (default):** WorkOS uses a default audience unique to your
|
|
113
|
+
WorkOS environment. Decode a real access token (e.g. at [jwt.io](https://jwt.io)) and copy the
|
|
114
|
+
exact `aud` value into your `audience` config.
|
|
115
|
+
- **Resource Indicators configured:** the `aud` matches the requested resource — set `audience`
|
|
116
|
+
to that resource identifier.
|
|
117
|
+
|
|
118
|
+
> If `audience` does not match the token's `aud`, verification fails with
|
|
119
|
+
> `jwt audience invalid`. This is the second most common misconfiguration after the missing email
|
|
120
|
+
> claim.
|
|
121
|
+
|
|
122
|
+
## Step 3 — Add an `email` claim (JWT Template)
|
|
123
|
+
|
|
124
|
+
**This step is required.** A WorkOS AuthKit access token carries identity/session claims
|
|
125
|
+
(`sub`, `sid`, `org_id`, `role`, `permissions`) but **not** the user's email. MemberJunction
|
|
126
|
+
looks the user up by email server-side, so you must add it.
|
|
127
|
+
|
|
128
|
+
In the WorkOS dashboard, go to **Authentication → Sessions → JWT Template** (or
|
|
129
|
+
**JWT Templates**) and add:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"email": "{{user.email}}",
|
|
134
|
+
"given_name": "{{user.first_name}}",
|
|
135
|
+
"family_name": "{{user.last_name}}"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `email` is what MJ matches against the `User.Email` column — **required**.
|
|
140
|
+
- `given_name` / `family_name` are optional but recommended: they let MJ
|
|
141
|
+
[auto-provision new users](../MJServer/README.md) with proper names when
|
|
142
|
+
`userHandling.autoCreateNewUsers` is enabled.
|
|
143
|
+
- WorkOS JWT Templates drop any claim whose value renders to `null`, so users missing a first/last
|
|
144
|
+
name simply won't have those claims — the provider handles that gracefully (it falls back to
|
|
145
|
+
splitting a `name` claim, or to the email).
|
|
146
|
+
|
|
147
|
+
> **Why not just read email from the ID token?** The browser SDK's `getUser()` always has the
|
|
148
|
+
> email (used for display), but the **server** only sees the bearer **access token**. The JWT
|
|
149
|
+
> Template is what puts the email into that access token.
|
|
150
|
+
|
|
151
|
+
## Step 4 — Configure the browser (MJExplorer)
|
|
152
|
+
|
|
153
|
+
Set `AUTH_TYPE` to `workos` and provide the WorkOS environment keys. These flow into the provider
|
|
154
|
+
through `AuthServicesModule.forRoot(environment)`:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// environment.ts
|
|
158
|
+
export const environment = {
|
|
159
|
+
// ...
|
|
160
|
+
AUTH_TYPE: 'workos',
|
|
161
|
+
WORKOS_CLIENTID: 'client_01H...', // required
|
|
162
|
+
WORKOS_REDIRECT_URI: window.location.origin, // optional; defaults to window.location.origin
|
|
163
|
+
WORKOS_API_HOSTNAME: undefined, // optional; only for custom domains / proxies
|
|
164
|
+
WORKOS_DEV_MODE: false, // optional; localStorage session for local dev
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Env key | Required | Purpose |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `AUTH_TYPE` | ✅ | Must be `'workos'` to select this provider |
|
|
171
|
+
| `WORKOS_CLIENTID` | ✅ | Your AuthKit Client ID |
|
|
172
|
+
| `WORKOS_REDIRECT_URI` | — | Redirect target after login (default: `window.location.origin`) |
|
|
173
|
+
| `WORKOS_API_HOSTNAME` | — | Override the WorkOS API hostname (custom domains / proxy) |
|
|
174
|
+
| `WORKOS_DEV_MODE` | — | AuthKit dev mode — localStorage-backed session for local development |
|
|
175
|
+
|
|
176
|
+
That's it. On startup, `MJWorkOSProvider.initialize()` constructs the AuthKit client (which also
|
|
177
|
+
processes any pending redirect callback), establishes the session, and the rest of MJ works
|
|
178
|
+
unchanged — `getIdToken()` hands the access token to the GraphQL client as a Bearer token.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Token claims reference
|
|
183
|
+
|
|
184
|
+
A WorkOS AuthKit **access token** (what `getAccessToken()` returns and the server validates),
|
|
185
|
+
**after** adding the recommended JWT Template:
|
|
186
|
+
|
|
187
|
+
```jsonc
|
|
188
|
+
{
|
|
189
|
+
"iss": "https://api.workos.com/user_management/client_01H...",
|
|
190
|
+
"sub": "user_01HXYZ...", // WorkOS user id → AuthUserInfo.userId
|
|
191
|
+
"sid": "session_01H...", // session id
|
|
192
|
+
"jti": "01H...",
|
|
193
|
+
"aud": "<environment default or resource indicator>", // MJ enforces this
|
|
194
|
+
"org_id": "org_01H...", // present for organization-scoped sessions
|
|
195
|
+
"role": "admin", // organization-membership role
|
|
196
|
+
"permissions": ["posts:read"], // role permissions
|
|
197
|
+
"exp": 1717000000,
|
|
198
|
+
"iat": 1716996400,
|
|
199
|
+
|
|
200
|
+
// ── added by your JWT Template ──
|
|
201
|
+
"email": "ada@example.com", // REQUIRED for MJ user resolution
|
|
202
|
+
"given_name": "Ada",
|
|
203
|
+
"family_name": "Lovelace"
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`WorkOSProvider.extractUserInfo()` maps this to MJ's `AuthUserInfo`:
|
|
208
|
+
|
|
209
|
+
| `AuthUserInfo` field | Source claim | Fallback |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `email` | `email` | — (undefined if no JWT Template) |
|
|
212
|
+
| `firstName` | `given_name` | first word of `name` |
|
|
213
|
+
| `lastName` | `family_name` | second word of `name`, else first word |
|
|
214
|
+
| `fullName` | `name` | — |
|
|
215
|
+
| `preferredUsername` | `preferred_username` | `email` |
|
|
216
|
+
| `userId` | `sub` | — |
|
|
217
|
+
|
|
218
|
+
## How validation works under the hood
|
|
219
|
+
|
|
220
|
+
1. The browser sends `Authorization: Bearer <accessToken>` to the MJ GraphQL API.
|
|
221
|
+
2. MJServer decodes the token (without verifying) to read the `iss` claim.
|
|
222
|
+
3. `AuthProviderFactory.getByIssuer(iss)` resolves the `WorkOSProvider` instance (issuer matching
|
|
223
|
+
is case-insensitive and trailing-slash tolerant).
|
|
224
|
+
4. `jwt.verify()` validates the RS256 signature against the JWKS at
|
|
225
|
+
`https://api.workos.com/sso/jwks/<clientId>` (cached, with retry/backoff handled by
|
|
226
|
+
`BaseAuthProvider`), and checks `iss` + `aud`.
|
|
227
|
+
5. `WorkOSProvider.extractUserInfo(payload)` produces the normalized identity.
|
|
228
|
+
6. MJServer resolves/creates the `User` record by **email** and builds the request context.
|
|
229
|
+
|
|
230
|
+
See [`@memberjunction/auth-providers` README](README.md) and
|
|
231
|
+
[`@memberjunction/server`](../MJServer/README.md) for the shared validation pipeline.
|
|
232
|
+
|
|
233
|
+
## Troubleshooting
|
|
234
|
+
|
|
235
|
+
| Symptom | Likely cause | Fix |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| Server logs "user not found" / login bounces | Access token has no `email` claim | [Add the JWT Template](#step-3--add-an-email-claim-jwt-template) |
|
|
238
|
+
| `jwt audience invalid` | `audience` config ≠ token `aud` | Decode a real token, copy its `aud` into `audience` ([Step 2](#getting-audience-right)) |
|
|
239
|
+
| `No authentication provider found for issuer` | `issuer` config doesn't match the token | Ensure `issuer` is `https://api.workos.com/user_management/<clientId>` with your real Client ID |
|
|
240
|
+
| `jwt expired` right after login | Clock skew or stale token | The browser SDK auto-refreshes; ensure server time is correct |
|
|
241
|
+
| Login redirect loops | Redirect URI not registered | Add your exact origin under WorkOS dashboard → Redirects |
|
|
242
|
+
| New users not auto-created | Missing `given_name`/`family_name` or `autoCreateNewUsers` off | Add name claims to the JWT Template; check `userHandling` config |
|
|
243
|
+
|
|
244
|
+
## Scope & non-goals
|
|
245
|
+
|
|
246
|
+
- **In scope:** WorkOS **AuthKit / User Management** (OIDC/JWT bearer login + validation). This is
|
|
247
|
+
the same OIDC/JWT model every other MJ provider uses.
|
|
248
|
+
- **Out of scope (today):** WorkOS **SSO via SAML assertions** and **Directory Sync (SCIM)**
|
|
249
|
+
user provisioning as distinct flows. AuthKit can itself broker SSO connections behind its hosted
|
|
250
|
+
login, so SSO end-users still authenticate through the `workos` provider — but MJ does not parse
|
|
251
|
+
raw SAML assertions or run SCIM provisioning. Those would be a separate, larger effort.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
*Part of [`@memberjunction/auth-providers`](README.md) in the
|
|
256
|
+
[MemberJunction](https://github.com/MemberJunction/MJ) monorepo.*
|
|
@@ -6,6 +6,7 @@ import './providers/MSALProvider.js';
|
|
|
6
6
|
import './providers/OktaProvider.js';
|
|
7
7
|
import './providers/CognitoProvider.js';
|
|
8
8
|
import './providers/GoogleProvider.js';
|
|
9
|
+
import './providers/WorkOSProvider.js';
|
|
9
10
|
import './providers/MagicLinkProvider.js';
|
|
10
11
|
/**
|
|
11
12
|
* Factory and registry for managing authentication providers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthProviderFactory.d.ts","sourceRoot":"","sources":["../src/AuthProviderFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAe,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAY,aAAa,EAAc,MAAM,wBAAwB,CAAC;AAG7E,OAAO,8BAA8B,CAAC;AACtC,OAAO,6BAA6B,CAAC;AACrC,OAAO,6BAA6B,CAAC;AACrC,OAAO,gCAAgC,CAAC;AACxC,OAAO,+BAA+B,CAAC;AACvC,OAAO,kCAAkC,CAAC;AAE1C;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,aAAa,CAAC,mBAAmB,CAAC;IACzE,OAAO,CAAC,SAAS,CAAyC;IAC1D;;;;;;OAMG;IACH,OAAO,CAAC,WAAW,CAA6F;IAChH;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAAiG;;IAMzH;;OAEG;IACH,WAAkB,QAAQ,IAAI,mBAAmB,CAEhD;IAED;;;OAGG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa;IAsBhE;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAiBvC;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAmBtD;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE;IAqB/C;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIlD;;OAEG;IACH,eAAe,IAAI,aAAa,EAAE;IAIlC;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,MAAM,CAAC,0BAA0B,IAAI,MAAM,EAAE;IAW7C;;OAEG;IACH,MAAM,CAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CASvD"}
|
|
1
|
+
{"version":3,"file":"AuthProviderFactory.d.ts","sourceRoot":"","sources":["../src/AuthProviderFactory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAe,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAY,aAAa,EAAc,MAAM,wBAAwB,CAAC;AAG7E,OAAO,8BAA8B,CAAC;AACtC,OAAO,6BAA6B,CAAC;AACrC,OAAO,6BAA6B,CAAC;AACrC,OAAO,gCAAgC,CAAC;AACxC,OAAO,+BAA+B,CAAC;AACvC,OAAO,+BAA+B,CAAC;AACvC,OAAO,kCAAkC,CAAC;AAE1C;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,aAAa,CAAC,mBAAmB,CAAC;IACzE,OAAO,CAAC,SAAS,CAAyC;IAC1D;;;;;;OAMG;IACH,OAAO,CAAC,WAAW,CAA6F;IAChH;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAAiG;;IAMzH;;OAEG;IACH,WAAkB,QAAQ,IAAI,mBAAmB,CAEhD;IAED;;;OAGG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa;IAsBhE;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAiBvC;;OAEG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAmBtD;;;;;;OAMG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE;IAqB/C;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIlD;;OAEG;IACH,eAAe,IAAI,aAAa,EAAE;IAIlC;;OAEG;IACH,YAAY,IAAI,OAAO;IAIvB;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,MAAM,CAAC,0BAA0B,IAAI,MAAM,EAAE;IAW7C;;OAEG;IACH,MAAM,CAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CASvD"}
|
|
@@ -7,6 +7,7 @@ import './providers/MSALProvider.js';
|
|
|
7
7
|
import './providers/OktaProvider.js';
|
|
8
8
|
import './providers/CognitoProvider.js';
|
|
9
9
|
import './providers/GoogleProvider.js';
|
|
10
|
+
import './providers/WorkOSProvider.js';
|
|
10
11
|
import './providers/MagicLinkProvider.js';
|
|
11
12
|
/**
|
|
12
13
|
* Factory and registry for managing authentication providers
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthProviderFactory.js","sourceRoot":"","sources":["../src/AuthProviderFactory.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuthProviderFactory.js","sourceRoot":"","sources":["../src/AuthProviderFactory.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { BaseAuthProvider } from './BaseAuthProvider.js';
|
|
|
3
3
|
export { AuthProviderFactory } from './AuthProviderFactory.js';
|
|
4
4
|
export { TokenExpiredError } from './tokenExpiredError.js';
|
|
5
5
|
export { MagicLinkProvider } from './providers/MagicLinkProvider.js';
|
|
6
|
+
export { HostIdentityProvider, type HostAssertionVerifyResult, type HostAssertionError, } from './providers/HostIdentityProvider.js';
|
|
6
7
|
export type { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,oBAAoB,EACpB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { BaseAuthProvider } from './BaseAuthProvider.js';
|
|
|
2
2
|
export { AuthProviderFactory } from './AuthProviderFactory.js';
|
|
3
3
|
export { TokenExpiredError } from './tokenExpiredError.js';
|
|
4
4
|
export { MagicLinkProvider } from './providers/MagicLinkProvider.js';
|
|
5
|
+
export { HostIdentityProvider, } from './providers/HostIdentityProvider.js';
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,oBAAoB,GAGrB,MAAM,qCAAqC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { JwtPayload } from 'jsonwebtoken';
|
|
2
|
+
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
3
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
4
|
+
/** Why a host-assertion verification failed (mirrors the public web-widget mint error codes). */
|
|
5
|
+
export type HostAssertionError = 'missing' | 'bad_signature' | 'expired' | 'no_email' | 'no_key';
|
|
6
|
+
/** Result of verifying a host-signed identity assertion. `userInfo` is present iff `ok` is true. */
|
|
7
|
+
export interface HostAssertionVerifyResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
userInfo?: AuthUserInfo;
|
|
10
|
+
/** The host's opaque user id for the visitor (assertion `sub`) — audit correlation, not an MJ user id. */
|
|
11
|
+
hostUserId?: string;
|
|
12
|
+
errorCode?: HostAssertionError;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Authentication provider for the public web-widget **host-identity** strategy (D1).
|
|
16
|
+
*
|
|
17
|
+
* When a widget is embedded in an already-authenticated host portal, the host signs a short-lived
|
|
18
|
+
* RS256 assertion (with ITS OWN key) describing the visitor. Unlike Auth0/magic-link tokens — validated
|
|
19
|
+
* by the standard issuer→JWKS path — a host assertion is verified against a STATIC per-widget public key
|
|
20
|
+
* (stored on `WidgetInstance.HostPublicKey`) and exchanged at `POST /widget/session` for an MJ guest JWT.
|
|
21
|
+
* So this provider does NOT use the JWKS machinery: it exposes {@link VerifyHostAssertion} (static-PEM
|
|
22
|
+
* RS256 verification) + {@link extractUserInfo}, and the widget mint resolves it via the ClassFactory.
|
|
23
|
+
*
|
|
24
|
+
* SECURITY INVARIANT — key/assertion provenance MUST be independent. The party that presents a signed
|
|
25
|
+
* assertion must NOT also be able to provide or influence the public key used to verify it. If one party
|
|
26
|
+
* controlled both, the signature would prove nothing: an attacker could mint a fresh keypair, sign any
|
|
27
|
+
* claims with the private key, hand over the matching public key, and the check would trivially pass. The
|
|
28
|
+
* verifying key must therefore come from an independent trust path — here, out-of-band administrator
|
|
29
|
+
* configuration on `WidgetInstance.HostPublicKey` (the analog, for a static PEM, of pinning a trusted
|
|
30
|
+
* issuer's JWKS endpoint). That is why {@link VerifyHostAssertion} takes the PEM as a caller-supplied
|
|
31
|
+
* argument rather than reading any key material from the assertion itself: the caller (the widget mint)
|
|
32
|
+
* is responsible for sourcing the PEM from the registered widget record, never from the request payload.
|
|
33
|
+
*
|
|
34
|
+
* It still subclasses {@link BaseAuthProvider} (and registers in the same factory) so host-identity is a
|
|
35
|
+
* first-class, discoverable provider rather than ad-hoc mint code — the architecture the widget plan calls
|
|
36
|
+
* for. The base constructor builds a JWKS client from `jwksUri`; host-identity has no JWKS, so callers pass
|
|
37
|
+
* a placeholder `jwksUri` that is never contacted (verification goes through {@link VerifyHostAssertion}).
|
|
38
|
+
*/
|
|
39
|
+
export declare class HostIdentityProvider extends BaseAuthProvider {
|
|
40
|
+
constructor(config: AuthProviderConfig);
|
|
41
|
+
/** Host identity never uses JWKS — config is valid as long as it names the provider. */
|
|
42
|
+
validateConfig(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Verifies a host-signed RS256 assertion against the host's STATIC public key (PEM) and extracts the
|
|
45
|
+
* asserted visitor identity. The assertion's `aud` must equal `expectedAudience` (the widget key, bound
|
|
46
|
+
* at the host) and it must carry an `email`. It must also be short-lived: it must declare its own `exp`
|
|
47
|
+
* AND be no older than {@link HOST_ASSERTION_MAX_AGE_SECONDS} since `iat` — so a misbehaving host cannot
|
|
48
|
+
* mint an unbounded token. Never throws — returns a structured result.
|
|
49
|
+
*
|
|
50
|
+
* SECURITY: `hostPublicKeyPem` MUST originate from an independent trust path (the registered
|
|
51
|
+
* `WidgetInstance.HostPublicKey`, set out-of-band by an administrator) — NEVER from the same request
|
|
52
|
+
* that carries `assertion`. Passing a key the assertion's presenter could influence defeats the
|
|
53
|
+
* signature check entirely (see the class-level SECURITY INVARIANT note).
|
|
54
|
+
*/
|
|
55
|
+
VerifyHostAssertion(assertion: string | undefined, hostPublicKeyPem: string | undefined, expectedAudience: string): HostAssertionVerifyResult;
|
|
56
|
+
/** Maps a verified host-assertion payload to standard MJ identity fields. */
|
|
57
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=HostIdentityProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostIdentityProvider.d.ts","sourceRoot":"","sources":["../../src/providers/HostIdentityProvider.ts"],"names":[],"mappings":"AAAA,OAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,iGAAiG;AACjG,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAUjG,oGAAoG;AACpG,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,0GAA0G;IAC1G,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBACa,oBAAqB,SAAQ,gBAAgB;gBAC5C,MAAM,EAAE,kBAAkB;IAItC,wFAAwF;IAC/E,cAAc,IAAI,OAAO;IAIlC;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,gBAAgB,EAAE,MAAM,GACvB,yBAAyB;IAiC5B,6EAA6E;IAC7E,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,YAAY;CAanD"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import jwt from 'jsonwebtoken';
|
|
11
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
12
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
13
|
+
/**
|
|
14
|
+
* Absolute ceiling on a host assertion's age (measured from its `iat`), independent of the host-chosen
|
|
15
|
+
* `exp`. Even if a host mints a long-lived — or `exp`-less — assertion, MJ refuses to accept it beyond
|
|
16
|
+
* this window. Enforces the "short-lived" contract the strategy assumes rather than trusting the host to
|
|
17
|
+
* honor it. Effective lifetime is therefore min(assertion `exp`, `iat` + this).
|
|
18
|
+
*/
|
|
19
|
+
const HOST_ASSERTION_MAX_AGE_SECONDS = 600; // 10 minutes
|
|
20
|
+
/**
|
|
21
|
+
* Authentication provider for the public web-widget **host-identity** strategy (D1).
|
|
22
|
+
*
|
|
23
|
+
* When a widget is embedded in an already-authenticated host portal, the host signs a short-lived
|
|
24
|
+
* RS256 assertion (with ITS OWN key) describing the visitor. Unlike Auth0/magic-link tokens — validated
|
|
25
|
+
* by the standard issuer→JWKS path — a host assertion is verified against a STATIC per-widget public key
|
|
26
|
+
* (stored on `WidgetInstance.HostPublicKey`) and exchanged at `POST /widget/session` for an MJ guest JWT.
|
|
27
|
+
* So this provider does NOT use the JWKS machinery: it exposes {@link VerifyHostAssertion} (static-PEM
|
|
28
|
+
* RS256 verification) + {@link extractUserInfo}, and the widget mint resolves it via the ClassFactory.
|
|
29
|
+
*
|
|
30
|
+
* SECURITY INVARIANT — key/assertion provenance MUST be independent. The party that presents a signed
|
|
31
|
+
* assertion must NOT also be able to provide or influence the public key used to verify it. If one party
|
|
32
|
+
* controlled both, the signature would prove nothing: an attacker could mint a fresh keypair, sign any
|
|
33
|
+
* claims with the private key, hand over the matching public key, and the check would trivially pass. The
|
|
34
|
+
* verifying key must therefore come from an independent trust path — here, out-of-band administrator
|
|
35
|
+
* configuration on `WidgetInstance.HostPublicKey` (the analog, for a static PEM, of pinning a trusted
|
|
36
|
+
* issuer's JWKS endpoint). That is why {@link VerifyHostAssertion} takes the PEM as a caller-supplied
|
|
37
|
+
* argument rather than reading any key material from the assertion itself: the caller (the widget mint)
|
|
38
|
+
* is responsible for sourcing the PEM from the registered widget record, never from the request payload.
|
|
39
|
+
*
|
|
40
|
+
* It still subclasses {@link BaseAuthProvider} (and registers in the same factory) so host-identity is a
|
|
41
|
+
* first-class, discoverable provider rather than ad-hoc mint code — the architecture the widget plan calls
|
|
42
|
+
* for. The base constructor builds a JWKS client from `jwksUri`; host-identity has no JWKS, so callers pass
|
|
43
|
+
* a placeholder `jwksUri` that is never contacted (verification goes through {@link VerifyHostAssertion}).
|
|
44
|
+
*/
|
|
45
|
+
let HostIdentityProvider = class HostIdentityProvider extends BaseAuthProvider {
|
|
46
|
+
constructor(config) {
|
|
47
|
+
super(config);
|
|
48
|
+
}
|
|
49
|
+
/** Host identity never uses JWKS — config is valid as long as it names the provider. */
|
|
50
|
+
validateConfig() {
|
|
51
|
+
return !!this.name;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verifies a host-signed RS256 assertion against the host's STATIC public key (PEM) and extracts the
|
|
55
|
+
* asserted visitor identity. The assertion's `aud` must equal `expectedAudience` (the widget key, bound
|
|
56
|
+
* at the host) and it must carry an `email`. It must also be short-lived: it must declare its own `exp`
|
|
57
|
+
* AND be no older than {@link HOST_ASSERTION_MAX_AGE_SECONDS} since `iat` — so a misbehaving host cannot
|
|
58
|
+
* mint an unbounded token. Never throws — returns a structured result.
|
|
59
|
+
*
|
|
60
|
+
* SECURITY: `hostPublicKeyPem` MUST originate from an independent trust path (the registered
|
|
61
|
+
* `WidgetInstance.HostPublicKey`, set out-of-band by an administrator) — NEVER from the same request
|
|
62
|
+
* that carries `assertion`. Passing a key the assertion's presenter could influence defeats the
|
|
63
|
+
* signature check entirely (see the class-level SECURITY INVARIANT note).
|
|
64
|
+
*/
|
|
65
|
+
VerifyHostAssertion(assertion, hostPublicKeyPem, expectedAudience) {
|
|
66
|
+
if (!assertion) {
|
|
67
|
+
return { ok: false, errorCode: 'missing' };
|
|
68
|
+
}
|
|
69
|
+
if (!hostPublicKeyPem) {
|
|
70
|
+
return { ok: false, errorCode: 'no_key' };
|
|
71
|
+
}
|
|
72
|
+
let payload;
|
|
73
|
+
try {
|
|
74
|
+
// maxAge caps the assertion's age from `iat` regardless of the host-chosen `exp` (and requires `iat`
|
|
75
|
+
// to be present); jsonwebtoken surfaces a breach as TokenExpiredError, same as a normal expiry.
|
|
76
|
+
payload = jwt.verify(assertion, hostPublicKeyPem, {
|
|
77
|
+
algorithms: ['RS256'],
|
|
78
|
+
audience: expectedAudience,
|
|
79
|
+
maxAge: HOST_ASSERTION_MAX_AGE_SECONDS,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// jsonwebtoken throws TokenExpiredError for expiry / maxAge; everything else is a signature/format fault.
|
|
84
|
+
return { ok: false, errorCode: e instanceof jwt.TokenExpiredError ? 'expired' : 'bad_signature' };
|
|
85
|
+
}
|
|
86
|
+
// Require an explicit expiry: we refuse to treat an unbounded assertion as valid even inside the maxAge
|
|
87
|
+
// window, so the host's "short-lived" contract is enforced, not merely assumed.
|
|
88
|
+
if (typeof payload.exp !== 'number') {
|
|
89
|
+
return { ok: false, errorCode: 'expired' };
|
|
90
|
+
}
|
|
91
|
+
const userInfo = this.extractUserInfo(payload);
|
|
92
|
+
if (!userInfo.email) {
|
|
93
|
+
return { ok: false, errorCode: 'no_email' };
|
|
94
|
+
}
|
|
95
|
+
const sub = typeof payload.sub === 'string' ? payload.sub : undefined;
|
|
96
|
+
return { ok: true, userInfo, hostUserId: sub };
|
|
97
|
+
}
|
|
98
|
+
/** Maps a verified host-assertion payload to standard MJ identity fields. */
|
|
99
|
+
extractUserInfo(payload) {
|
|
100
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
101
|
+
const email = str(payload.email);
|
|
102
|
+
const firstName = str(payload.given_name) ?? str(payload['firstName']);
|
|
103
|
+
const lastName = str(payload.family_name) ?? str(payload['lastName']);
|
|
104
|
+
return {
|
|
105
|
+
email,
|
|
106
|
+
firstName,
|
|
107
|
+
lastName,
|
|
108
|
+
fullName: str(payload.name) ?? ([firstName, lastName].filter(Boolean).join(' ') || undefined),
|
|
109
|
+
preferredUsername: email,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
HostIdentityProvider = __decorate([
|
|
114
|
+
RegisterClass(BaseAuthProvider, 'host-identity'),
|
|
115
|
+
__metadata("design:paramtypes", [Object])
|
|
116
|
+
], HostIdentityProvider);
|
|
117
|
+
export { HostIdentityProvider };
|
|
118
|
+
//# sourceMappingURL=HostIdentityProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HostIdentityProvider.js","sourceRoot":"","sources":["../../src/providers/HostIdentityProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { JwtPayload } from 'jsonwebtoken';
|
|
2
|
+
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
3
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
4
|
+
/**
|
|
5
|
+
* WorkOS (AuthKit) authentication provider implementation.
|
|
6
|
+
*
|
|
7
|
+
* Validates JWT access tokens minted by WorkOS AuthKit / User Management. These tokens are
|
|
8
|
+
* signed with RS256 and verified against the per-environment JWKS endpoint
|
|
9
|
+
* (`https://api.workos.com/sso/jwks/<clientId>`); their `iss` claim is
|
|
10
|
+
* `https://api.workos.com/user_management/<clientId>`.
|
|
11
|
+
*
|
|
12
|
+
* ## Email is required — and is NOT in a WorkOS token by default
|
|
13
|
+
* MemberJunction resolves the signed-in user by **email**, but a WorkOS AuthKit access token
|
|
14
|
+
* only carries identity/session claims (`sub`, `sid`, `org_id`, `role`, `permissions`) out of
|
|
15
|
+
* the box — it does **not** include `email`. To use WorkOS with MJ you must add the email (and
|
|
16
|
+
* ideally the name) to the token via a WorkOS **JWT Template**:
|
|
17
|
+
*
|
|
18
|
+
* ```json
|
|
19
|
+
* {
|
|
20
|
+
* "email": "{{user.email}}",
|
|
21
|
+
* "given_name": "{{user.first_name}}",
|
|
22
|
+
* "family_name": "{{user.last_name}}"
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* See `WORKOS.md` in this package for the full, step-by-step integration guide.
|
|
27
|
+
*/
|
|
28
|
+
export declare class WorkOSProvider extends BaseAuthProvider {
|
|
29
|
+
constructor(config: AuthProviderConfig);
|
|
30
|
+
/**
|
|
31
|
+
* Extracts user information from a WorkOS AuthKit JWT payload.
|
|
32
|
+
*
|
|
33
|
+
* `email`, `given_name`, and `family_name` are expected to be supplied by a WorkOS JWT
|
|
34
|
+
* Template (see class docs). `sub` is the stable WorkOS user id (`user_...`). We fall back
|
|
35
|
+
* through standard OIDC claim names so a token shaped by a custom template (e.g. one that
|
|
36
|
+
* only sets `name`) still resolves a usable identity.
|
|
37
|
+
*/
|
|
38
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo;
|
|
39
|
+
/**
|
|
40
|
+
* Validates WorkOS-specific configuration.
|
|
41
|
+
*
|
|
42
|
+
* Beyond the base requirements (`name`, `issuer`, `audience`, `jwksUri`), WorkOS needs a
|
|
43
|
+
* `clientId` — it appears in both the issuer and JWKS URLs and identifies the AuthKit
|
|
44
|
+
* environment.
|
|
45
|
+
*/
|
|
46
|
+
validateConfig(): boolean;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=WorkOSProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkOSProvider.d.ts","sourceRoot":"","sources":["../../src/providers/WorkOSProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBACa,cAAe,SAAQ,gBAAgB;gBACtC,MAAM,EAAE,kBAAkB;IAItC;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,YAAY;IAoBlD;;;;;;OAMG;IACH,cAAc,IAAI,OAAO;CAM1B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
11
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
12
|
+
/**
|
|
13
|
+
* WorkOS (AuthKit) authentication provider implementation.
|
|
14
|
+
*
|
|
15
|
+
* Validates JWT access tokens minted by WorkOS AuthKit / User Management. These tokens are
|
|
16
|
+
* signed with RS256 and verified against the per-environment JWKS endpoint
|
|
17
|
+
* (`https://api.workos.com/sso/jwks/<clientId>`); their `iss` claim is
|
|
18
|
+
* `https://api.workos.com/user_management/<clientId>`.
|
|
19
|
+
*
|
|
20
|
+
* ## Email is required — and is NOT in a WorkOS token by default
|
|
21
|
+
* MemberJunction resolves the signed-in user by **email**, but a WorkOS AuthKit access token
|
|
22
|
+
* only carries identity/session claims (`sub`, `sid`, `org_id`, `role`, `permissions`) out of
|
|
23
|
+
* the box — it does **not** include `email`. To use WorkOS with MJ you must add the email (and
|
|
24
|
+
* ideally the name) to the token via a WorkOS **JWT Template**:
|
|
25
|
+
*
|
|
26
|
+
* ```json
|
|
27
|
+
* {
|
|
28
|
+
* "email": "{{user.email}}",
|
|
29
|
+
* "given_name": "{{user.first_name}}",
|
|
30
|
+
* "family_name": "{{user.last_name}}"
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* See `WORKOS.md` in this package for the full, step-by-step integration guide.
|
|
35
|
+
*/
|
|
36
|
+
let WorkOSProvider = class WorkOSProvider extends BaseAuthProvider {
|
|
37
|
+
constructor(config) {
|
|
38
|
+
super(config);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extracts user information from a WorkOS AuthKit JWT payload.
|
|
42
|
+
*
|
|
43
|
+
* `email`, `given_name`, and `family_name` are expected to be supplied by a WorkOS JWT
|
|
44
|
+
* Template (see class docs). `sub` is the stable WorkOS user id (`user_...`). We fall back
|
|
45
|
+
* through standard OIDC claim names so a token shaped by a custom template (e.g. one that
|
|
46
|
+
* only sets `name`) still resolves a usable identity.
|
|
47
|
+
*/
|
|
48
|
+
extractUserInfo(payload) {
|
|
49
|
+
const email = payload.email;
|
|
50
|
+
const fullName = payload.name;
|
|
51
|
+
const firstName = payload.given_name;
|
|
52
|
+
const lastName = payload.family_name;
|
|
53
|
+
// WorkOS access tokens have no `preferred_username`; email is the natural handle.
|
|
54
|
+
const preferredUsername = payload.preferred_username || email;
|
|
55
|
+
// `sub` is the WorkOS user identifier (e.g. "user_01H...").
|
|
56
|
+
const userId = payload.sub;
|
|
57
|
+
return {
|
|
58
|
+
email,
|
|
59
|
+
firstName: firstName || fullName?.split(' ')[0],
|
|
60
|
+
lastName: lastName || fullName?.split(' ')[1] || fullName?.split(' ')[0],
|
|
61
|
+
fullName,
|
|
62
|
+
preferredUsername,
|
|
63
|
+
userId
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Validates WorkOS-specific configuration.
|
|
68
|
+
*
|
|
69
|
+
* Beyond the base requirements (`name`, `issuer`, `audience`, `jwksUri`), WorkOS needs a
|
|
70
|
+
* `clientId` — it appears in both the issuer and JWKS URLs and identifies the AuthKit
|
|
71
|
+
* environment.
|
|
72
|
+
*/
|
|
73
|
+
validateConfig() {
|
|
74
|
+
const baseValid = super.validateConfig();
|
|
75
|
+
const hasClientId = !!this.config.clientId;
|
|
76
|
+
return baseValid && hasClientId;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
WorkOSProvider = __decorate([
|
|
80
|
+
RegisterClass(BaseAuthProvider, 'workos'),
|
|
81
|
+
__metadata("design:paramtypes", [Object])
|
|
82
|
+
], WorkOSProvider);
|
|
83
|
+
export { WorkOSProvider };
|
|
84
|
+
//# sourceMappingURL=WorkOSProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkOSProvider.js","sourceRoot":"","sources":["../../src/providers/WorkOSProvider.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,gBAAgB;IAClD,YAAY,MAA0B;QACpC,KAAK,CAAC,MAAM,CAAC,CAAC;IAChB,CAAC;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,OAAmB;QACjC,MAAM,KAAK,GAAG,OAAO,CAAC,KAA2B,CAAC;QAClD,MAAM,QAAQ,GAAG,OAAO,CAAC,IAA0B,CAAC;QACpD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAgC,CAAC;QAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAiC,CAAC;QAC3D,kFAAkF;QAClF,MAAM,iBAAiB,GAAG,OAAO,CAAC,kBAAwC,IAAI,KAAK,CAAC;QACpF,4DAA4D;QAC5D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAyB,CAAC;QAEjD,OAAO;YACL,KAAK;YACL,SAAS,EAAE,SAAS,IAAI,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC/C,QAAQ,EAAE,QAAQ,IAAI,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACxE,QAAQ;YACR,iBAAiB;YACjB,MAAM;SACP,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,cAAc;QACZ,MAAM,SAAS,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC;QACzC,MAAM,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAE3C,OAAO,SAAS,IAAI,WAAW,CAAC;IAClC,CAAC;CACF,CAAA;AA9CY,cAAc;IAD1B,aAAa,CAAC,gBAAgB,EAAE,QAAQ,CAAC;;GAC7B,cAAc,CA8C1B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/auth-providers",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.45.0",
|
|
4
4
|
"description": "Authentication provider interfaces, base classes, and implementations for MemberJunction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"test:watch": "vitest"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@memberjunction/core": "5.
|
|
15
|
-
"@memberjunction/global": "5.
|
|
14
|
+
"@memberjunction/core": "5.45.0",
|
|
15
|
+
"@memberjunction/global": "5.45.0",
|
|
16
16
|
"graphql": "^16.12.0",
|
|
17
17
|
"jsonwebtoken": "9.0.3",
|
|
18
18
|
"jwks-rsa": "^3.2.2"
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for WorkOSProvider.
|
|
3
|
+
*
|
|
4
|
+
* Covers the two pieces of logic the provider owns on top of BaseAuthProvider:
|
|
5
|
+
* - extractUserInfo(): mapping WorkOS AuthKit JWT claims → AuthUserInfo, including the
|
|
6
|
+
* fallbacks that keep a usable identity when a custom JWT Template only sets `name`.
|
|
7
|
+
* - validateConfig(): the WorkOS-specific requirement (clientId) on top of the base fields.
|
|
8
|
+
*
|
|
9
|
+
* It also asserts the provider registers with the class factory under the `workos` key, which
|
|
10
|
+
* is what makes `AuthProviderFactory.createProvider({ type: 'workos', ... })` resolve it.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import type { JwtPayload } from 'jsonwebtoken';
|
|
14
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
15
|
+
import { AuthProviderConfig } from '@memberjunction/core';
|
|
16
|
+
import { BaseAuthProvider } from '../BaseAuthProvider';
|
|
17
|
+
import { WorkOSProvider } from '../providers/WorkOSProvider';
|
|
18
|
+
|
|
19
|
+
const CLIENT_ID = 'client_01HABCDEF';
|
|
20
|
+
|
|
21
|
+
function makeConfig(overrides: Partial<AuthProviderConfig> = {}): AuthProviderConfig {
|
|
22
|
+
return {
|
|
23
|
+
name: 'workos-test',
|
|
24
|
+
type: 'workos',
|
|
25
|
+
issuer: `https://api.workos.com/user_management/${CLIENT_ID}`,
|
|
26
|
+
audience: CLIENT_ID,
|
|
27
|
+
jwksUri: `https://api.workos.com/sso/jwks/${CLIENT_ID}`,
|
|
28
|
+
clientId: CLIENT_ID,
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeProvider(overrides: Partial<AuthProviderConfig> = {}): WorkOSProvider {
|
|
34
|
+
return new WorkOSProvider(makeConfig(overrides));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('WorkOSProvider', () => {
|
|
38
|
+
describe('extractUserInfo', () => {
|
|
39
|
+
it('maps a token shaped by the recommended JWT Template (email + given/family name)', () => {
|
|
40
|
+
const provider = makeProvider();
|
|
41
|
+
const payload: JwtPayload = {
|
|
42
|
+
sub: 'user_01HXYZ',
|
|
43
|
+
email: 'ada@example.com',
|
|
44
|
+
given_name: 'Ada',
|
|
45
|
+
family_name: 'Lovelace',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const info = provider.extractUserInfo(payload);
|
|
49
|
+
|
|
50
|
+
expect(info.email).toBe('ada@example.com');
|
|
51
|
+
expect(info.firstName).toBe('Ada');
|
|
52
|
+
expect(info.lastName).toBe('Lovelace');
|
|
53
|
+
// No preferred_username on WorkOS tokens — falls back to email.
|
|
54
|
+
expect(info.preferredUsername).toBe('ada@example.com');
|
|
55
|
+
// `sub` is surfaced as the provider user id.
|
|
56
|
+
expect(info.userId).toBe('user_01HXYZ');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('derives first/last name from a single `name` claim when given/family are absent', () => {
|
|
60
|
+
const provider = makeProvider();
|
|
61
|
+
const payload: JwtPayload = {
|
|
62
|
+
sub: 'user_01HXYZ',
|
|
63
|
+
email: 'grace.hopper@example.com',
|
|
64
|
+
name: 'Grace Hopper',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const info = provider.extractUserInfo(payload);
|
|
68
|
+
|
|
69
|
+
expect(info.fullName).toBe('Grace Hopper');
|
|
70
|
+
expect(info.firstName).toBe('Grace');
|
|
71
|
+
expect(info.lastName).toBe('Hopper');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('falls back to the single name token for both first and last when name has one word', () => {
|
|
75
|
+
const provider = makeProvider();
|
|
76
|
+
const payload: JwtPayload = { sub: 'user_1', email: 'cher@example.com', name: 'Cher' };
|
|
77
|
+
|
|
78
|
+
const info = provider.extractUserInfo(payload);
|
|
79
|
+
|
|
80
|
+
expect(info.firstName).toBe('Cher');
|
|
81
|
+
expect(info.lastName).toBe('Cher');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('prefers explicit given_name/family_name over the `name` split', () => {
|
|
85
|
+
const provider = makeProvider();
|
|
86
|
+
const payload: JwtPayload = {
|
|
87
|
+
sub: 'user_2',
|
|
88
|
+
email: 'jean.luc@example.com',
|
|
89
|
+
name: 'Wrong Split',
|
|
90
|
+
given_name: 'Jean-Luc',
|
|
91
|
+
family_name: 'Picard',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const info = provider.extractUserInfo(payload);
|
|
95
|
+
|
|
96
|
+
expect(info.firstName).toBe('Jean-Luc');
|
|
97
|
+
expect(info.lastName).toBe('Picard');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('honors an explicit preferred_username when present', () => {
|
|
101
|
+
const provider = makeProvider();
|
|
102
|
+
const payload: JwtPayload = {
|
|
103
|
+
sub: 'user_3',
|
|
104
|
+
email: 'alan@example.com',
|
|
105
|
+
preferred_username: 'aturing',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const info = provider.extractUserInfo(payload);
|
|
109
|
+
|
|
110
|
+
expect(info.preferredUsername).toBe('aturing');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns undefined email (rather than throwing) when no JWT Template added it', () => {
|
|
114
|
+
// This is the default WorkOS access token shape — identity/session claims only.
|
|
115
|
+
const provider = makeProvider();
|
|
116
|
+
const payload: JwtPayload = {
|
|
117
|
+
sub: 'user_4',
|
|
118
|
+
sid: 'session_01H',
|
|
119
|
+
org_id: 'org_01H',
|
|
120
|
+
role: 'admin',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const info = provider.extractUserInfo(payload);
|
|
124
|
+
|
|
125
|
+
expect(info.email).toBeUndefined();
|
|
126
|
+
expect(info.userId).toBe('user_4');
|
|
127
|
+
expect(info.preferredUsername).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('validateConfig', () => {
|
|
132
|
+
it('is valid with the full WorkOS config (base fields + clientId)', () => {
|
|
133
|
+
expect(makeProvider().validateConfig()).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('is invalid without a clientId', () => {
|
|
137
|
+
expect(makeProvider({ clientId: undefined }).validateConfig()).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('is invalid when a base field (audience) is missing', () => {
|
|
141
|
+
expect(makeProvider({ audience: '' }).validateConfig()).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('exposes clientId on the instance for the OAuth proxy path', () => {
|
|
145
|
+
expect(makeProvider().clientId).toBe(CLIENT_ID);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('class-factory registration', () => {
|
|
150
|
+
it('is registered under the lowercase `workos` key and instantiable via the factory', () => {
|
|
151
|
+
const instance = MJGlobal.Instance.ClassFactory.CreateInstance<BaseAuthProvider>(
|
|
152
|
+
BaseAuthProvider,
|
|
153
|
+
'workos',
|
|
154
|
+
makeConfig(),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(instance).toBeInstanceOf(WorkOSProvider);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('issuer matching (inherited)', () => {
|
|
162
|
+
it('matches its configured issuer case-insensitively and ignores a trailing slash', () => {
|
|
163
|
+
const provider = makeProvider();
|
|
164
|
+
expect(provider.matchesIssuer(`https://api.workos.com/user_management/${CLIENT_ID}`)).toBe(true);
|
|
165
|
+
expect(provider.matchesIssuer(`https://api.workos.com/user_management/${CLIENT_ID}/`)).toBe(true);
|
|
166
|
+
expect(provider.matchesIssuer('https://api.workos.com/user_management/other')).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,11 @@ export { BaseAuthProvider } from './BaseAuthProvider.js';
|
|
|
3
3
|
export { AuthProviderFactory } from './AuthProviderFactory.js';
|
|
4
4
|
export { TokenExpiredError } from './tokenExpiredError.js';
|
|
5
5
|
export { MagicLinkProvider } from './providers/MagicLinkProvider.js';
|
|
6
|
+
export {
|
|
7
|
+
HostIdentityProvider,
|
|
8
|
+
type HostAssertionVerifyResult,
|
|
9
|
+
type HostAssertionError,
|
|
10
|
+
} from './providers/HostIdentityProvider.js';
|
|
6
11
|
|
|
7
12
|
// Re-export types consumers commonly need alongside the auth providers
|
|
8
13
|
export type { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import jwt, { JwtPayload } from 'jsonwebtoken';
|
|
2
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
3
|
+
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
4
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
5
|
+
|
|
6
|
+
/** Why a host-assertion verification failed (mirrors the public web-widget mint error codes). */
|
|
7
|
+
export type HostAssertionError = 'missing' | 'bad_signature' | 'expired' | 'no_email' | 'no_key';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Absolute ceiling on a host assertion's age (measured from its `iat`), independent of the host-chosen
|
|
11
|
+
* `exp`. Even if a host mints a long-lived — or `exp`-less — assertion, MJ refuses to accept it beyond
|
|
12
|
+
* this window. Enforces the "short-lived" contract the strategy assumes rather than trusting the host to
|
|
13
|
+
* honor it. Effective lifetime is therefore min(assertion `exp`, `iat` + this).
|
|
14
|
+
*/
|
|
15
|
+
const HOST_ASSERTION_MAX_AGE_SECONDS = 600; // 10 minutes
|
|
16
|
+
|
|
17
|
+
/** Result of verifying a host-signed identity assertion. `userInfo` is present iff `ok` is true. */
|
|
18
|
+
export interface HostAssertionVerifyResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
userInfo?: AuthUserInfo;
|
|
21
|
+
/** The host's opaque user id for the visitor (assertion `sub`) — audit correlation, not an MJ user id. */
|
|
22
|
+
hostUserId?: string;
|
|
23
|
+
errorCode?: HostAssertionError;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Authentication provider for the public web-widget **host-identity** strategy (D1).
|
|
28
|
+
*
|
|
29
|
+
* When a widget is embedded in an already-authenticated host portal, the host signs a short-lived
|
|
30
|
+
* RS256 assertion (with ITS OWN key) describing the visitor. Unlike Auth0/magic-link tokens — validated
|
|
31
|
+
* by the standard issuer→JWKS path — a host assertion is verified against a STATIC per-widget public key
|
|
32
|
+
* (stored on `WidgetInstance.HostPublicKey`) and exchanged at `POST /widget/session` for an MJ guest JWT.
|
|
33
|
+
* So this provider does NOT use the JWKS machinery: it exposes {@link VerifyHostAssertion} (static-PEM
|
|
34
|
+
* RS256 verification) + {@link extractUserInfo}, and the widget mint resolves it via the ClassFactory.
|
|
35
|
+
*
|
|
36
|
+
* SECURITY INVARIANT — key/assertion provenance MUST be independent. The party that presents a signed
|
|
37
|
+
* assertion must NOT also be able to provide or influence the public key used to verify it. If one party
|
|
38
|
+
* controlled both, the signature would prove nothing: an attacker could mint a fresh keypair, sign any
|
|
39
|
+
* claims with the private key, hand over the matching public key, and the check would trivially pass. The
|
|
40
|
+
* verifying key must therefore come from an independent trust path — here, out-of-band administrator
|
|
41
|
+
* configuration on `WidgetInstance.HostPublicKey` (the analog, for a static PEM, of pinning a trusted
|
|
42
|
+
* issuer's JWKS endpoint). That is why {@link VerifyHostAssertion} takes the PEM as a caller-supplied
|
|
43
|
+
* argument rather than reading any key material from the assertion itself: the caller (the widget mint)
|
|
44
|
+
* is responsible for sourcing the PEM from the registered widget record, never from the request payload.
|
|
45
|
+
*
|
|
46
|
+
* It still subclasses {@link BaseAuthProvider} (and registers in the same factory) so host-identity is a
|
|
47
|
+
* first-class, discoverable provider rather than ad-hoc mint code — the architecture the widget plan calls
|
|
48
|
+
* for. The base constructor builds a JWKS client from `jwksUri`; host-identity has no JWKS, so callers pass
|
|
49
|
+
* a placeholder `jwksUri` that is never contacted (verification goes through {@link VerifyHostAssertion}).
|
|
50
|
+
*/
|
|
51
|
+
@RegisterClass(BaseAuthProvider, 'host-identity')
|
|
52
|
+
export class HostIdentityProvider extends BaseAuthProvider {
|
|
53
|
+
constructor(config: AuthProviderConfig) {
|
|
54
|
+
super(config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Host identity never uses JWKS — config is valid as long as it names the provider. */
|
|
58
|
+
override validateConfig(): boolean {
|
|
59
|
+
return !!this.name;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Verifies a host-signed RS256 assertion against the host's STATIC public key (PEM) and extracts the
|
|
64
|
+
* asserted visitor identity. The assertion's `aud` must equal `expectedAudience` (the widget key, bound
|
|
65
|
+
* at the host) and it must carry an `email`. It must also be short-lived: it must declare its own `exp`
|
|
66
|
+
* AND be no older than {@link HOST_ASSERTION_MAX_AGE_SECONDS} since `iat` — so a misbehaving host cannot
|
|
67
|
+
* mint an unbounded token. Never throws — returns a structured result.
|
|
68
|
+
*
|
|
69
|
+
* SECURITY: `hostPublicKeyPem` MUST originate from an independent trust path (the registered
|
|
70
|
+
* `WidgetInstance.HostPublicKey`, set out-of-band by an administrator) — NEVER from the same request
|
|
71
|
+
* that carries `assertion`. Passing a key the assertion's presenter could influence defeats the
|
|
72
|
+
* signature check entirely (see the class-level SECURITY INVARIANT note).
|
|
73
|
+
*/
|
|
74
|
+
VerifyHostAssertion(
|
|
75
|
+
assertion: string | undefined,
|
|
76
|
+
hostPublicKeyPem: string | undefined,
|
|
77
|
+
expectedAudience: string,
|
|
78
|
+
): HostAssertionVerifyResult {
|
|
79
|
+
if (!assertion) {
|
|
80
|
+
return { ok: false, errorCode: 'missing' };
|
|
81
|
+
}
|
|
82
|
+
if (!hostPublicKeyPem) {
|
|
83
|
+
return { ok: false, errorCode: 'no_key' };
|
|
84
|
+
}
|
|
85
|
+
let payload: JwtPayload;
|
|
86
|
+
try {
|
|
87
|
+
// maxAge caps the assertion's age from `iat` regardless of the host-chosen `exp` (and requires `iat`
|
|
88
|
+
// to be present); jsonwebtoken surfaces a breach as TokenExpiredError, same as a normal expiry.
|
|
89
|
+
payload = jwt.verify(assertion, hostPublicKeyPem, {
|
|
90
|
+
algorithms: ['RS256'],
|
|
91
|
+
audience: expectedAudience,
|
|
92
|
+
maxAge: HOST_ASSERTION_MAX_AGE_SECONDS,
|
|
93
|
+
}) as JwtPayload;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// jsonwebtoken throws TokenExpiredError for expiry / maxAge; everything else is a signature/format fault.
|
|
96
|
+
return { ok: false, errorCode: e instanceof jwt.TokenExpiredError ? 'expired' : 'bad_signature' };
|
|
97
|
+
}
|
|
98
|
+
// Require an explicit expiry: we refuse to treat an unbounded assertion as valid even inside the maxAge
|
|
99
|
+
// window, so the host's "short-lived" contract is enforced, not merely assumed.
|
|
100
|
+
if (typeof payload.exp !== 'number') {
|
|
101
|
+
return { ok: false, errorCode: 'expired' };
|
|
102
|
+
}
|
|
103
|
+
const userInfo = this.extractUserInfo(payload);
|
|
104
|
+
if (!userInfo.email) {
|
|
105
|
+
return { ok: false, errorCode: 'no_email' };
|
|
106
|
+
}
|
|
107
|
+
const sub = typeof payload.sub === 'string' ? payload.sub : undefined;
|
|
108
|
+
return { ok: true, userInfo, hostUserId: sub };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Maps a verified host-assertion payload to standard MJ identity fields. */
|
|
112
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo {
|
|
113
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined);
|
|
114
|
+
const email = str(payload.email);
|
|
115
|
+
const firstName = str(payload.given_name) ?? str(payload['firstName']);
|
|
116
|
+
const lastName = str(payload.family_name) ?? str(payload['lastName']);
|
|
117
|
+
return {
|
|
118
|
+
email,
|
|
119
|
+
firstName,
|
|
120
|
+
lastName,
|
|
121
|
+
fullName: str(payload.name) ?? ([firstName, lastName].filter(Boolean).join(' ') || undefined),
|
|
122
|
+
preferredUsername: email,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { JwtPayload } from 'jsonwebtoken';
|
|
2
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
3
|
+
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
4
|
+
import { BaseAuthProvider } from '../BaseAuthProvider.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WorkOS (AuthKit) authentication provider implementation.
|
|
9
|
+
*
|
|
10
|
+
* Validates JWT access tokens minted by WorkOS AuthKit / User Management. These tokens are
|
|
11
|
+
* signed with RS256 and verified against the per-environment JWKS endpoint
|
|
12
|
+
* (`https://api.workos.com/sso/jwks/<clientId>`); their `iss` claim is
|
|
13
|
+
* `https://api.workos.com/user_management/<clientId>`.
|
|
14
|
+
*
|
|
15
|
+
* ## Email is required — and is NOT in a WorkOS token by default
|
|
16
|
+
* MemberJunction resolves the signed-in user by **email**, but a WorkOS AuthKit access token
|
|
17
|
+
* only carries identity/session claims (`sub`, `sid`, `org_id`, `role`, `permissions`) out of
|
|
18
|
+
* the box — it does **not** include `email`. To use WorkOS with MJ you must add the email (and
|
|
19
|
+
* ideally the name) to the token via a WorkOS **JWT Template**:
|
|
20
|
+
*
|
|
21
|
+
* ```json
|
|
22
|
+
* {
|
|
23
|
+
* "email": "{{user.email}}",
|
|
24
|
+
* "given_name": "{{user.first_name}}",
|
|
25
|
+
* "family_name": "{{user.last_name}}"
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* See `WORKOS.md` in this package for the full, step-by-step integration guide.
|
|
30
|
+
*/
|
|
31
|
+
@RegisterClass(BaseAuthProvider, 'workos')
|
|
32
|
+
export class WorkOSProvider extends BaseAuthProvider {
|
|
33
|
+
constructor(config: AuthProviderConfig) {
|
|
34
|
+
super(config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts user information from a WorkOS AuthKit JWT payload.
|
|
39
|
+
*
|
|
40
|
+
* `email`, `given_name`, and `family_name` are expected to be supplied by a WorkOS JWT
|
|
41
|
+
* Template (see class docs). `sub` is the stable WorkOS user id (`user_...`). We fall back
|
|
42
|
+
* through standard OIDC claim names so a token shaped by a custom template (e.g. one that
|
|
43
|
+
* only sets `name`) still resolves a usable identity.
|
|
44
|
+
*/
|
|
45
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo {
|
|
46
|
+
const email = payload.email as string | undefined;
|
|
47
|
+
const fullName = payload.name as string | undefined;
|
|
48
|
+
const firstName = payload.given_name as string | undefined;
|
|
49
|
+
const lastName = payload.family_name as string | undefined;
|
|
50
|
+
// WorkOS access tokens have no `preferred_username`; email is the natural handle.
|
|
51
|
+
const preferredUsername = payload.preferred_username as string | undefined || email;
|
|
52
|
+
// `sub` is the WorkOS user identifier (e.g. "user_01H...").
|
|
53
|
+
const userId = payload.sub as string | undefined;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
email,
|
|
57
|
+
firstName: firstName || fullName?.split(' ')[0],
|
|
58
|
+
lastName: lastName || fullName?.split(' ')[1] || fullName?.split(' ')[0],
|
|
59
|
+
fullName,
|
|
60
|
+
preferredUsername,
|
|
61
|
+
userId
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates WorkOS-specific configuration.
|
|
67
|
+
*
|
|
68
|
+
* Beyond the base requirements (`name`, `issuer`, `audience`, `jwksUri`), WorkOS needs a
|
|
69
|
+
* `clientId` — it appears in both the issuer and JWKS URLs and identifies the AuthKit
|
|
70
|
+
* environment.
|
|
71
|
+
*/
|
|
72
|
+
validateConfig(): boolean {
|
|
73
|
+
const baseValid = super.validateConfig();
|
|
74
|
+
const hasClientId = !!this.config.clientId;
|
|
75
|
+
|
|
76
|
+
return baseValid && hasClientId;
|
|
77
|
+
}
|
|
78
|
+
}
|