@kwirthmagnify/kwirth-common-back 0.5.42 → 0.5.43

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/oidc.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  import { IIdpAuthContext, IIdpCallbackContext, IIdpConfigFieldDef, IIdpIdentity } from './IIdpConnector';
2
+ export interface IOidcCallbackOptions {
3
+ emailClaims?: string[];
4
+ assumeVerified?: boolean;
5
+ multiTenant?: boolean;
6
+ allowedTenants?: string[];
7
+ }
2
8
  export declare function oidcConfigSchema(): IIdpConfigFieldDef[];
9
+ export declare function mapOidcIdentity(claims: Record<string, unknown>, opts?: IOidcCallbackOptions): IIdpIdentity;
10
+ export declare function tenantAllowed(tid: string | undefined, allowed?: string[]): boolean;
3
11
  export declare function oidcBuildAuthorizationUrl(config: Record<string, unknown>, ctx: IIdpAuthContext, defaultIssuer?: string): Promise<string>;
4
- export declare function oidcHandleCallback(config: Record<string, unknown>, ctx: IIdpCallbackContext, defaultIssuer?: string): Promise<IIdpIdentity>;
12
+ export declare function oidcHandleCallback(config: Record<string, unknown>, ctx: IIdpCallbackContext, defaultIssuer?: string, opts?: IOidcCallbackOptions): Promise<IIdpIdentity>;
package/dist/oidc.js CHANGED
@@ -1,17 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.oidcConfigSchema = oidcConfigSchema;
37
+ exports.mapOidcIdentity = mapOidcIdentity;
38
+ exports.tenantAllowed = tenantAllowed;
4
39
  exports.oidcBuildAuthorizationUrl = oidcBuildAuthorizationUrl;
5
40
  exports.oidcHandleCallback = oidcHandleCallback;
6
41
  const openid_client_1 = require("openid-client");
7
- /*
8
- Lógica OIDC compartida por todos los conectores OIDC (Google, Keycloak, GitLab, Microsoft, ...).
9
- Vive en common-back y el back la expone como global (__kwirth_back__.kwirthCommonBack), de modo
10
- que los conectores la usan por composición SIN bundlear openid-client ni duplicar el flujo.
11
-
12
- Flujo Authorization Code + PKCE con intercambio back-channel (el id_token llega por TLS del
13
- token endpoint, así que openid-client valida issuer/aud y basta con eso).
14
- */
42
+ const jose = __importStar(require("jose"));
15
43
  // esquema de config estándar de un IdP OIDC (clientSecret es 'password' → se enmascara en la UI)
