Skip to content
204 changes: 203 additions & 1 deletion apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";

import * as DesktopBackendPool from "./DesktopBackendPool.ts";
import * as DesktopLocalEnvironmentAuth from "./DesktopLocalEnvironmentAuth.ts";
import * as DesktopLocalEnvironmentAuthTokenStore from "./DesktopLocalEnvironmentAuthTokenStore.ts";

const config = {
executablePath: "/electron",
Expand All @@ -29,10 +30,97 @@ const config = {
captureOutput: true,
};

type SessionValidationResult = "authenticated" | "unauthenticated" | "error";

const readToken = DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth.pipe(
Effect.flatMap((auth) => auth.getBearerToken),
);

function makePersistedSessionTestLayer(input: {
readonly requests: Ref.Ref<ReadonlyArray<string>>;
readonly persistedToken: Ref.Ref<Option.Option<string>>;
readonly tokenStoreOperations: Ref.Ref<ReadonlyArray<string>>;
readonly tokenStoreReadError?: DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStoreError;
readonly sessionValidationResult: SessionValidationResult;
}) {
const httpClientLayer = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
const response = request.url.endsWith("/oauth/token")
? Response.json({
access_token: "desktop-bearer-token",
issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
token_type: "Bearer",
expires_in: 3600,
scope: "orchestration:read",
})
: input.sessionValidationResult === "error"
? Response.json({ error: "temporary failure" }, { status: 500 })
: Response.json({
authenticated: input.sessionValidationResult === "authenticated",
auth: {
policy: "desktop-managed-local",
bootstrapMethods: ["desktop-bootstrap"],
sessionMethods: ["bearer-access-token"],
sessionCookieName: "t3_session",
},
...(input.sessionValidationResult === "authenticated"
? {
scopes: ["orchestration:read"],
sessionMethod: "bearer-access-token",
expiresAt: "2026-08-01T00:00:00.000Z",
}
: {}),
});
return Ref.update(input.requests, (current) => [...current, request.url]).pipe(
Effect.as(HttpClientResponse.fromWeb(request, response)),
);
}),
);
const poolLayer = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
list: Effect.succeed([
{
id: PRIMARY_LOCAL_ENVIRONMENT_ID,
label: Effect.succeed("Windows"),
currentConfig: Effect.succeed(Option.some(config)),
},
]),
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
const tokenStoreLayer = Layer.succeed(
DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStore,
{
get: Ref.update(input.tokenStoreOperations, (operations) => [...operations, "get"]).pipe(
Effect.andThen(
input.tokenStoreReadError === undefined
? Ref.get(input.persistedToken)
: Effect.fail(input.tokenStoreReadError),
),
),
set: (token) =>
Effect.gen(function* () {
yield* Ref.update(input.tokenStoreOperations, (operations) => [
...operations,
`set:${token}`,
]);
yield* Ref.set(input.persistedToken, Option.some(token));
return true;
}),
clear: Ref.update(input.tokenStoreOperations, (operations) => [...operations, "clear"]).pipe(
Effect.andThen(Ref.set(input.persistedToken, Option.none())),
),
},
);

return DesktopLocalEnvironmentAuth.layer.pipe(
Layer.provide(Layer.mergeAll(poolLayer, httpClientLayer, tokenStoreLayer)),
);
}

