History

fix(server): migrate legacy web sessions

Bil0000 committed 2 months ago

Commit 2ca78081 parent a0e7e225 files · +73 −15Browse files

Unified · Split

5 files
  1. apps/server/src/auth/EnvironmentAuth.ts +3 −1
  2. apps/server/src/auth/SessionStore.ts +7 −2
  3. apps/server/src/auth/http.ts +35 −12
  4. apps/server/src/auth/utils.ts +10 −0
  5. apps/server/src/server.test.ts +18 −0
apps/server/src/auth/EnvironmentAuth.ts+3−1
@@ -600,7 +600,9 @@ export const make = Effect.gen(function* () { · 599 unmodified lines
600600 const authenticateRequest = (
601601 request: HttpServerRequest.HttpServerRequest,
602602 ): Effect.Effect<AuthenticatedSession, ServerAuthCredentialError | ServerAuthInternalError> => {
603− const cookieToken = request.cookies[sessions.cookieName];
603+ const cookieToken =
604+ request.cookies[sessions.cookieName] ??
605+ (sessions.legacyCookieName ? request.cookies[sessions.legacyCookieName] : undefined);
604606 const bearerToken = parseBearerToken(request);
605607 const dpopToken = parseDpopToken(request);
606608 const credential = cookieToken ?? bearerToken ?? dpopToken;
apps/server/src/auth/SessionStore.ts+7−2
@@ -26,6 +26,7 @@ import * as ServerSecretStore from "./ServerSecretStore.ts"; · 25 unmodified lines
2626 import {
2727 base64UrlDecodeUtf8,
2828 base64UrlEncode,
29+ resolveLegacySessionCookieName,
2930 resolveSessionCookieName,
3031 signPayload,
3132 timingSafeEqualBase64Url,
@@ -360,6 +361,7 @@ export class SessionStore extends Context.Service< · 328 unmodified lines
360361 SessionStore,
361362 {
362363 readonly cookieName: string;
364+ readonly legacyCookieName: string | undefined;
363365 readonly issue: (input?: {
364366 readonly ttl?: Duration.Duration;
365367 readonly subject?: string;
@@ -475,13 +477,15 @@ export const make = Effect.gen(function* () { · 109 unmodified lines
475477 const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32);
476478 const connectedSessionsRef = yield* Ref.make(new Map<string, number>());
477479 const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>();
478− const cookieName = resolveSessionCookieName({
480+ const cookieInput = {
479481 mode: serverConfig.mode,
480482 port: serverConfig.port,
481483 host: serverConfig.host,
482484 instanceKey: serverConfig.stateDir,
483485 development: serverConfig.devUrl !== undefined,
484− });
486+ } as const;
487+ const cookieName = resolveSessionCookieName(cookieInput);
488+ const legacyCookieName = resolveLegacySessionCookieName(cookieInput);
485489
486490 const emitUpsert = (clientSession: AuthClientSession) =>
487491 PubSub.publish(changesPubSub, {
@@ -930,6 +934,7 @@ export const make = Effect.gen(function* () { · 442 unmodified lines
930934
931935 return SessionStore.of({
932936 cookieName,
937+ legacyCookieName,
933938 issue,
934939 verify,
935940 issueWebSocketToken,
apps/server/src/auth/http.ts+35−12
@@ -171,6 +171,23 @@ export function failEnvironmentInternal(reason: EnvironmentInternalErrorReason, · 170 unmodified lines
171171 });
172172 }
173173
174+const appendSessionCookie = (cookieName: string, token: string, expiresAt: DateTime.DateTime) =>
175+ Effect.fromResult(
176+ Cookies.set(Cookies.empty, cookieName, token, {
177+ expires: DateTime.toDate(expiresAt),
178+ httpOnly: true,
179+ path: "/",
180+ sameSite: "lax",
181+ }),
182+ ).pipe(
183+ Effect.catch(() => failEnvironmentInternal("browser_session_cookie_failed")),
184+ Effect.flatMap((cookies) =>
185+ HttpEffect.appendPreResponseHandler((_request, response) =>
186+ Effect.succeed(HttpServerResponse.mergeCookies(response, cookies)),
187+ ),
188+ ),
189+ );
190+
174191 export const requireEnvironmentScope = Effect.fn("environment.auth.requireScope")(function* (
175192 scope: AuthEnvironmentScope,
176193 ) {
@@ -224,7 +241,20 @@ export const authHttpApiLayer = HttpApiBuilder.group( · 47 unmodified lines
224241 function* (args) {
225242 yield* annotateEnvironmentRequest(args.endpoint.name);
226243 const request = yield* HttpServerRequest.HttpServerRequest;
227− return yield* serverAuth.getSessionState(request);
244+ const result = yield* serverAuth.getSessionState(request);
245+ const legacyToken =
246+ sessions.legacyCookieName && !request.cookies[sessions.cookieName]
247+ ? request.cookies[sessions.legacyCookieName]
248+ : undefined;
249+ if (
250+ legacyToken &&
251+ result.authenticated &&
252+ result.sessionMethod === "browser-session-cookie" &&
253+ result.expiresAt
254+ ) {
255+ yield* appendSessionCookie(sessions.cookieName, legacyToken, result.expiresAt);
256+ }
257+ return result;
228258 },
229259 Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) =>
230260 failEnvironmentInternal("internal_error", error),
@@ -241,17 +271,10 @@ export const authHttpApiLayer = HttpApiBuilder.group( · 10 unmodified lines
241271 args.payload.credential,
242272 deriveAuthClientMetadata({ request }),
243273 );
244− const sessionCookies = yield* Effect.fromResult(
245− Cookies.set(Cookies.empty, sessions.cookieName, result.sessionToken, {
246− expires: DateTime.toDate(result.response.expiresAt),
247− httpOnly: true,
248− path: "/",
249− sameSite: "lax",
250− }),
251− ).pipe(Effect.catch(() => failEnvironmentInternal("browser_session_cookie_failed")));
252−
253− yield* HttpEffect.appendPreResponseHandler((_request, response) =>
254− Effect.succeed(HttpServerResponse.mergeCookies(response, sessionCookies)),
274+ yield* appendSessionCookie(
275+ sessions.cookieName,
276+ result.sessionToken,
277+ result.response.expiresAt,
255278 );
256279 yield* appendCredentialResponseHeaders;
257280 return result.response;
apps/server/src/auth/utils.ts+10−0
@@ -48,6 +48,16 @@ export function resolveSessionCookieName(input: { · 47 unmodified lines
4848 return `${SESSION_COOKIE_NAME}_${input.port}_${instanceHash}`;
4949 }
5050
51+export function resolveLegacySessionCookieName(input: {
52+ readonly mode: "web" | "desktop";
53+ readonly host: string | undefined;
54+ readonly development: boolean;
55+}): string | undefined {
56+ return input.mode === "web" && !input.development && isRemoteReachableHost(input.host)
57+ ? SESSION_COOKIE_NAME
58+ : undefined;
59+}
60+
5161 export function isRemoteReachableHost(host: string | undefined): boolean {
5262 if (host === "0.0.0.0" || host === "::" || host === "[::]") {
5363 return true;
apps/server/src/server.test.ts+18−0
@@ -1638,6 +1638,24 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 1637 unmodified lines
16381638 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
16391639 );
16401640
1641+ it.effect("migrates a valid legacy remote-web session cookie", () =>
1642+ Effect.gen(function* () {
1643+ yield* buildAppUnderTest({ config: { mode: "web", host: "192.168.1.50" } });
1644+
1645+ const { cookie } = yield* bootstrapBrowserSession();
1646+ const currentCookie = cookie?.split(";")[0] ?? "";
1647+ const legacyCookie = currentCookie.replace(/^t3_session_[^=]+=/, "t3_session=");
1648+ const sessionUrl = yield* getHttpServerUrl("/api/auth/session");
1649+ const response = yield* fetchEffect(sessionUrl, {
1650+ headers: { cookie: legacyCookie },
1651+ });
1652+ const body = yield* responseJsonEffect<{ readonly authenticated: boolean }>(response);
1653+
1654+ assert.equal(body.authenticated, true);
1655+ assert.match(response.headers["set-cookie"] ?? "", /^t3_session_[a-f0-9]{12}=/);
1656+ }).pipe(Effect.provide(NodeHttpServer.layerTest)),
1657+ );
1658+
16411659 it.effect("exchanges a bootstrap grant for a scoped bearer access token", () =>
16421660 Effect.gen(function* () {
16431661 yield* buildAppUnderTest();