@culturefy/shared 1.0.39 → 1.0.40
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/build/cjs/constants/app.js +45 -0
- package/build/cjs/constants/app.js.map +1 -0
- package/build/cjs/constants/index.js +10 -0
- package/build/cjs/constants/index.js.map +1 -0
- package/build/cjs/index.js +6 -0
- package/build/cjs/index.js.map +1 -1
- package/build/cjs/middlewares/verify-middleware.js +202 -0
- package/build/cjs/middlewares/verify-middleware.js.map +1 -0
- package/build/cjs/types/app.js +2 -0
- package/build/cjs/types/app.js.map +1 -0
- package/build/cjs/utils/cookies.js +28 -0
- package/build/cjs/utils/cookies.js.map +1 -0
- package/build/esm/constants/app.js +41 -0
- package/build/esm/constants/app.js.map +1 -0
- package/build/esm/constants/index.js +2 -0
- package/build/esm/constants/index.js.map +1 -0
- package/build/esm/index.js +1 -0
- package/build/esm/index.js.map +1 -1
- package/build/esm/middlewares/verify-middleware.js +197 -0
- package/build/esm/middlewares/verify-middleware.js.map +1 -0
- package/build/esm/types/app.js +2 -0
- package/build/esm/types/app.js.map +1 -0
- package/build/esm/utils/cookies.js +24 -0
- package/build/esm/utils/cookies.js.map +1 -0
- package/build/src/constants/app.d.ts +2 -0
- package/build/src/constants/app.js +38 -0
- package/build/src/constants/app.js.map +1 -0
- package/build/src/constants/index.d.ts +1 -0
- package/build/src/constants/index.js +5 -0
- package/build/src/constants/index.js.map +1 -0
- package/build/src/index.d.ts +1 -0
- package/build/src/index.js +1 -0
- package/build/src/index.js.map +1 -1
- package/build/src/middlewares/verify-middleware.d.ts +2 -0
- package/build/src/middlewares/verify-middleware.js +167 -0
- package/build/src/middlewares/verify-middleware.js.map +1 -0
- package/build/src/types/app.d.ts +29 -0
- package/build/src/types/app.js +3 -0
- package/build/src/types/app.js.map +1 -0
- package/build/src/utils/cookies.d.ts +2 -0
- package/build/src/utils/cookies.js +25 -0
- package/build/src/utils/cookies.js.map +1 -0
- package/package.json +1 -1
- package/src/constants/app.ts +40 -0
- package/src/constants/index.ts +1 -0
- package/src/index.ts +2 -1
- package/src/middlewares/verify-middleware.ts +197 -0
- package/src/types/app.ts +27 -0
- package/src/utils/cookies.ts +24 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { HttpResponseInit } from "@azure/functions";
|
|
2
|
+
import { HttpRequest } from "@azure/functions";
|
|
3
|
+
import { InvocationContext } from "@azure/functions";
|
|
4
|
+
import { sendResponse } from "../utils";
|
|
5
|
+
import { IMiddleware } from "../types/middleware";
|
|
6
|
+
import { IAppId } from "../types/app";
|
|
7
|
+
import { APP_MAP } from "../constants";
|
|
8
|
+
import { jwtDecode } from "jwt-decode";
|
|
9
|
+
import { setCookieKV } from "../utils/cookies";
|
|
10
|
+
|
|
11
|
+
const apiURL = process.env.REFRESH_SESSION_URL || ''
|
|
12
|
+
|
|
13
|
+
const parseCookieHeader = (header: string | null | undefined) => {
|
|
14
|
+
const out: Record<string, string> = {};
|
|
15
|
+
if (!header) return out;
|
|
16
|
+
for (const part of header.split(";")) {
|
|
17
|
+
const [k, ...rest] = part.trim().split("=");
|
|
18
|
+
if (!k) continue;
|
|
19
|
+
out[k] = decodeURIComponent(rest.join("=") || "");
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const verifyMw: IMiddleware = async (
|
|
25
|
+
req: HttpRequest,
|
|
26
|
+
ctx: InvocationContext,
|
|
27
|
+
next: () => Promise<HttpResponseInit>
|
|
28
|
+
): Promise<HttpResponseInit> => {
|
|
29
|
+
const appId = req.headers.get("app-id") as IAppId | undefined;
|
|
30
|
+
|
|
31
|
+
if (!appId || !APP_MAP?.[appId]?.clientId) {
|
|
32
|
+
return sendResponse(400, { status: "bad_request", reason: "invalid_app" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const expectedClientId = APP_MAP[appId].clientId;
|
|
36
|
+
|
|
37
|
+
// cookies
|
|
38
|
+
const cookies = parseCookieHeader(req.headers.get("cookie"));
|
|
39
|
+
const at = cookies[`__Secure-session-v1.${appId}.at`];
|
|
40
|
+
const rt = cookies[`__Secure-session-v1.${appId}.rt`];
|
|
41
|
+
|
|
42
|
+
if (!at && !rt) {
|
|
43
|
+
return sendResponse(401, { status: "unauthenticated", reason: "no_tokens" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// decode/verify (lightweight; replace with your verifyJsonWebToken if you have it)
|
|
47
|
+
let p: any;
|
|
48
|
+
try {
|
|
49
|
+
p = jwtDecode(at);
|
|
50
|
+
} catch {
|
|
51
|
+
return sendResponse(401, { status: "unauthenticated", reason: "invalid_token" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!p?.sid) {
|
|
55
|
+
return sendResponse(401, { status: "unauthenticated", reason: "user_not_found" });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const now = Math.floor(Date.now() / 1000);
|
|
59
|
+
// if (typeof p.exp === "number" && p.exp <= now) {
|
|
60
|
+
if (typeof p.exp === "number" && p.exp >= now) {
|
|
61
|
+
// Delegate to refresh helper; it will handle setting cookies/state or returning an error
|
|
62
|
+
return await getNewRefreshToken(req, ctx, appId, expectedClientId, rt, p, next);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// audience checks
|
|
66
|
+
const audOk =
|
|
67
|
+
(Array.isArray(p.aud) && p.aud.includes(expectedClientId)) ||
|
|
68
|
+
(typeof p.aud === "string" && (p.aud === expectedClientId || p.aud === "account")) ||
|
|
69
|
+
p.azp === expectedClientId;
|
|
70
|
+
|
|
71
|
+
if (!audOk) {
|
|
72
|
+
return sendResponse(403, { status: "forbidden", reason: "audience_mismatch" });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
setCookieKV(ctx, 'ew','rre');
|
|
77
|
+
|
|
78
|
+
// pass data downstream
|
|
79
|
+
(ctx as any).state ??= {};
|
|
80
|
+
const tenantId = p.cfy_tid ?? (p.iss ? new URL(p.iss).pathname.split("/").pop() : null);
|
|
81
|
+
|
|
82
|
+
(ctx as any).state.auth = {
|
|
83
|
+
appId,
|
|
84
|
+
userId: p.sub ?? null,
|
|
85
|
+
businessId: p.cfy_bid ?? tenantId ?? null,
|
|
86
|
+
tenantId,
|
|
87
|
+
email: p.email ?? p.preferred_username ?? null,
|
|
88
|
+
name: p.name ?? undefined,
|
|
89
|
+
roles: p.resource_access?.[expectedClientId]?.roles ?? p.realm_access?.roles ?? [],
|
|
90
|
+
exp: p.exp,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return next();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async function getNewRefreshToken(
|
|
99
|
+
req: HttpRequest,
|
|
100
|
+
ctx: InvocationContext,
|
|
101
|
+
appId: IAppId,
|
|
102
|
+
expectedClientId: string,
|
|
103
|
+
rt: string | undefined,
|
|
104
|
+
p: any,
|
|
105
|
+
next: () => Promise<HttpResponseInit>
|
|
106
|
+
): Promise<HttpResponseInit> {
|
|
107
|
+
// Attempt server-side refresh using RT
|
|
108
|
+
if (!rt) {
|
|
109
|
+
return sendResponse(401, { status: "unauthenticated", reason: "expired_no_rt" });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Resolve realm for refresh
|
|
113
|
+
let realmId: string | undefined = APP_MAP[appId].auth?.realm;
|
|
114
|
+
if (!realmId) {
|
|
115
|
+
try {
|
|
116
|
+
const issRealm = p?.iss ? new URL(p.iss).pathname.split("/").pop() : undefined;
|
|
117
|
+
realmId = (p as any)?.cfy_tid || issRealm || undefined;
|
|
118
|
+
} catch {
|
|
119
|
+
realmId = undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!realmId) {
|
|
124
|
+
return sendResponse(401, { status: "unauthenticated", reason: "cannot_resolve_realm" });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ctx.info("refreshing token payload ----------------------", {
|
|
128
|
+
realmId,
|
|
129
|
+
expectedClientId,
|
|
130
|
+
rt
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// Call auth service to refresh
|
|
135
|
+
try {
|
|
136
|
+
const resp = await fetch(apiURL, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
realmId,
|
|
141
|
+
clientId: expectedClientId,
|
|
142
|
+
refresh_token: rt
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!resp.ok) {
|
|
147
|
+
const text = await resp.text();
|
|
148
|
+
ctx.warn?.(`refresh call failed: ${resp.status} ${text}`);
|
|
149
|
+
return sendResponse(401, { status: "unauthenticated", reason: "refresh_failed" });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
const payload = await resp.json();
|
|
154
|
+
const data = payload?.data || {};
|
|
155
|
+
const newAT = data.access_token as string | undefined;
|
|
156
|
+
const newRT = data.refresh_token as string | undefined;
|
|
157
|
+
if (!newAT || !newRT) {
|
|
158
|
+
return sendResponse(401, { status: "unauthenticated", reason: "invalid_refresh_response" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set refreshed cookies for client session
|
|
162
|
+
setCookieKV(ctx, `__Secure-session-v1.${appId}.at`, newAT);
|
|
163
|
+
setCookieKV(ctx, `__Secure-session-v1.${appId}.rt`, newRT);
|
|
164
|
+
|
|
165
|
+
// Decode new AT and proceed
|
|
166
|
+
let p2: any;
|
|
167
|
+
try { p2 = jwtDecode(newAT); } catch { return sendResponse(401, { status: "unauthenticated", reason: "invalid_new_token" }); }
|
|
168
|
+
|
|
169
|
+
const audOk2 =
|
|
170
|
+
(Array.isArray(p2.aud) && p2.aud.includes(expectedClientId)) ||
|
|
171
|
+
(typeof p2.aud === "string" && (p2.aud === expectedClientId || p2.aud === "account")) ||
|
|
172
|
+
p2.azp === expectedClientId;
|
|
173
|
+
if (!audOk2) {
|
|
174
|
+
return sendResponse(403, { status: "forbidden", reason: "audience_mismatch" });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update downstream auth state with refreshed token
|
|
178
|
+
(ctx as any).state ??= {};
|
|
179
|
+
const tenantId2 = p2.cfy_tid ?? (p2.iss ? new URL(p2.iss).pathname.split("/").pop() : null);
|
|
180
|
+
(ctx as any).state.auth = {
|
|
181
|
+
appId,
|
|
182
|
+
userId: p2.sub ?? null,
|
|
183
|
+
businessId: p2.cfy_bid ?? tenantId2 ?? null,
|
|
184
|
+
tenantId: tenantId2,
|
|
185
|
+
email: p2.email ?? p2.preferred_username ?? null,
|
|
186
|
+
name: p2.name ?? undefined,
|
|
187
|
+
roles: p2.resource_access?.[expectedClientId]?.roles ?? p2.realm_access?.roles ?? [],
|
|
188
|
+
exp: p2.exp,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Continue pipeline after refresh
|
|
192
|
+
return next();
|
|
193
|
+
} catch (e) {
|
|
194
|
+
ctx.error?.("refresh exception", e as any);
|
|
195
|
+
return sendResponse(401, { status: "unauthenticated", reason: "refresh_exception" });
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/types/app.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type IAppId = "3238hxa2";
|
|
2
|
+
|
|
3
|
+
export interface IDomainMappings {
|
|
4
|
+
domains: Record<string, string[]>;
|
|
5
|
+
clientId: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
exclude: Record<string, string[]>;
|
|
9
|
+
cookie: {
|
|
10
|
+
prefix: string;
|
|
11
|
+
domain: {
|
|
12
|
+
local: string | null;
|
|
13
|
+
dev: string;
|
|
14
|
+
staging: string;
|
|
15
|
+
prod: string;
|
|
16
|
+
};
|
|
17
|
+
path: string;
|
|
18
|
+
sameSite: string;
|
|
19
|
+
secure: boolean;
|
|
20
|
+
httpOnly: boolean;
|
|
21
|
+
maxAgeSec: { sid: number; rt: number };
|
|
22
|
+
};
|
|
23
|
+
auth?: {
|
|
24
|
+
realm: string;
|
|
25
|
+
clientId: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { InvocationContext } from "@azure/functions";
|
|
2
|
+
|
|
3
|
+
export function setCookieKV(ctx: InvocationContext, key: string, value: string): void {
|
|
4
|
+
// Object-cookie bag (preferred)
|
|
5
|
+
const CTX_COOKIES_OBJ = Symbol.for("cfy.resCookies.obj");
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
const objBag = ((ctx as any)[CTX_COOKIES_OBJ] ??= [] as HttpCookie[]);
|
|
8
|
+
objBag.push({
|
|
9
|
+
name: key,
|
|
10
|
+
value,
|
|
11
|
+
path: "/",
|
|
12
|
+
httpOnly: true,
|
|
13
|
+
secure: true, // drop to false if testing on http://
|
|
14
|
+
sameSite: "None", // use "Lax" for same-site
|
|
15
|
+
maxAge: 300, // seconds
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// (Optional) Keep your string fallback too:
|
|
19
|
+
const CTX_COOKIES = Symbol.for("cfy.resCookies");
|
|
20
|
+
const strBag = ((ctx as any)[CTX_COOKIES] ??= [] as string[]);
|
|
21
|
+
strBag.push(
|
|
22
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=300`
|
|
23
|
+
);
|
|
24
|
+
}
|