@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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @memberjunction/auth-providers@5.44.0 build
2
+ > @memberjunction/auth-providers@5.45.1 build
3
3
  > tsc && tsc-alias -f
4
4
 
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
@@ -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;AAGrE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,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.44.0",
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.44.0",
15
- "@memberjunction/global": "5.44.0",
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
+ }