@memberjunction/auth-providers 5.30.0 → 5.31.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 +22 -0
- package/README.md +301 -0
- package/package.json +3 -3
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @memberjunction/auth-providers
|
|
2
2
|
|
|
3
|
+
## 5.31.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7ed7a4b: no metadata/migration changes
|
|
8
|
+
- Updated dependencies [7ed7a4b]
|
|
9
|
+
- Updated dependencies [60e7541]
|
|
10
|
+
- Updated dependencies [18be074]
|
|
11
|
+
- Updated dependencies [17b8087]
|
|
12
|
+
- Updated dependencies [6779c1e]
|
|
13
|
+
- Updated dependencies [de34786]
|
|
14
|
+
- Updated dependencies [5db36d9]
|
|
15
|
+
- @memberjunction/core@5.31.0
|
|
16
|
+
- @memberjunction/global@5.31.0
|
|
17
|
+
|
|
18
|
+
## 5.30.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- @memberjunction/core@5.30.1
|
|
23
|
+
- @memberjunction/global@5.30.1
|
|
24
|
+
|
|
3
25
|
## 5.30.0
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# @memberjunction/auth-providers
|
|
2
|
+
|
|
3
|
+
Authentication provider interfaces, base classes, and ready-made implementations for validating JWTs from any OAuth 2.0 / OIDC compliant identity provider in MemberJunction.
|
|
4
|
+
|
|
5
|
+
This package gives the MJ server (and any other Node.js consumer) a uniform, pluggable way to:
|
|
6
|
+
|
|
7
|
+
- Resolve a JWT's signing key from a remote JWKS endpoint
|
|
8
|
+
- Verify the issuer and audience of incoming tokens
|
|
9
|
+
- Extract a normalized `AuthUserInfo` from provider-specific claim shapes
|
|
10
|
+
- Register additional providers at runtime via the MJ class-factory system
|
|
11
|
+
|
|
12
|
+
It ships with first-class support for **Auth0**, **Microsoft Entra ID / MSAL**, **Okta**, **AWS Cognito**, and **Google Identity Platform**, and is the extension point used to plug custom providers into [`@memberjunction/server`](../MJServer/README.md).
|
|
13
|
+
|
|
14
|
+
## When to use this package
|
|
15
|
+
|
|
16
|
+
Use `@memberjunction/auth-providers` when you are:
|
|
17
|
+
|
|
18
|
+
- Running an MJ GraphQL server and need to validate user JWTs (this is wired up automatically by [`@memberjunction/server`](../MJServer/README.md))
|
|
19
|
+
- Building a custom Node.js service that needs to validate tokens issued for an MJ tenant
|
|
20
|
+
- Adding support for a new identity provider that is not in the built-in list above
|
|
21
|
+
- Implementing an MCP server or other backend that participates in MJ's auth flow (see [`@memberjunction/ai-mcp-server`](../AI/MCPServer))
|
|
22
|
+
|
|
23
|
+
You generally do **not** need this package directly in browser / Angular code — the front-end auth flow is handled by provider SDKs (MSAL.js, Auth0 SPA SDK, etc.) and the resulting access token is sent to MJ APIs, where this package validates it server-side.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @memberjunction/auth-providers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This package is a Node.js / server-side package. It depends on:
|
|
32
|
+
|
|
33
|
+
- [`@memberjunction/core`](../MJCore/readme.md) — provides `AuthProviderConfig` and `AuthUserInfo` types
|
|
34
|
+
- [`@memberjunction/global`](../MJGlobal/README.md) — provides `BaseSingleton`, `MJGlobal`, and `@RegisterClass`
|
|
35
|
+
- `jsonwebtoken` — JWT primitives
|
|
36
|
+
- `jwks-rsa` — JWKS key retrieval with caching
|
|
37
|
+
- `graphql` — used by `TokenExpiredError` to surface a typed GraphQL error
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ @memberjunction/server │
|
|
44
|
+
│ │
|
|
45
|
+
│ incoming request ──▶ JWT extracted ──▶ getSigningKeys(issuer) │
|
|
46
|
+
│ │ │
|
|
47
|
+
└──────────────────────────────────────────────┼───────────────────┘
|
|
48
|
+
▼
|
|
49
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ @memberjunction/auth-providers │
|
|
51
|
+
│ │
|
|
52
|
+
│ AuthProviderFactory (singleton) │
|
|
53
|
+
│ │ │
|
|
54
|
+
│ ├── getByIssuer(iss) ──▶ IAuthProvider │
|
|
55
|
+
│ │ │ │
|
|
56
|
+
│ │ ├── getSigningKey() │
|
|
57
|
+
│ │ │ (jwks-rsa + retry) │
|
|
58
|
+
│ │ │ │
|
|
59
|
+
│ │ └── extractUserInfo() │
|
|
60
|
+
│ │ │
|
|
61
|
+
│ └── createProvider(config) │
|
|
62
|
+
│ │ │
|
|
63
|
+
│ ▼ │
|
|
64
|
+
│ MJGlobal.ClassFactory │
|
|
65
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'auth0') │
|
|
66
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'msal') │
|
|
67
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'okta') │
|
|
68
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'cognito') │
|
|
69
|
+
│ ├─ @RegisterClass(BaseAuthProvider, 'google') │
|
|
70
|
+
│ └─ @RegisterClass(BaseAuthProvider, 'your-custom') │
|
|
71
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Key pieces
|
|
75
|
+
|
|
76
|
+
| Export | Role |
|
|
77
|
+
| ------------------------- | -------------------------------------------------------------------------- |
|
|
78
|
+
| `IAuthProvider` | Contract every provider must satisfy |
|
|
79
|
+
| `BaseAuthProvider` | Abstract base class — handles JWKS, retries, issuer matching |
|
|
80
|
+
| `AuthProviderFactory` | Singleton registry + factory; resolves providers by issuer or name |
|
|
81
|
+
| `TokenExpiredError` | `GraphQLError` subclass with `JWT_EXPIRED` code and `expiryDate` extension |
|
|
82
|
+
| `AuthProviderConfig` | Re-exported config shape (defined in `@memberjunction/core`) |
|
|
83
|
+
| `AuthUserInfo` | Re-exported normalized user shape (defined in `@memberjunction/core`) |
|
|
84
|
+
|
|
85
|
+
`AuthProviderFactory` extends [`BaseSingleton<T>`](../MJGlobal/README.md) — the global object store guarantees a single instance even if the bundler duplicates the module across execution paths.
|
|
86
|
+
|
|
87
|
+
## Built-in providers
|
|
88
|
+
|
|
89
|
+
Each built-in provider is registered with the MJ class factory under a lowercase type key. Set `type` in your config to one of these values to instantiate the matching provider.
|
|
90
|
+
|
|
91
|
+
| Type key | Class | Required config (in addition to the base set) |
|
|
92
|
+
| ---------- | ----------------- | -------------------------------------------------------- |
|
|
93
|
+
| `auth0` | `Auth0Provider` | `clientId`, `domain` |
|
|
94
|
+
| `msal` | `MSALProvider` | `clientId`, `tenantId` |
|
|
95
|
+
| `okta` | `OktaProvider` | `clientId`, `domain` |
|
|
96
|
+
| `cognito` | `CognitoProvider` | `clientId`, `region`, `userPoolId` |
|
|
97
|
+
| `google` | `GoogleProvider` | `clientId` |
|
|
98
|
+
|
|
99
|
+
Every provider also requires the base fields: `name`, `type`, `issuer`, `audience`, `jwksUri`. See [`AuthProviderConfig`](../MJCore/src/generic/authTypes.ts) for the full shape.
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
In an MJ server, providers are configured under `authProviders` in `mj.config.cjs`. Multiple providers can be registered concurrently — the factory dispatches incoming tokens to the right one based on the `iss` claim.
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
// mj.config.cjs
|
|
107
|
+
module.exports = {
|
|
108
|
+
authProviders: [
|
|
109
|
+
{
|
|
110
|
+
name: 'corporate-azure-ad',
|
|
111
|
+
type: 'msal',
|
|
112
|
+
clientId: process.env.AZURE_CLIENT_ID,
|
|
113
|
+
tenantId: process.env.AZURE_TENANT_ID,
|
|
114
|
+
issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
|
|
115
|
+
audience: process.env.AZURE_CLIENT_ID,
|
|
116
|
+
jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'customer-auth0',
|
|
120
|
+
type: 'auth0',
|
|
121
|
+
clientId: process.env.AUTH0_CLIENT_ID,
|
|
122
|
+
domain: 'tenant.auth0.com',
|
|
123
|
+
issuer: 'https://tenant.auth0.com/',
|
|
124
|
+
audience: 'https://api.example.com',
|
|
125
|
+
jwksUri: 'https://tenant.auth0.com/.well-known/jwks.json',
|
|
126
|
+
},
|
|
127
|
+
// ...add more providers here
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> **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
|
+
|
|
134
|
+
## Usage
|
|
135
|
+
|
|
136
|
+
### In MJServer (the typical case)
|
|
137
|
+
|
|
138
|
+
You do not call this package directly when using [`@memberjunction/server`](../MJServer/README.md). The server runs `initializeAuthProviders()` at startup, which reads `authProviders` from your config and registers each one with the factory. The GraphQL middleware then uses the factory automatically.
|
|
139
|
+
|
|
140
|
+
### Direct programmatic use
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
AuthProviderFactory,
|
|
145
|
+
TokenExpiredError,
|
|
146
|
+
} from '@memberjunction/auth-providers';
|
|
147
|
+
import jwt, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
|
|
148
|
+
|
|
149
|
+
// One-time setup at app boot
|
|
150
|
+
const factory = AuthProviderFactory.Instance;
|
|
151
|
+
|
|
152
|
+
const provider = AuthProviderFactory.createProvider({
|
|
153
|
+
name: 'main-auth0',
|
|
154
|
+
type: 'auth0',
|
|
155
|
+
clientId: process.env.AUTH0_CLIENT_ID!,
|
|
156
|
+
domain: 'tenant.auth0.com',
|
|
157
|
+
issuer: 'https://tenant.auth0.com/',
|
|
158
|
+
audience: 'https://api.example.com',
|
|
159
|
+
jwksUri: 'https://tenant.auth0.com/.well-known/jwks.json',
|
|
160
|
+
});
|
|
161
|
+
factory.register(provider);
|
|
162
|
+
|
|
163
|
+
// Per-request token validation
|
|
164
|
+
function validate(token: string) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
// First decode (without verifying) to read the issuer claim
|
|
167
|
+
const decoded = jwt.decode(token, { complete: true });
|
|
168
|
+
const issuer = decoded?.payload && typeof decoded.payload === 'object'
|
|
169
|
+
? (decoded.payload as jwt.JwtPayload).iss
|
|
170
|
+
: undefined;
|
|
171
|
+
if (!issuer) return reject(new Error('Token missing iss claim'));
|
|
172
|
+
|
|
173
|
+
const matched = factory.getByIssuer(issuer);
|
|
174
|
+
if (!matched) return reject(new Error(`Unknown issuer: ${issuer}`));
|
|
175
|
+
|
|
176
|
+
jwt.verify(
|
|
177
|
+
token,
|
|
178
|
+
(header: JwtHeader, cb: SigningKeyCallback) => matched.getSigningKey(header, cb),
|
|
179
|
+
{ issuer: matched.issuer, audience: matched.audience },
|
|
180
|
+
(err, payload) => {
|
|
181
|
+
if (err?.name === 'TokenExpiredError') {
|
|
182
|
+
return reject(new TokenExpiredError(new Date((err as jwt.TokenExpiredError).expiredAt)));
|
|
183
|
+
}
|
|
184
|
+
if (err || !payload || typeof payload !== 'object') return reject(err);
|
|
185
|
+
resolve({
|
|
186
|
+
payload,
|
|
187
|
+
user: matched.extractUserInfo(payload as jwt.JwtPayload),
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Building a custom provider
|
|
196
|
+
|
|
197
|
+
Custom providers extend `BaseAuthProvider` and register themselves with the MJ class factory. Once registered, they're instantiable by `type` like any built-in provider.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { JwtPayload } from 'jsonwebtoken';
|
|
201
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
202
|
+
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
|
|
203
|
+
import { BaseAuthProvider } from '@memberjunction/auth-providers';
|
|
204
|
+
|
|
205
|
+
@RegisterClass(BaseAuthProvider, 'keycloak')
|
|
206
|
+
export class KeycloakProvider extends BaseAuthProvider {
|
|
207
|
+
constructor(config: AuthProviderConfig) {
|
|
208
|
+
super(config);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo {
|
|
212
|
+
return {
|
|
213
|
+
email: payload.email as string | undefined,
|
|
214
|
+
firstName: payload.given_name as string | undefined,
|
|
215
|
+
lastName: payload.family_name as string | undefined,
|
|
216
|
+
fullName: payload.name as string | undefined,
|
|
217
|
+
preferredUsername: payload.preferred_username as string | undefined,
|
|
218
|
+
roles: (payload.realm_access as { roles?: string[] } | undefined)?.roles,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
validateConfig(): boolean {
|
|
223
|
+
return super.validateConfig() && !!this.config.clientId;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Then in mj.config.cjs use type: 'keycloak'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
> **Important:** because the provider is loaded via class-factory metadata and not by direct reference, your bundler may tree-shake it out. Make sure the file containing the `@RegisterClass` decorator is imported (directly or transitively) before `AuthProviderFactory.createProvider()` runs. The built-in providers do this from [`AuthProviderFactory.ts`](src/AuthProviderFactory.ts); follow the same pattern in your own entry point or a manifest. See the class-registration manifest discussion in the [root project guide](../../CLAUDE.md) for background.
|
|
231
|
+
|
|
232
|
+
## API reference
|
|
233
|
+
|
|
234
|
+
### `IAuthProvider`
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
interface IAuthProvider {
|
|
238
|
+
name: string;
|
|
239
|
+
issuer: string;
|
|
240
|
+
audience: string;
|
|
241
|
+
jwksUri: string;
|
|
242
|
+
clientId?: string;
|
|
243
|
+
validateConfig(): boolean;
|
|
244
|
+
getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void;
|
|
245
|
+
extractUserInfo(payload: JwtPayload): AuthUserInfo;
|
|
246
|
+
matchesIssuer(issuer: string): boolean;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### `BaseAuthProvider`
|
|
251
|
+
|
|
252
|
+
Abstract class that implements all of `IAuthProvider` except `extractUserInfo`. It also handles:
|
|
253
|
+
|
|
254
|
+
- A keep-alive HTTP(S) agent for the JWKS client (50 max sockets, 60s timeout)
|
|
255
|
+
- JWKS response caching (5 entries, 10 minute TTL)
|
|
256
|
+
- Up to **3 retries with exponential backoff** for JWKS fetches on common transient errors (`socket hang up`, `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, `EAI_AGAIN`)
|
|
257
|
+
- Case-insensitive, trailing-slash-tolerant issuer matching
|
|
258
|
+
|
|
259
|
+
Subclasses must implement `extractUserInfo(payload)` and may override `validateConfig()` to enforce provider-specific required fields.
|
|
260
|
+
|
|
261
|
+
### `AuthProviderFactory`
|
|
262
|
+
|
|
263
|
+
Singleton registry/factory. All instance methods are on `AuthProviderFactory.Instance`; `createProvider`, `getRegisteredProviderTypes`, and `isProviderTypeRegistered` are static helpers.
|
|
264
|
+
|
|
265
|
+
| Method | Purpose |
|
|
266
|
+
| ------------------------------------------ | -------------------------------------------------------------------- |
|
|
267
|
+
| `static createProvider(config)` | Creates a provider via `MJGlobal.ClassFactory` from `config.type` |
|
|
268
|
+
| `register(provider)` | Validates and adds a provider; clears issuer caches |
|
|
269
|
+
| `getByIssuer(iss)` | Returns the first provider whose issuer matches (cached) |
|
|
270
|
+
| `getAllByIssuer(iss)` | Returns **all** providers for an issuer (multi-app / multi-audience) |
|
|
271
|
+
| `getByName(name)` | Lookup by configured `name` |
|
|
272
|
+
| `getAllProviders()` | All registered providers |
|
|
273
|
+
| `hasProviders()` | Quick boolean check |
|
|
274
|
+
| `clear()` | Drop all providers and caches (used in tests) |
|
|
275
|
+
| `static getRegisteredProviderTypes()` | All `type` keys registered with the class factory |
|
|
276
|
+
| `static isProviderTypeRegistered(type)` | Whether a given `type` key resolves to a registration |
|
|
277
|
+
|
|
278
|
+
### `TokenExpiredError`
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
new TokenExpiredError(expiryDate: Date, message?: string)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A `GraphQLError` with `extensions.code = 'JWT_EXPIRED'` and `extensions.expiryDate` set to the ISO string of the expiry. Throw this from GraphQL resolvers when you detect an expired token so clients can branch on the error code and trigger a silent refresh.
|
|
285
|
+
|
|
286
|
+
## Related packages
|
|
287
|
+
|
|
288
|
+
- [`@memberjunction/core`](../MJCore/readme.md) — defines `AuthProviderConfig`, `AuthUserInfo`, `AuthTokenInfo`, `AuthJwtPayload` and the `AUTH_PROVIDER_TYPES` constants
|
|
289
|
+
- [`@memberjunction/global`](../MJGlobal/README.md) — provides `BaseSingleton`, `@RegisterClass`, and the `MJGlobal.ClassFactory` that drives provider instantiation
|
|
290
|
+
- [`@memberjunction/server`](../MJServer/README.md) — primary consumer; wires this package into the GraphQL request pipeline
|
|
291
|
+
- [`@memberjunction/ai-mcp-server`](../AI/MCPServer) — uses this package to validate tokens on MCP transport endpoints (see [MCP OAuth spec](../../specs/601-mcp-oauth/spec.md))
|
|
292
|
+
- [`@memberjunction/api-keys-base`](../APIKeys/Base) and [`@memberjunction/api-keys-engine`](../APIKeys/Engine) — complementary auth path for non-interactive (machine-to-machine) callers
|
|
293
|
+
|
|
294
|
+
## Project guidelines
|
|
295
|
+
|
|
296
|
+
- Class registration & tree-shaking caveats: see the manifest section of the [root project guide](../../CLAUDE.md)
|
|
297
|
+
- Singleton best practices: see the `BaseSingleton` rules in the [root project guide](../../CLAUDE.md)
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
ISC — part of the [MemberJunction](https://github.com/MemberJunction/MJ) monorepo.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/auth-providers",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.31.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.31.0",
|
|
15
|
+
"@memberjunction/global": "5.31.0",
|
|
16
16
|
"graphql": "^16.12.0",
|
|
17
17
|
"jsonwebtoken": "9.0.3",
|
|
18
18
|
"jwks-rsa": "^3.2.2"
|