@camstack/server 1.2.246 → 1.2.248
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/auth/session-cookie.js +41 -0
- package/dist/main.js +9 -3
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ exports.shouldRedirectToLogin = shouldRedirectToLogin;
|
|
|
8
8
|
exports.loginRedirectUrl = loginRedirectUrl;
|
|
9
9
|
exports.isEmbedRedirectTarget = isEmbedRedirectTarget;
|
|
10
10
|
exports.isSessionGradeJwtPayload = isSessionGradeJwtPayload;
|
|
11
|
+
exports.sessionCookieIsSecure = sessionCookieIsSecure;
|
|
11
12
|
/** Browser session cookie carrying the hub JWT. Set by POST /api/auth/session
|
|
12
13
|
* after a tRPC login; read by the addon-route catch-all for `authenticated`
|
|
13
14
|
* routes hit by a plain browser navigation. */
|
|
@@ -105,3 +106,43 @@ function isSessionGradeJwtPayload(payload) {
|
|
|
105
106
|
const p = payload;
|
|
106
107
|
return p.kind === undefined && typeof p.userId === 'string' && typeof p.isAdmin === 'boolean';
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Should the session cookie carry `Secure`, for a request that may have crossed
|
|
111
|
+
* a reverse proxy?
|
|
112
|
+
*
|
|
113
|
+
* The hub decides this from its own leg of the connection, and behind a proxy
|
|
114
|
+
* that leg is the wrong one. Home Assistant's relay reaches the hub over HTTPS
|
|
115
|
+
* while the browser reached Home Assistant over plain HTTP on the LAN, so the
|
|
116
|
+
* hub marked the cookie `Secure` and the browser silently DISCARDED it: the
|
|
117
|
+
* login succeeded, the session existed, and every `<img>` that can only
|
|
118
|
+
* authenticate by cookie came back 401 while the rest of the app — which
|
|
119
|
+
* attaches its bearer token explicitly — worked. That is why the admin UI
|
|
120
|
+
* showed no snapshots inside Home Assistant and everything else looked fine.
|
|
121
|
+
*
|
|
122
|
+
* The relay already SENDS `X-Forwarded-Proto` with the browser's own scheme,
|
|
123
|
+
* next to the prefix header the admin UI's `<base>` depends on. Only this side
|
|
124
|
+
* was missing: the hub read its own socket and never the declaration.
|
|
125
|
+
*
|
|
126
|
+
* Two guards, because `X-Forwarded-Proto` is a client-supplied string:
|
|
127
|
+
*
|
|
128
|
+
* - it may only ever DOWNGRADE. A request that arrived here over plain HTTP
|
|
129
|
+
* can never be talked into claiming HTTPS, so the header cannot be used to
|
|
130
|
+
* make a cookie look safer than its transport.
|
|
131
|
+
* - it is read ONLY when the request also carries the relay's prefix header,
|
|
132
|
+
* which is set by the proxy and by nothing else on a direct browser hit. A
|
|
133
|
+
* direct caller inventing the proto header alone changes nothing.
|
|
134
|
+
*/
|
|
135
|
+
function sessionCookieIsSecure(requestProtocol, headers, forwardedPrefixHeader) {
|
|
136
|
+
const secureLeg = requestProtocol === 'https';
|
|
137
|
+
if (!secureLeg)
|
|
138
|
+
return false;
|
|
139
|
+
if (headers[forwardedPrefixHeader] === undefined)
|
|
140
|
+
return true;
|
|
141
|
+
const raw = headers['x-forwarded-proto'];
|
|
142
|
+
const proto = Array.isArray(raw) ? raw[0] : raw;
|
|
143
|
+
if (proto === undefined)
|
|
144
|
+
return true;
|
|
145
|
+
// A comma list means several hops; the FIRST is the browser's own scheme.
|
|
146
|
+
const browserScheme = proto.split(',')[0]?.trim().toLowerCase();
|
|
147
|
+
return browserScheme !== 'http';
|
|
148
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -894,13 +894,17 @@ async function bootstrap() {
|
|
|
894
894
|
catch {
|
|
895
895
|
return reply.status(401).send({ error: 'invalid token' });
|
|
896
896
|
}
|
|
897
|
-
const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
|
|
897
|
+
const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
|
|
898
|
+
secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
|
|
899
|
+
});
|
|
898
900
|
reply.setCookie(c.name, c.value, c.options);
|
|
899
901
|
return reply.send({ ok: true });
|
|
900
902
|
});
|
|
901
903
|
// DELETE /api/auth/session — clear the cookie on logout.
|
|
902
904
|
fastify.delete('/api/auth/session', async (request, reply) => {
|
|
903
|
-
const c = (0, session_cookie_js_1.clearSessionCookie)({
|
|
905
|
+
const c = (0, session_cookie_js_1.clearSessionCookie)({
|
|
906
|
+
secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
|
|
907
|
+
});
|
|
904
908
|
reply.setCookie(c.name, c.value, c.options);
|
|
905
909
|
return reply.send({ ok: true });
|
|
906
910
|
});
|
|
@@ -930,7 +934,9 @@ async function bootstrap() {
|
|
|
930
934
|
catch {
|
|
931
935
|
return reply.status(401).send({ error: 'invalid token' });
|
|
932
936
|
}
|
|
933
|
-
const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
|
|
937
|
+
const c = (0, session_cookie_js_1.buildSessionCookie)(token, ttlSec, {
|
|
938
|
+
secure: (0, session_cookie_js_1.sessionCookieIsSecure)(request.protocol, request.headers, spa_mount_1.FORWARDED_PREFIX_HEADER),
|
|
939
|
+
});
|
|
934
940
|
reply.setCookie(c.name, c.value, c.options);
|
|
935
941
|
return reply.redirect(next);
|
|
936
942
|
});
|