16
44
  function oidcConfigSchema() {
17
45
  return [
@@ -26,15 +54,43 @@ async function makeClient(config, redirectUri, defaultIssuer) {
26
54
  if (!issuerUrl)
27
55
  throw new Error('OIDC issuer not configured');
28
56
  const issuer = await openid_client_1.Issuer.discover(issuerUrl);
29
- return new issuer.Client({
57
+ const client = new issuer.Client({
30
58
  client_id: config.clientId,
31
59
  client_secret: config.clientSecret,
32
60
  redirect_uris: [redirectUri],
33
61
  response_types: ['code']
34
62
  });
63
+ return { issuer, client };
64
+ }
65
+ // mapea los claims del id_token a la identidad de Kwirth (fallback de email + verified asumido)
66
+ function mapOidcIdentity(claims, opts) {
67
+ const emailClaims = opts?.emailClaims ?? ['email'];
68
+ let email = '';
69
+ for (const claim of emailClaims) {
70
+ const value = claims[claim];
71
+ if (typeof value === 'string' && value !== '') {
72
+ email = value;
73
+ break;
74
+ }
75
+ }
76
+ return {
77
+ email,
78
+ emailVerified: claims.email_verified === true || opts?.assumeVerified === true,
79
+ name: typeof claims.name === 'string' ? claims.name : undefined,
80
+ sub: claims.sub !== undefined ? String(claims.sub) : undefined
81
+ };
82
+ }
83
+ // ¿el tenant (tid) está permitido? (allowlist vacía = cualquier tenant)
84
+ function tenantAllowed(tid, allowed) {
85
+ if (!allowed || allowed.length === 0)
86
+ return true;
87
+ if (!tid)
88
+ return false;
89
+ const lower = tid.toLowerCase();
90
+ return allowed.some(t => t.toLowerCase() === lower);
35
91
  }
36
92
  async function oidcBuildAuthorizationUrl(config, ctx, defaultIssuer) {
37
- const client = await makeClient(config, ctx.redirectUri, defaultIssuer);
93
+ const { client } = await makeClient(config, ctx.redirectUri, defaultIssuer);
38
94
  const scope = config.scopes || 'openid email profile';
39
95
  return client.authorizationUrl({
40
96
  scope,
@@ -44,20 +100,42 @@ async function oidcBuildAuthorizationUrl(config, ctx, defaultIssuer) {
44
100
  code_challenge_method: 'S256'
45
101
  });
46
102
  }
47
- async function oidcHandleCallback(config, ctx, defaultIssuer) {
48
- const client = await makeClient(config, ctx.redirectUri, defaultIssuer);
49
- // pasamos los params crudos del callback (incluyen iss para RFC 9207 y state); el state ya lo
50
- // valida el core, pero openid-client exige checks.state si el param state viene presente.
103
+ async function oidcHandleCallback(config, ctx, defaultIssuer, opts) {
104
+ const { issuer, client } = await makeClient(config, ctx.redirectUri, defaultIssuer);
105
+ if (opts?.multiTenant) {
106
+ // openid-client valida el iss de forma LITERAL y en multi-tenant el issuer descubierto lleva el
107
+ // placeholder {tenantid}; hacemos el intercambio crudo (grant, sin validar id_token) y validamos
108
+ // el id_token a mano con jose: firma (JWKS del issuer) + iss contra el tid concreto del token.
109
+ const tokenSet = await client.grant({
110
+ grant_type: 'authorization_code',
111
+ code: ctx.code,
112
+ redirect_uri: ctx.redirectUri,
113
+ code_verifier: ctx.codeVerifier
114
+ });
115
+ if (!tokenSet.id_token)
116
+ throw new Error('OIDC multi-tenant: token response has no id_token');
117
+ const tid = tokenSet.claims().tid; // sin verificar; solo para conocer el tenant
118
+ if (!tid)
119
+ throw new Error('OIDC multi-tenant: id_token has no tid claim');
120
+ if (!tenantAllowed(tid, opts.allowedTenants))
121
+ throw new Error(`OIDC multi-tenant: tenant '${tid}' not allowed`);
122
+ const jwksUri = issuer.metadata.jwks_uri;
123
+ if (!jwksUri)
124
+ throw new Error('OIDC multi-tenant: issuer metadata has no jwks_uri');
125
+ const jwks = jose.createRemoteJWKSet(new URL(jwksUri));
126
+ const { payload } = await jose.jwtVerify(tokenSet.id_token, jwks, {
127
+ issuer: `https://login.microsoftonline.com/${tid}/v2.0`,
128
+ audience: config.clientId,
129
+ clockTolerance: 5
130
+ });
131
+ return mapOidcIdentity(payload, opts);
132
+ }
133
+ // single-tenant: pasamos los params crudos del callback (incluyen iss para RFC 9207 y state); el state
134
+ // ya lo valida el core, pero openid-client exige checks.state si el param state viene presente.
51
135
  const params = ctx.params ?? { code: ctx.code };
52
136
  const checks = { code_verifier: ctx.codeVerifier };
53
137
  if (params.state !== undefined)
54
138
  checks.state = params.state;
55
139
  const tokenSet = await client.callback(ctx.redirectUri, params, checks);
56
- const claims = tokenSet.claims();
57
- return {
58
- email: String(claims.email ?? ''),
59
- emailVerified: claims.email_verified === true,
60
- name: claims.name ? String(claims.name) : undefined,
61
- sub: claims.sub
62
- };
140
+ return mapOidcIdentity(tokenSet.claims(), opts);
63
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kwirthmagnify/kwirth-common-back",
3
- "version": "0.5.42",
3
+ "version": "0.5.43",
4
4
  "description": "Backend interfaces for building Kwirth provider and channel plugins",
5
5
  "scripts": {
6
6
  "build": "tsc",
@@ -28,6 +28,7 @@
28
28
  "@kubernetes/client-node": "^1.4.0",
29
29
  "@kwirthmagnify/kwirth-common": "^0.5.46",
30
30
  "express": "^4.18.0",
31
+ "jose": "^4.15.9",
31
32
  "js-yaml": "^4.1.0",
32
33
  "openid-client": "^5.7.0"
33
34
  },