History

Stabilize auth session cookies per server mode (#1898)

Julius Marminge committed 7 months ago

Commit 4ae9de31 parent 5fa09fa11 files · +212 −22Browse files

Showing the changes to apps/server/src (6 of 11 files). Show all

Unified · Split

6 files
  1. apps/server/src/auth/Layers/ServerAuth.ts +11 −1
  2. apps/server/src/auth/Layers/ServerAuthPolicy.test.ts +3 −0
  3. apps/server/src/auth/Layers/ServerAuthPolicy.ts +5 −2
  4. apps/server/src/auth/Layers/SessionCredentialService.ts +8 −2
  5. apps/server/src/auth/utils.ts +12 −1
  6. apps/server/src/server.test.ts +1 −1
apps/server/src/auth/Layers/ServerAuth.ts+11−1
@@ -20,7 +20,10 @@ import { · 19 unmodified lines
20 AuthError,20 AuthError,
21 type ServerAuthShape,21 type ServerAuthShape,
22} from "../Services/ServerAuth.ts";22} from "../Services/ServerAuth.ts";
23import { SessionCredentialService } from "../Services/SessionCredentialService.ts";23import {
24 SessionCredentialError,
25 SessionCredentialService,
26} from "../Services/SessionCredentialService.ts";
24import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";27import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";
25 28
26type BootstrapExchangeResult = {29type BootstrapExchangeResult = {
@@ -65,6 +68,13 @@ export const makeServerAuth = Effect.gen(function* () { · 38 unmodified lines
65 68
66 const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>69 const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>
67 sessions.verify(token).pipe(70 sessions.verify(token).pipe(
71 Effect.tapError((cause: SessionCredentialError) =>
72 Effect.logWarning("Rejected authenticated session credential.").pipe(
73 Effect.annotateLogs({
74 reason: cause.message,
75 }),
76 ),
77 ),
68 Effect.map((session) => ({78 Effect.map((session) => ({
69 sessionId: session.sessionId,79 sessionId: session.sessionId,
70 subject: session.subject,80 subject: session.subject,
apps/server/src/auth/Layers/ServerAuthPolicy.test.ts+3−0
@@ -33,10 +33,12 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => { · 32 unmodified lines
33 33
34 expect(descriptor.policy).toBe("desktop-managed-local");34 expect(descriptor.policy).toBe("desktop-managed-local");
35 expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap"]);35 expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap"]);
36 expect(descriptor.sessionCookieName).toBe("t3_session_3773");
36 }).pipe(37 }).pipe(
37 Effect.provide(38 Effect.provide(
38 makeServerAuthPolicyLayer({39 makeServerAuthPolicyLayer({
39 mode: "desktop",40 mode: "desktop",
41 port: 3773,
40 }),42 }),
41 ),43 ),
42 ),44 ),
@@ -66,6 +68,7 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => { · 23 unmodified lines
66 68
67 expect(descriptor.policy).toBe("loopback-browser");69 expect(descriptor.policy).toBe("loopback-browser");
68 expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]);70 expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]);
71 expect(descriptor.sessionCookieName).toBe("t3_session");
69 }).pipe(72 }).pipe(
70 Effect.provide(73 Effect.provide(
71 makeServerAuthPolicyLayer({74 makeServerAuthPolicyLayer({
apps/server/src/auth/Layers/ServerAuthPolicy.ts+5−2
@@ -3,7 +3,7 @@ import { Effect, Layer } from "effect"; · 2 unmodified lines
3 3
4import { ServerConfig } from "../../config.ts";4import { ServerConfig } from "../../config.ts";
5import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";5import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
6import { SESSION_COOKIE_NAME } from "../utils.ts";6import { resolveSessionCookieName } from "../utils.ts";
7import { isLoopbackHost, isWildcardHost } from "../../startupAccess.ts";7import { isLoopbackHost, isWildcardHost } from "../../startupAccess.ts";
8 8
9export const makeServerAuthPolicy = Effect.gen(function* () {9export const makeServerAuthPolicy = Effect.gen(function* () {
@@ -30,7 +30,10 @@ export const makeServerAuthPolicy = Effect.gen(function* () { · 20 unmodified lines
30 policy,30 policy,
31 bootstrapMethods,31 bootstrapMethods,
32 sessionMethods: ["browser-session-cookie", "bearer-session-token"],32 sessionMethods: ["browser-session-cookie", "bearer-session-token"],
33 sessionCookieName: SESSION_COOKIE_NAME,33 sessionCookieName: resolveSessionCookieName({
34 mode: config.mode,
35 port: config.port,
36 }),
34 };37 };
35 38
36 return {39 return {
apps/server/src/auth/Layers/SessionCredentialService.ts+8−2
@@ -2,10 +2,10 @@ import { AuthSessionId, type AuthClientMetadata, type AuthClientSession } from " · 1 unmodified line
2import { Clock, DateTime, Duration, Effect, Layer, PubSub, Ref, Schema, Stream } from "effect";2import { Clock, DateTime, Duration, Effect, Layer, PubSub, Ref, Schema, Stream } from "effect";
3import { Option } from "effect";3import { Option } from "effect";
4 4
5import { ServerConfig } from "../../config.ts";
5import { AuthSessionRepositoryLive } from "../../persistence/Layers/AuthSessions.ts";6import { AuthSessionRepositoryLive } from "../../persistence/Layers/AuthSessions.ts";
6import { AuthSessionRepository } from "../../persistence/Services/AuthSessions.ts";7import { AuthSessionRepository } from "../../persistence/Services/AuthSessions.ts";
7import { ServerSecretStore } from "../Services/ServerSecretStore.ts";8import { ServerSecretStore } from "../Services/ServerSecretStore.ts";
8import { SESSION_COOKIE_NAME } from "../utils.ts";
9import {9import {
10 SessionCredentialError,10 SessionCredentialError,
11 SessionCredentialService,11 SessionCredentialService,
@@ -17,6 +17,7 @@ import { · 5 unmodified lines
17import {17import {
18 base64UrlDecodeUtf8,18 base64UrlDecodeUtf8,
19 base64UrlEncode,19 base64UrlEncode,
20 resolveSessionCookieName,
20 signPayload,21 signPayload,
21 timingSafeEqualBase64Url,22 timingSafeEqualBase64Url,
22} from "../utils.ts";23} from "../utils.ts";
@@ -81,11 +82,16 @@ function toAuthClientSession(input: Omit<AuthClientSession, "current">): AuthCli · 58 unmodified lines
81}82}
82 83
83export const makeSessionCredentialService = Effect.gen(function* () {84export const makeSessionCredentialService = Effect.gen(function* () {
85 const serverConfig = yield* ServerConfig;
84 const secretStore = yield* ServerSecretStore;86 const secretStore = yield* ServerSecretStore;
85 const authSessions = yield* AuthSessionRepository;87 const authSessions = yield* AuthSessionRepository;
86 const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32);88 const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32);
87 const connectedSessionsRef = yield* Ref.make(new Map<string, number>());89 const connectedSessionsRef = yield* Ref.make(new Map<string, number>());
88 const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>();90 const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>();
91 const cookieName = resolveSessionCookieName({
92 mode: serverConfig.mode,
93 port: serverConfig.port,
94 });
89 95
90 const toSessionCredentialError = (message: string) => (cause: unknown) =>96 const toSessionCredentialError = (message: string) => (cause: unknown) =>
91 new SessionCredentialError({97 new SessionCredentialError({
@@ -472,7 +478,7 @@ export const makeSessionCredentialService = Effect.gen(function* () { · 380 unmodified lines
472 }).pipe(Effect.mapError(toSessionCredentialError("Failed to revoke other sessions.")));478 }).pipe(Effect.mapError(toSessionCredentialError("Failed to revoke other sessions.")));
473 479
474 return {480 return {
475 cookieName: SESSION_COOKIE_NAME,481 cookieName,
476 issue,482 issue,
477 verify,483 verify,
478 issueWebSocketToken,484 issueWebSocketToken,
apps/server/src/auth/utils.ts+12−1
@@ -2,7 +2,18 @@ import type { AuthClientMetadata, AuthClientMetadataDeviceType } from "@t3tools/ · 1 unmodified line
2import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";2import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
3import * as Crypto from "node:crypto";3import * as Crypto from "node:crypto";
4 4
5export const SESSION_COOKIE_NAME = "t3_session";5const SESSION_COOKIE_NAME = "t3_session";
6
7export function resolveSessionCookieName(input: {
8 readonly mode: "web" | "desktop";
9 readonly port: number;
10}): string {
11 if (input.mode !== "desktop") {
12 return SESSION_COOKIE_NAME;
13 }
14
15 return `${SESSION_COOKIE_NAME}_${input.port}`;
16}
6 17
7export function base64UrlEncode(input: string | Uint8Array): string {18export function base64UrlEncode(input: string | Uint8Array): string {
8 const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);19 const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input);
apps/server/src/server.test.ts+1−1
@@ -789,7 +789,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { · 788 unmodified lines
789 "browser-session-cookie",789 "browser-session-cookie",
790 "bearer-session-token",790 "bearer-session-token",
791 ]);791 ]);
792 assert.equal(body.auth.sessionCookieName, "t3_session");792 assert.isTrue(body.auth.sessionCookieName.startsWith("t3_session_"));
793 }).pipe(Effect.provide(NodeHttpServer.layerTest)),793 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
794 );794 );
795 795

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t