zudoku 0.82.3 → 0.82.4
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/dist/cli/cli.js +29 -25
- package/dist/cli/worker.js +19 -0
- package/dist/declarations/config/config.d.ts +3 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +38 -0
- package/dist/declarations/lib/authentication/providers/entra.d.ts +13 -0
- package/dist/declarations/lib/authentication/providers/openid.d.ts +4 -1
- package/dist/declarations/lib/authentication/providers/util.d.ts +4 -0
- package/dist/declarations/lib/plugins/openapi/McpClientLogos.d.ts +4 -0
- package/dist/declarations/lib/plugins/openapi/mcp-configs.d.ts +3 -0
- package/dist/flat-config.d.ts +17 -0
- package/docs/configuration/authentication-azure-ad.md +17 -15
- package/docs/configuration/authentication.md +22 -2
- package/docs/guides/mcp-servers.md +26 -0
- package/package.json +12 -9
- package/src/config/config.ts +4 -0
- package/src/config/validators/ZudokuConfig.ts +19 -0
- package/src/lib/auth/issuer.ts +7 -1
- package/src/lib/authentication/providers/entra.tsx +97 -0
- package/src/lib/authentication/providers/openid.tsx +29 -13
- package/src/lib/authentication/providers/util.ts +7 -0
- package/src/lib/plugins/openapi/MCPEndpoint.tsx +347 -228
- package/src/lib/plugins/openapi/McpClientLogos.tsx +72 -0
- package/src/lib/plugins/openapi/mcp-configs.ts +55 -0
- package/src/zuplo/enrich-with-zuplo-mcp.ts +14 -2
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type * as oauth from "oauth4webapi";
|
|
2
|
+
import type { EntraAuthenticationConfig } from "../../../config/config.js";
|
|
3
|
+
import type {
|
|
4
|
+
AuthenticationPlugin,
|
|
5
|
+
AuthenticationProviderInitializer,
|
|
6
|
+
} from "../authentication.js";
|
|
7
|
+
import type { UserProfile } from "../state.js";
|
|
8
|
+
import { OpenIDAuthenticationProvider } from "./openid.js";
|
|
9
|
+
import { getEntraIssuer } from "./util.js";
|
|
10
|
+
|
|
11
|
+
const ISSUER_TENANT_PLACEHOLDER = "{tenantid}";
|
|
12
|
+
|
|
13
|
+
export class EntraAuthenticationProvider
|
|
14
|
+
extends OpenIDAuthenticationProvider
|
|
15
|
+
implements AuthenticationPlugin
|
|
16
|
+
{
|
|
17
|
+
constructor(config: EntraAuthenticationConfig) {
|
|
18
|
+
super({ ...config, type: "openid", issuer: getEntraIssuer(config) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Multi-tenant authorities advertise a templated issuer that never matches
|
|
22
|
+
// the requested authority, so the strict discovery check would throw. Expect
|
|
23
|
+
// the template itself; resolveTokenIssuer resolves the concrete tenant per
|
|
24
|
+
// token. Recommended by oauth4webapi's maintainer:
|
|
25
|
+
// https://github.com/panva/oauth4webapi/issues/100
|
|
26
|
+
protected override async getExpectedDiscoveryIssuer(
|
|
27
|
+
issuerUrl: URL,
|
|
28
|
+
response: Response,
|
|
29
|
+
): Promise<URL> {
|
|
30
|
+
try {
|
|
31
|
+
const metadata = (await response.clone().json()) as { issuer?: unknown };
|
|
32
|
+
return typeof metadata.issuer === "string" &&
|
|
33
|
+
metadata.issuer.includes(ISSUER_TENANT_PLACEHOLDER)
|
|
34
|
+
? new URL(metadata.issuer)
|
|
35
|
+
: issuerUrl;
|
|
36
|
+
} catch {
|
|
37
|
+
// Malformed body: keep the strict path so processDiscoveryResponse
|
|
38
|
+
// surfaces the real error.
|
|
39
|
+
return issuerUrl;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Without a usable ID token the template stays in place and fails the strict
|
|
44
|
+
// `iss` validation loudly.
|
|
45
|
+
protected override async resolveTokenIssuer(
|
|
46
|
+
as: oauth.AuthorizationServer,
|
|
47
|
+
response: Response,
|
|
48
|
+
): Promise<oauth.AuthorizationServer> {
|
|
49
|
+
if (!as.issuer.includes(ISSUER_TENANT_PLACEHOLDER)) return as;
|
|
50
|
+
try {
|
|
51
|
+
const body = (await response.clone().json()) as { id_token?: unknown };
|
|
52
|
+
if (typeof body.id_token !== "string") return as;
|
|
53
|
+
const { decodeJwt } = await import("jose");
|
|
54
|
+
const { tid } = decodeJwt(body.id_token);
|
|
55
|
+
return typeof tid === "string" && tid.length > 0
|
|
56
|
+
? {
|
|
57
|
+
...as,
|
|
58
|
+
issuer: as.issuer.replace(ISSUER_TENANT_PLACEHOLDER, () => tid),
|
|
59
|
+
}
|
|
60
|
+
: as;
|
|
61
|
+
} catch {
|
|
62
|
+
return as;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Entra's `email` claim is only present when the account has a mailbox, which
|
|
67
|
+
// isn't guaranteed — often absent for users from external tenants. Its
|
|
68
|
+
// UserInfo endpoint also omits `preferred_username`, so the base profile
|
|
69
|
+
// (built from UserInfo) can lack any usable identifier. Fill `email` from the
|
|
70
|
+
// ID token's username claims, and surface the immutable `oid`/`tid` for
|
|
71
|
+
// reliable identity and tenant checks. `email`/`preferred_username` are
|
|
72
|
+
// mutable — kept unverified and unsuitable for authorization.
|
|
73
|
+
// https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference
|
|
74
|
+
protected override buildUserProfile(
|
|
75
|
+
userInfo: oauth.UserInfoResponse,
|
|
76
|
+
claims: oauth.IDToken | undefined,
|
|
77
|
+
): UserProfile {
|
|
78
|
+
const profile = super.buildUserProfile(userInfo, claims);
|
|
79
|
+
const claim = (name: string) =>
|
|
80
|
+
typeof claims?.[name] === "string" ? claims[name] : undefined;
|
|
81
|
+
|
|
82
|
+
const preferredUsername = claim("preferred_username");
|
|
83
|
+
return {
|
|
84
|
+
...profile,
|
|
85
|
+
email: profile.email ?? preferredUsername ?? claim("upn"),
|
|
86
|
+
...(preferredUsername && { preferred_username: preferredUsername }),
|
|
87
|
+
...(claim("oid") && { oid: claim("oid") }),
|
|
88
|
+
...(claim("tid") && { tid: claim("tid") }),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entraAuth: AuthenticationProviderInitializer<
|
|
94
|
+
EntraAuthenticationConfig
|
|
95
|
+
> = (options) => new EntraAuthenticationProvider(options);
|
|
96
|
+
|
|
97
|
+
export default entraAuth;
|
|
@@ -143,14 +143,32 @@ export class OpenIDAuthenticationProvider
|
|
|
143
143
|
issuerUrl,
|
|
144
144
|
this.oauthOptions,
|
|
145
145
|
);
|
|
146
|
+
|
|
146
147
|
this.authorizationServer = await oauth.processDiscoveryResponse(
|
|
147
|
-
issuerUrl,
|
|
148
|
+
await this.getExpectedDiscoveryIssuer(issuerUrl, response),
|
|
148
149
|
response,
|
|
149
150
|
);
|
|
150
151
|
}
|
|
151
152
|
return this.authorizationServer;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
// Override points for providers whose issuers deviate from the spec (see
|
|
156
|
+
// EntraAuthenticationProvider). Overrides must peek via `response.clone()`;
|
|
157
|
+
// the body is consumed downstream.
|
|
158
|
+
protected async getExpectedDiscoveryIssuer(
|
|
159
|
+
issuerUrl: URL,
|
|
160
|
+
_response: Response,
|
|
161
|
+
): Promise<URL> {
|
|
162
|
+
return issuerUrl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected async resolveTokenIssuer(
|
|
166
|
+
as: oauth.AuthorizationServer,
|
|
167
|
+
_response: Response,
|
|
168
|
+
): Promise<oauth.AuthorizationServer> {
|
|
169
|
+
return as;
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
/**
|
|
155
173
|
* Sets the tokens from various OAuth responses
|
|
156
174
|
* @param response
|
|
@@ -239,12 +257,12 @@ export class OpenIDAuthenticationProvider
|
|
|
239
257
|
});
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
protected buildUserProfile(
|
|
243
261
|
userInfo: oauth.UserInfoResponse,
|
|
244
|
-
|
|
262
|
+
claims: oauth.IDToken | undefined,
|
|
245
263
|
): UserProfile {
|
|
246
264
|
const emailVerified =
|
|
247
|
-
userInfo.email_verified ??
|
|
265
|
+
userInfo.email_verified ?? claims?.email_verified ?? false;
|
|
248
266
|
return {
|
|
249
267
|
...userInfo,
|
|
250
268
|
sub: userInfo.sub,
|
|
@@ -303,12 +321,10 @@ export class OpenIDAuthenticationProvider
|
|
|
303
321
|
const userInfo = await userInfoResponse.json();
|
|
304
322
|
|
|
305
323
|
const { providerData } = useAuthState.getState();
|
|
306
|
-
const
|
|
307
|
-
providerData?.type === "openid"
|
|
308
|
-
? providerData.claims?.email_verified
|
|
309
|
-
: undefined;
|
|
324
|
+
const claims =
|
|
325
|
+
providerData?.type === "openid" ? providerData.claims : undefined;
|
|
310
326
|
|
|
311
|
-
const profile = this.buildUserProfile(userInfo,
|
|
327
|
+
const profile = this.buildUserProfile(userInfo, claims);
|
|
312
328
|
|
|
313
329
|
useAuthState.setState({
|
|
314
330
|
isAuthenticated: true,
|
|
@@ -496,7 +512,7 @@ export class OpenIDAuthenticationProvider
|
|
|
496
512
|
);
|
|
497
513
|
|
|
498
514
|
const result = await oauth.processRefreshTokenResponse(
|
|
499
|
-
as,
|
|
515
|
+
await this.resolveTokenIssuer(as, response),
|
|
500
516
|
this.client,
|
|
501
517
|
response,
|
|
502
518
|
);
|
|
@@ -590,7 +606,7 @@ export class OpenIDAuthenticationProvider
|
|
|
590
606
|
this.oauthOptions,
|
|
591
607
|
);
|
|
592
608
|
const result = await oauth.processRefreshTokenResponse(
|
|
593
|
-
as,
|
|
609
|
+
await this.resolveTokenIssuer(as, response),
|
|
594
610
|
this.client,
|
|
595
611
|
response,
|
|
596
612
|
);
|
|
@@ -657,7 +673,7 @@ export class OpenIDAuthenticationProvider
|
|
|
657
673
|
);
|
|
658
674
|
|
|
659
675
|
const oauthResult = await oauth.processAuthorizationCodeResponse(
|
|
660
|
-
authServer,
|
|
676
|
+
await this.resolveTokenIssuer(authServer, response),
|
|
661
677
|
this.client,
|
|
662
678
|
response,
|
|
663
679
|
);
|
|
@@ -678,7 +694,7 @@ export class OpenIDAuthenticationProvider
|
|
|
678
694
|
);
|
|
679
695
|
const userInfo = await userInfoResponse.json();
|
|
680
696
|
|
|
681
|
-
const profile = this.buildUserProfile(userInfo, claims
|
|
697
|
+
const profile = this.buildUserProfile(userInfo, claims);
|
|
682
698
|
|
|
683
699
|
useAuthState.setState({
|
|
684
700
|
isAuthenticated: true,
|
|
@@ -12,6 +12,13 @@ export const redirectToSignUpUrl = (
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export const getEntraIssuer = (config: {
|
|
16
|
+
issuer?: string;
|
|
17
|
+
tenantId?: string;
|
|
18
|
+
}) =>
|
|
19
|
+
config.issuer ??
|
|
20
|
+
`https://login.microsoftonline.com/${config.tenantId ?? "common"}/v2.0`;
|
|
21
|
+
|
|
15
22
|
export const getClerkFrontendApi = (publishableKey: string) => {
|
|
16
23
|
// Split by underscore and get the base64 encoded part (3rd segment)
|
|
17
24
|
const parts = publishableKey.split("_");
|