@memberjunction/auth-providers 5.44.0 → 5.45.1
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 +23 -0
- 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/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/providers/HostIdentityProvider.ts +125 -0
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @memberjunction/auth-providers
|
|
2
2
|
|
|
3
|
+
## 5.45.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @memberjunction/core@5.45.1
|
|
8
|
+
- @memberjunction/global@5.45.1
|
|
9
|
+
|
|
10
|
+
## 5.45.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [45d121b]
|
|
15
|
+
- Updated dependencies [21e33fe]
|
|
16
|
+
- Updated dependencies [b7cf50f]
|
|
17
|
+
- Updated dependencies [f4f11fa]
|
|
18
|
+
- Updated dependencies [e370816]
|
|
19
|
+
- Updated dependencies [fbee64c]
|
|
20
|
+
- Updated dependencies [b2927f1]
|
|
21
|
+
- Updated dependencies [c1f2d3d]
|
|
22
|
+
- Updated dependencies [0b1e009]
|
|
23
|
+
- @memberjunction/core@5.45.0
|
|
24
|
+
- @memberjunction/global@5.45.0
|
|
25
|
+
|
|
3
26
|
## 5.44.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/auth-providers",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.45.1",
|
|
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.1",
|
|
15
|
+
"@memberjunction/global": "5.45.1",
|
|
16
16
|
"graphql": "^16.12.0",
|
|
17
17
|
"jsonwebtoken": "9.0.3",
|
|
18
18
|
"jwks-rsa": "^3.2.2"
|
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
|
+
}
|