Authentication provider interfaces, base classes, and ready-made implementations for validating JWTs from any OAuth 2.0 / OIDC compliant identity provider in MemberJunction.
This package gives the MJ server (and any other Node.js consumer) a uniform, pluggable way to:
AuthUserInfo from provider-specific claim shapesIt 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.
Integrating WorkOS? See the dedicated end-to-end guide: WORKOS.md — it covers the browser + server setup and the two WorkOS-specific gotchas (the required
audclaim).
Use @memberjunction/auth-providers when you are:
@memberjunction/server)@memberjunction/ai-mcp-server)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.
npm install @memberjunction/auth-providers
This package is a Node.js / server-side package. It depends on:
@memberjunction/core — provides AuthProviderConfig and AuthUserInfo types@memberjunction/global — provides BaseSingleton, MJGlobal, and @RegisterClassjsonwebtoken — JWT primitivesjwks-rsa — JWKS key retrieval with cachinggraphql — used by TokenExpiredError to surface a typed GraphQL error┌──────────────────────────────────────────────────────────────────┐
│ @memberjunction/server │
│ │
│ incoming request ──▶ JWT extracted ──▶ getSigningKeys(issuer) │
│ │ │
└──────────────────────────────────────────────┼───────────────────┘
▼
┌──────────────────────────────────────────────────────────────────┐
│ @memberjunction/auth-providers │
│ │
│ AuthProviderFactory (singleton) │
│ │ │
│ ├── getByIssuer(iss) ──▶ IAuthProvider │
│ │ │ │
│ │ ├── getSigningKey() │
│ │ │ (jwks-rsa + retry) │
│ │ │ │
│ │ └── extractUserInfo() │
│ │ │
│ └── createProvider(config) │
│ │ │
│ ▼ │
│ MJGlobal.ClassFactory │
│ ├─ @RegisterClass(BaseAuthProvider, 'auth0') │
│ ├─ @RegisterClass(BaseAuthProvider, 'msal') │
│ ├─ @RegisterClass(BaseAuthProvider, 'okta') │
│ ├─ @RegisterClass(BaseAuthProvider, 'cognito') │
│ ├─ @RegisterClass(BaseAuthProvider, 'google') │
│ ├─ @RegisterClass(BaseAuthProvider, 'workos') │
│ └─ @RegisterClass(BaseAuthProvider, 'your-custom') │
└──────────────────────────────────────────────────────────────────┘
| Export | Role |
|---|---|
IAuthProvider |
Contract every provider must satisfy |
BaseAuthProvider |
Abstract base class — handles JWKS, retries, issuer matching |
AuthProviderFactory |
Singleton registry + factory; resolves providers by issuer or name |
TokenExpiredError |
GraphQLError subclass with JWT_EXPIRED code and expiryDate extension |
AuthProviderConfig |
Re-exported config shape (defined in @memberjunction/core) |
AuthUserInfo |
Re-exported normalized user shape (defined in @memberjunction/core) |
AuthProviderFactory extends BaseSingleton<T> — the global object store guarantees a single instance even if the bundler duplicates the module across execution paths.
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.
| Type key | Class | Required config (in addition to the base set) |
|---|---|---|
auth0 |
Auth0Provider |
clientId, domain |
msal |
MSALProvider |
clientId, tenantId |
okta |
OktaProvider |
clientId, domain |
cognito |
CognitoProvider |
clientId, region, userPoolId |
google |
GoogleProvider |
clientId |
workos |
WorkOSProvider |
clientId (see WORKOS.md for the required email JWT Template + aud) |
Every provider also requires the base fields: name, type, issuer, audience, jwksUri. See AuthProviderConfig for the full shape.
The list above is not a closed set — it is what MJ happens to ship.
typeis resolved through the class factory, so any@RegisterClass(BaseAuthProvider, 'your-key')subclass is instantiable the same way, with no change to this package. Nothing validatestypeagainst an enum.
A provider config can reach the factory from three places. They layer rather than compete, and none is mandatory:
| Source | Set by | Notes |
|---|---|---|
| Environment variables | Each provider class's optional ConfigFromEnvironment static |
The zero-config path. See below. |
mj.config.cjs |
The authProviders array |
Explicit and exact. Declaring it replaces the environment-derived set rather than merging. |
MJ: Authentication Providers metadata |
Rows in the database | Enables admin-managed configuration and the multi-IdP login picker. Layered on top at startup by MJServer's AuthProviderEngine. |
Config and environment remain the bootstrap path: you cannot configure authentication through a UI you must first authenticate to reach.
ConfigFromEnvironment)A provider class declares its own environment mapping by implementing the optional
IEnvironmentConfigurableProvider static.
AuthProviderFactory.DiscoverFromEnvironment() walks the class-factory registry and collects every
provider that finds its variables — so a third-party provider gets the same "set two variables and
you're done" experience as a built-in, without touching MJ core.
| Provider | Variables | Resulting provider name |
|---|---|---|
| Entra ID / Azure AD | TENANT_ID + WEB_CLIENT_ID |
azure |
| Auth0 | AUTH0_DOMAIN + AUTH0_CLIENT_ID (optional AUTH0_CLIENT_SECRET) |
auth0 |
| AWS Cognito | COGNITO_USER_POOL_ID + COGNITO_CLIENT_ID + AWS_REGION |
cognito |
| Okta | OKTA_DOMAIN + OKTA_CLIENT_ID (optional OKTA_ISSUER, OKTA_AUDIENCE) |
okta |
| WorkOS | WORKOS_CLIENT_ID (optional WORKOS_AUDIENCE) |
workos |
Implementing it on your own provider:
@RegisterClass(BaseAuthProvider, 'my-idp')
export class MyIdpProvider extends BaseAuthProvider {
static ConfigFromEnvironment(env: NodeJS.ProcessEnv): AuthProviderConfig | null {
// MUST return null — never a partial config — when the variables are absent, or the
// half-populated result fails validateConfig() and errors on every deployment that
// simply does not use this provider.
if (!env.MY_IDP_DOMAIN || !env.MY_IDP_CLIENT_ID) return null;
return {
name: 'my-idp',
type: 'my-idp',
issuer: `https://${env.MY_IDP_DOMAIN}/`,
audience: env.MY_IDP_CLIENT_ID,
jwksUri: `https://${env.MY_IDP_DOMAIN}/.well-known/jwks.json`,
clientId: env.MY_IDP_CLIENT_ID
};
}
// ...extractUserInfo, etc.
}
MJ: Authentication Providers rows name a DriverClass — the same key as @RegisterClass — plus
the non-secret OIDC fields, and are resolved at runtime through the class factory. Two configuration
blobs are split by trust boundary: AdditionalConfiguration is server-only and never published,
while ClientConfiguration is published verbatim to the unauthenticated pre-auth catalog endpoint
that the browser reads before login. Real secrets belong behind CredentialID, which points at an
encrypted MJ: Credentials record.
MJ ships seed rows for its providers (all Inactive, connection fields blank) — see
metadata/authentication-providers/.
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.
// mj.config.cjs
module.exports = {
authProviders: [
{
name: 'corporate-azure-ad',
type: 'msal',
clientId: process.env.AZURE_CLIENT_ID,
tenantId: process.env.AZURE_TENANT_ID,
issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
audience: process.env.AZURE_CLIENT_ID,
jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`,
},
{
name: 'customer-auth0',
type: 'auth0',
clientId: process.env.AUTH0_CLIENT_ID,
domain: 'tenant.auth0.com',
issuer: 'https://tenant.auth0.com/',
audience: 'https://api.example.com',
jwksUri: 'https://tenant.auth0.com/.well-known/jwks.json',
},
{
name: 'workos-prod',
type: 'workos',
clientId: process.env.WORKOS_CLIENT_ID, // client_01H...
issuer: `https://api.workos.com/user_management/${process.env.WORKOS_CLIENT_ID}`,
jwksUri: `https://api.workos.com/sso/jwks/${process.env.WORKOS_CLIENT_ID}`,
audience: process.env.WORKOS_CLIENT_ID, // must match the token's `aud` — see WORKOS.md
},
// ...add more providers here
],
};
WorkOS needs two extra steps beyond this config — an
audclaim. The full walkthrough is in WORKOS.md.
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.
You do not call this package directly when using @memberjunction/server. 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.
import {
AuthProviderFactory,
TokenExpiredError,
} from '@memberjunction/auth-providers';
import jwt, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
// One-time setup at app boot
const factory = AuthProviderFactory.Instance;
const provider = AuthProviderFactory.createProvider({
name: 'main-auth0',
type: 'auth0',
clientId: process.env.AUTH0_CLIENT_ID!,
domain: 'tenant.auth0.com',
issuer: 'https://tenant.auth0.com/',
audience: 'https://api.example.com',
jwksUri: 'https://tenant.auth0.com/.well-known/jwks.json',
});
factory.register(provider);
// Per-request token validation
function validate(token: string) {
return new Promise((resolve, reject) => {
// First decode (without verifying) to read the issuer claim
const decoded = jwt.decode(token, { complete: true });
const issuer = decoded?.payload && typeof decoded.payload === 'object'
? (decoded.payload as jwt.JwtPayload).iss
: undefined;
if (!issuer) return reject(new Error('Token missing iss claim'));
const matched = factory.getByIssuer(issuer);
if (!matched) return reject(new Error(`Unknown issuer: ${issuer}`));
jwt.verify(
token,
(header: JwtHeader, cb: SigningKeyCallback) => matched.getSigningKey(header, cb),
{ issuer: matched.issuer, audience: matched.audience },
(err, payload) => {
if (err?.name === 'TokenExpiredError') {
return reject(new TokenExpiredError(new Date((err as jwt.TokenExpiredError).expiredAt)));
}
if (err || !payload || typeof payload !== 'object') return reject(err);
resolve({
payload,
user: matched.extractUserInfo(payload as jwt.JwtPayload),
});
},
);
});
}
Custom providers extend BaseAuthProvider and register themselves with the MJ class factory. Once registered, they're instantiable by type like any built-in provider.
import { JwtPayload } from 'jsonwebtoken';
import { RegisterClass } from '@memberjunction/global';
import { AuthProviderConfig, AuthUserInfo } from '@memberjunction/core';
import { BaseAuthProvider } from '@memberjunction/auth-providers';
@RegisterClass(BaseAuthProvider, 'keycloak')
export class KeycloakProvider extends BaseAuthProvider {
constructor(config: AuthProviderConfig) {
super(config);
}
extractUserInfo(payload: JwtPayload): AuthUserInfo {
return {
email: payload.email as string | undefined,
firstName: payload.given_name as string | undefined,
lastName: payload.family_name as string | undefined,
fullName: payload.name as string | undefined,
preferredUsername: payload.preferred_username as string | undefined,
roles: (payload.realm_access as { roles?: string[] } | undefined)?.roles,
};
}
validateConfig(): boolean {
return super.validateConfig() && !!this.config.clientId;
}
}
// Then in mj.config.cjs use type: 'keycloak'
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
@RegisterClassdecorator is imported (directly or transitively) beforeAuthProviderFactory.createProvider()runs. The built-in providers achieve this by being exported from this package'sindex.ts— importing anything from@memberjunction/auth-providersloads and registers all of them. (AuthProviderFactory.tsused to carry a literal roster of side-effect imports for this; it was removed because it made the built-ins look like a closed set you had to append to, andindex.tsalready covered it.) For your own provider, export it from your package entry point and make sure that package is reachable from a class-registration manifest — see the discussion in packages/CodeGenLib/CLAUDE.md.
IAuthProviderinterface IAuthProvider {
name: string;
issuer: string;
audience: string;
jwksUri: string;
clientId?: string;
validateConfig(): boolean;
getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void;
extractUserInfo(payload: JwtPayload): AuthUserInfo;
matchesIssuer(issuer: string): boolean;
}
BaseAuthProviderAbstract class that implements all of IAuthProvider except extractUserInfo. It also handles:
socket hang up, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN)Subclasses must implement extractUserInfo(payload) and may override validateConfig() to enforce provider-specific required fields.
AuthProviderFactorySingleton registry/factory. All instance methods are on AuthProviderFactory.Instance; createProvider, getRegisteredProviderTypes, and isProviderTypeRegistered are static helpers.
| Method | Purpose |
|---|---|
static createProvider(config) |
Creates a provider via MJGlobal.ClassFactory from config.type |
register(provider) |
Validates and adds a provider; clears issuer caches |
getByIssuer(iss) |
Returns the first provider whose issuer matches (cached) |
getAllByIssuer(iss) |
Returns all providers for an issuer (multi-app / multi-audience) |
getByName(name) |
Lookup by configured name |
getAllProviders() |
All registered providers |
hasProviders() |
Quick boolean check |
clear() |
Drop all providers and caches (used in tests) |
static getRegisteredProviderTypes() |
All type keys registered with the class factory |
static isProviderTypeRegistered(type) |
Whether a given type key resolves to a registration |
TokenExpiredErrornew TokenExpiredError(expiryDate: Date, message?: string)
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.
@memberjunction/core — defines AuthProviderConfig, AuthUserInfo, AuthTokenInfo, AuthJwtPayload and the AUTH_PROVIDER_TYPES constants@memberjunction/global — provides BaseSingleton, @RegisterClass, and the MJGlobal.ClassFactory that drives provider instantiation@memberjunction/server — primary consumer; wires this package into the GraphQL request pipeline@memberjunction/ai-mcp-server — uses this package to validate tokens on MCP transport endpoints (see MCP OAuth spec)@memberjunction/api-keys-base and @memberjunction/api-keys-engine — complementary auth path for non-interactive (machine-to-machine) callersBaseSingleton rules in .claude/rules/typescript-style.mdBusiness Source License 1.1 — see LICENSE for details.