describe("DesktopLocalEnvironmentAuth", () => {
it.effect("exchanges the desktop bootstrap credential only once", () =>
Effect.gen(function* () {
const requestCount = yield* Ref.make(0);
const persistedToken = yield* Ref.make(Option.none<string>());
const httpClientLayer = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) =>
Expand Down Expand Up @@ -64,8 +152,16 @@ describe("DesktopLocalEnvironmentAuth", () => {
},
]),
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
const tokenStoreLayer = Layer.succeed(
DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStore,
{
get: Ref.get(persistedToken),
set: (token) => Ref.set(persistedToken, Option.some(token)).pipe(Effect.as(true)),
clear: Ref.set(persistedToken, Option.none()),
},
);
const testLayer = DesktopLocalEnvironmentAuth.layer.pipe(
Layer.provide(Layer.mergeAll(poolLayer, httpClientLayer)),
Layer.provide(Layer.mergeAll(poolLayer, httpClientLayer, tokenStoreLayer)),
);

const [first, second] = yield* Effect.gen(function* () {
Expand All @@ -76,6 +172,112 @@ describe("DesktopLocalEnvironmentAuth", () => {
assert.strictEqual(first, "desktop-bearer-token");
assert.strictEqual(second, "desktop-bearer-token");
assert.strictEqual(yield* Ref.get(requestCount), 1);
assert.deepStrictEqual(yield* Ref.get(persistedToken), Option.some("desktop-bearer-token"));
}),
);

it.effect("reuses a persisted bearer session after the desktop service restarts", () =>
Effect.gen(function* () {
const requests = yield* Ref.make<ReadonlyArray<string>>([]);
const persistedToken = yield* Ref.make(Option.none<string>());
const tokenStoreOperations = yield* Ref.make<ReadonlyArray<string>>([]);
const testLayer = makePersistedSessionTestLayer({
requests,
persistedToken,
tokenStoreOperations,
sessionValidationResult: "authenticated",
});

assert.strictEqual(yield* readToken.pipe(Effect.provide(testLayer)), "desktop-bearer-token");
assert.strictEqual(yield* readToken.pipe(Effect.provide(testLayer)), "desktop-bearer-token");
assert.deepStrictEqual(yield* Ref.get(requests), [
"http://127.0.0.1:3773/oauth/token",
"http://127.0.0.1:3773/api/auth/session",
]);
assert.deepStrictEqual(yield* Ref.get(tokenStoreOperations), [
"get",
"set:desktop-bearer-token",
"get",
]);
assert.deepStrictEqual(yield* Ref.get(persistedToken), Option.some("desktop-bearer-token"));
}),
);

it.effect("replaces an unauthenticated persisted bearer session", () =>
Effect.gen(function* () {
const requests = yield* Ref.make<ReadonlyArray<string>>([]);
const persistedToken = yield* Ref.make(Option.some("stale-bearer-token"));
const tokenStoreOperations = yield* Ref.make<ReadonlyArray<string>>([]);
const testLayer = makePersistedSessionTestLayer({
requests,
persistedToken,
tokenStoreOperations,
sessionValidationResult: "unauthenticated",
});

assert.strictEqual(yield* readToken.pipe(Effect.provide(testLayer)), "desktop-bearer-token");
assert.deepStrictEqual(yield* Ref.get(requests), [
"http://127.0.0.1:3773/api/auth/session",
"http://127.0.0.1:3773/oauth/token",
]);
assert.deepStrictEqual(yield* Ref.get(tokenStoreOperations), [
"get",
"clear",
"set:desktop-bearer-token",
]);
assert.deepStrictEqual(yield* Ref.get(persistedToken), Option.some("desktop-bearer-token"));
}),
);

it.effect("preserves a persisted token when session validation fails transiently", () =>
Effect.gen(function* () {
const requests = yield* Ref.make<ReadonlyArray<string>>([]);
const persistedToken = yield* Ref.make(Option.some("desktop-bearer-token"));
const tokenStoreOperations = yield* Ref.make<ReadonlyArray<string>>([]);
const testLayer = makePersistedSessionTestLayer({
requests,
persistedToken,
tokenStoreOperations,
sessionValidationResult: "error",
});

const error = yield* readToken.pipe(Effect.provide(testLayer), Effect.flip);

assert.instanceOf(
error,
DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuthSessionValidationError,
);
assert.deepStrictEqual(yield* Ref.get(requests), ["http://127.0.0.1:3773/api/auth/session"]);
assert.deepStrictEqual(yield* Ref.get(tokenStoreOperations), ["get"]);
assert.deepStrictEqual(yield* Ref.get(persistedToken), Option.some("desktop-bearer-token"));
}),
);

it.effect("does not create a session when reading the persisted token fails", () =>
Effect.gen(function* () {
const requests = yield* Ref.make<ReadonlyArray<string>>([]);
const persistedToken = yield* Ref.make(Option.some("desktop-bearer-token"));
const tokenStoreOperations = yield* Ref.make<ReadonlyArray<string>>([]);
const tokenStoreReadError =
new DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStoreError({
operation: "read",
path: "/tmp/t3/desktop-local-auth.json",
cause: new Error("temporary read failure"),
});
const testLayer = makePersistedSessionTestLayer({
requests,
persistedToken,
tokenStoreOperations,
tokenStoreReadError,
sessionValidationResult: "authenticated",
});

const error = yield* readToken.pipe(Effect.provide(testLayer), Effect.flip);

assert.strictEqual(error, tokenStoreReadError);
assert.deepStrictEqual(yield* Ref.get(requests), []);
assert.deepStrictEqual(yield* Ref.get(tokenStoreOperations), ["get"]);
assert.deepStrictEqual(yield* Ref.get(persistedToken), Option.some("desktop-bearer-token"));
}),
);
});
53 changes: 52 additions & 1 deletion apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { bootstrapRemoteBearerSession } from "@t3tools/client-runtime/authorization";
import {
bootstrapRemoteBearerSession,
fetchRemoteSessionState,
} from "@t3tools/client-runtime/authorization";
import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
Expand All @@ -10,6 +13,7 @@ import * as Semaphore from "effect/Semaphore";
import * as HttpClient from "effect/unstable/http/HttpClient";

import * as DesktopBackendPool from "./DesktopBackendPool.ts";
import * as DesktopLocalEnvironmentAuthTokenStore from "./DesktopLocalEnvironmentAuthTokenStore.ts";

export class DesktopLocalEnvironmentAuthBackendNotConfiguredError extends Schema.TaggedErrorClass<DesktopLocalEnvironmentAuthBackendNotConfiguredError>()(
"DesktopLocalEnvironmentAuthBackendNotConfiguredError",
Expand All @@ -29,9 +33,20 @@ export class DesktopLocalEnvironmentAuthSessionBootstrapError extends Schema.Tag
}
}

export class DesktopLocalEnvironmentAuthSessionValidationError extends Schema.TaggedErrorClass<DesktopLocalEnvironmentAuthSessionValidationError>()(
"DesktopLocalEnvironmentAuthSessionValidationError",
{ cause: Schema.Defect() },
) {
override get message(): string {
return "Failed to validate the saved local desktop bearer session.";
}
}

export const DesktopLocalEnvironmentAuthError = Schema.Union([
DesktopLocalEnvironmentAuthBackendNotConfiguredError,
DesktopLocalEnvironmentAuthSessionBootstrapError,
DesktopLocalEnvironmentAuthSessionValidationError,
DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStoreError,
]);
export type DesktopLocalEnvironmentAuthError = typeof DesktopLocalEnvironmentAuthError.Type;

Expand All @@ -44,6 +59,8 @@ export class DesktopLocalEnvironmentAuth extends Context.Service<

export const make = Effect.gen(function* () {
const pool = yield* DesktopBackendPool.DesktopBackendPool;
const tokenStore =
yield* DesktopLocalEnvironmentAuthTokenStore.DesktopLocalEnvironmentAuthTokenStore;
const httpClient = yield* HttpClient.HttpClient;
const tokenRef = yield* Ref.make(Option.none<string>());
const mutex = yield* Semaphore.make(1);
Expand All @@ -63,6 +80,33 @@ export const make = Effect.gen(function* () {
return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError();
}
const config = configOption.value;
const persistedToken = yield* tokenStore.get;
if (Option.isSome(persistedToken)) {
const session = yield* fetchRemoteSessionState({
httpBaseUrl: config.httpBaseUrl.href,
bearerToken: persistedToken.value,
}).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.mapError(
(cause) =>
new DesktopLocalEnvironmentAuthSessionValidationError({
cause,
}),
),
);
if (session.authenticated) {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
yield* Ref.set(tokenRef, persistedToken);
return persistedToken.value;
}
yield* tokenStore.clear.pipe(
Effect.catch((error) =>
Effect.logWarning("Could not clear the stale desktop authorization token.", {
error,
}),
),
);
}
Comment thread
cursor[bot] marked this conversation as resolved.

const credential = config.bootstrap.desktopBootstrapToken;
if (!credential) {
return yield* new DesktopLocalEnvironmentAuthBackendNotConfiguredError();
Expand All @@ -83,6 +127,13 @@ export const make = Effect.gen(function* () {
}),
),
);
yield* tokenStore.set(session.access_token).pipe(
Effect.catch((error) =>
Effect.logWarning("Could not save the desktop authorization token.", {
error,
}).pipe(Effect.as(false)),
),
);
yield* Ref.set(tokenRef, Option.some(session.access_token));
return session.access_token;
}),
Expand Down
Loading
Loading