@webtypen/webframez-core 0.3.75 → 0.3.76
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/auth-client.js +19 -6
- package/dist/Auth/SessionAuth.d.ts +2 -0
- package/dist/Auth/SessionAuth.js +10 -1
- package/dist/Auth/WebAuth.js +1 -1
- package/docs/authentication.md +3 -1
- package/package.json +1 -1
package/auth-client.js
CHANGED
|
@@ -28,17 +28,30 @@ exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch =
|
|
|
28
28
|
|| ![401, 403].includes(response.status)) return response;
|
|
29
29
|
const failure = await response.clone().json().catch(() => null);
|
|
30
30
|
if (!["unauthorized", "invalid_csrf"].includes(failure?.code)) return response;
|
|
31
|
-
|
|
31
|
+
const performRefresh = () => {
|
|
32
32
|
const refreshHeaders = new Headers();
|
|
33
33
|
const csrf = csrfCookie(options.cookieName);
|
|
34
34
|
if (csrf) refreshHeaders.set("X-CSRF-Token", csrf);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
return originalFetch(`${base}/refresh`, { method: "POST", credentials: "same-origin", headers: refreshHeaders });
|
|
36
|
+
};
|
|
37
|
+
const retry = () => {
|
|
38
|
+
const nextCsrf = csrfCookie(options.cookieName);
|
|
39
|
+
if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
|
|
40
|
+
return originalFetch(retryInput, { ...init, headers });
|
|
41
|
+
};
|
|
42
|
+
if (typeof navigator !== "undefined" && navigator.locks) {
|
|
43
|
+
// Hold the shared tab lock through the retry so another refresh cannot invalidate it.
|
|
44
|
+
return navigator.locks.request(`webframez-auth:${base}`, async () => {
|
|
45
|
+
const renewed = await performRefresh().then(result => result.ok, () => false);
|
|
46
|
+
return renewed ? retry() : response;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!refreshing) {
|
|
50
|
+
refreshing = performRefresh().then(result => result.ok, () => false)
|
|
51
|
+
.finally(() => { refreshing = undefined; });
|
|
37
52
|
}
|
|
38
53
|
if (!await refreshing) return response;
|
|
39
|
-
|
|
40
|
-
if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
|
|
41
|
-
return originalFetch(retryInput, { ...init, headers });
|
|
54
|
+
return retry();
|
|
42
55
|
};
|
|
43
56
|
};
|
|
44
57
|
|
|
@@ -65,6 +65,8 @@ export declare class SessionAuth {
|
|
|
65
65
|
verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
|
|
66
66
|
/** One winner under concurrency. A previously consumed refresh token revokes its device session. */
|
|
67
67
|
refresh(token: string): Promise<SessionTokenPair | null>;
|
|
68
|
+
/** Trusted server refresh-secret lookup; never expose as a public endpoint. */
|
|
69
|
+
inspectRefresh(token: string): Promise<AuthSession | null>;
|
|
68
70
|
/** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
|
|
69
71
|
renewAccess(token: string): Promise<SessionTokenPair | null>;
|
|
70
72
|
/** Browser login metadata; arbitrary request data and token hashes are never exposed. */
|
package/dist/Auth/SessionAuth.js
CHANGED
|
@@ -144,6 +144,15 @@ class SessionAuth {
|
|
|
144
144
|
return this.pair(next, access, refresh);
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
+
/** Trusted server refresh-secret lookup; never expose as a public endpoint. */
|
|
148
|
+
inspectRefresh(token) {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
const id = tokenSessionId(token);
|
|
151
|
+
if (!id)
|
|
152
|
+
return null;
|
|
153
|
+
return this.allowed(yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter())));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
147
156
|
/** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
|
|
148
157
|
renewAccess(token) {
|
|
149
158
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -151,7 +160,7 @@ class SessionAuth {
|
|
|
151
160
|
if (!id)
|
|
152
161
|
return null;
|
|
153
162
|
const rows = yield this.rows();
|
|
154
|
-
const filter = Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter());
|
|
163
|
+
const filter = Object.assign(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter()), { accessExpiresAt: { $lte: Date.now() } });
|
|
155
164
|
const row = yield rows.findOne(filter);
|
|
156
165
|
if (!row || !(yield this.allowed(row)))
|
|
157
166
|
return null;
|
package/dist/Auth/WebAuth.js
CHANGED
|
@@ -96,7 +96,7 @@ class WebAuth {
|
|
|
96
96
|
return null;
|
|
97
97
|
const pair = yield this.sessions.renewAccess(token);
|
|
98
98
|
if (!pair)
|
|
99
|
-
return
|
|
99
|
+
return this.sessions.inspectRefresh(token);
|
|
100
100
|
this.writePair(res, pair);
|
|
101
101
|
const name = this.cookies.prefix + "access";
|
|
102
102
|
const cookies = (0, AuthSecurity_1.authHeader)(req, "cookie").split(";").map(value => value.trim()).filter(value => value && !value.startsWith(`${name}=`));
|
package/docs/authentication.md
CHANGED
|
@@ -475,7 +475,9 @@ unsafe requests, revive expired sessions, or extend absolute expiry. Explicit
|
|
|
475
475
|
POST refresh rotates both secrets and retains replay protection. Middleware uses
|
|
476
476
|
this resumption automatically when a response is available. Browser `createAuthFetch`
|
|
477
477
|
refreshes rejected authentication/CSRF requests once, then repeats the rejected
|
|
478
|
-
request once; login failures and permission errors are never retried.
|
|
478
|
+
request once; login failures and permission errors are never retried. Web Locks
|
|
479
|
+
serialize refresh calls across tabs in browsers supporting that API; a per-window
|
|
480
|
+
single-flight promise remains the fallback.
|
|
479
481
|
|
|
480
482
|
Reset tokens have 256-bit random secrets; only a SHA-256 digest bound to the scope
|
|
481
483
|
and current password is stored. Per-account request cooldown is an atomic conditional
|
package/package.json
CHANGED