diff --git a/app/layout/app.tsx b/app/layout/app.tsx index 89a3e5e..07463a2 100644 --- a/app/layout/app.tsx +++ b/app/layout/app.tsx @@ -34,7 +34,7 @@ export async function loader({ request, context, ...rest }: Route.LoaderArgs) { try { const principal = await context.auth.require(request); - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const user = diff --git a/app/routes/acls/acl-action.ts b/app/routes/acls/acl-action.ts index a5fa6c3..5716e8f 100644 --- a/app/routes/acls/acl-action.ts +++ b/app/routes/acls/acl-action.ts @@ -26,7 +26,7 @@ export async function aclAction({ request, context }: Route.ActionArgs) { }); } - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); try { const { policy, updatedAt } = await api.setPolicy(policyData); diff --git a/app/routes/acls/acl-loader.ts b/app/routes/acls/acl-loader.ts index 83faca0..c1af5ae 100644 --- a/app/routes/acls/acl-loader.ts +++ b/app/routes/acls/acl-loader.ts @@ -29,7 +29,7 @@ export async function aclLoader({ request, context }: Route.LoaderArgs) { }; // Try to load the ACL policy from the API. - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); try { const { policy, updatedAt } = await api.getPolicy(); diff --git a/app/routes/auth/oidc-callback.ts b/app/routes/auth/oidc-callback.ts index 761f62e..c555fcf 100644 --- a/app/routes/auth/oidc-callback.ts +++ b/app/routes/auth/oidc-callback.ts @@ -107,7 +107,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { }); try { - const hsApi = context.hsApi.getRuntimeClient(context.oidc!.apiKey); + const hsApi = context.hsApi.getRuntimeClient(context.headscaleApiKey!); const hsUsers = await hsApi.getUsers(); const hsUser = findHeadscaleUserBySubject(hsUsers, claims.sub, userInfo.email); if (hsUser) { diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 2d2a0ad..ec765d5 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -28,7 +28,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { principal.kind === "oidc" && !principal.user.headscaleUserId ) { - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); let headscaleUsers: { id: string; name: string }[] = []; @@ -64,7 +64,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { } // No UI access — show the download/connect page - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); let linkedUserName: string | undefined; diff --git a/app/routes/machines/machine-actions.ts b/app/routes/machines/machine-actions.ts index 7f519c3..452beb3 100644 --- a/app/routes/machines/machine-actions.ts +++ b/app/routes/machines/machine-actions.ts @@ -10,9 +10,7 @@ export async function machineAction({ request, context }: Route.ActionArgs) { const principal = await context.auth.require(request); const formData = await request.formData(); - const api = context.hsApi.getRuntimeClient( - context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey), - ); + const api = context.hsApi.getRuntimeClient(context.auth.getHeadscaleApiKey(principal)); const action = formData.get("action_id")?.toString(); if (!action) { diff --git a/app/routes/machines/machine.tsx b/app/routes/machines/machine.tsx index 4e76ba1..3df43ac 100644 --- a/app/routes/machines/machine.tsx +++ b/app/routes/machines/machine.tsx @@ -38,9 +38,7 @@ export async function loader({ request, params, context }: Route.LoaderArgs) { } } - const api = context.hsApi.getRuntimeClient( - context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey), - ); + const api = context.hsApi.getRuntimeClient(context.auth.getHeadscaleApiKey(principal)); const [nodesSnap, usersSnap] = await Promise.all([ context.hsLive.get(nodesResource, api), context.hsLive.get(usersResource, api), diff --git a/app/routes/machines/overview.tsx b/app/routes/machines/overview.tsx index 58d9b92..a938cc1 100644 --- a/app/routes/machines/overview.tsx +++ b/app/routes/machines/overview.tsx @@ -30,9 +30,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { const writablePermission = context.auth.can(principal, Capabilities.write_machines); - const api = context.hsApi.getRuntimeClient( - context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey), - ); + const api = context.hsApi.getRuntimeClient(context.auth.getHeadscaleApiKey(principal)); const [nodesSnap, usersSnap] = await Promise.all([ context.hsLive.get(nodesResource, api), context.hsLive.get(usersResource, api), diff --git a/app/routes/settings/auth-keys/actions.ts b/app/routes/settings/auth-keys/actions.ts index 8702a8e..06d1a14 100644 --- a/app/routes/settings/auth-keys/actions.ts +++ b/app/routes/settings/auth-keys/actions.ts @@ -7,7 +7,7 @@ import type { Route } from "./+types/overview"; export async function authKeysAction({ request, context }: Route.ActionArgs) { const principal = await context.auth.require(request); - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const canGenerateAny = context.auth.can(principal, Capabilities.generate_authkeys); diff --git a/app/routes/settings/auth-keys/overview.tsx b/app/routes/settings/auth-keys/overview.tsx index 881107d..a75f555 100644 --- a/app/routes/settings/auth-keys/overview.tsx +++ b/app/routes/settings/auth-keys/overview.tsx @@ -20,7 +20,7 @@ import AddAuthKey from "./dialogs/add-auth-key"; export async function loader({ request, context }: Route.LoaderArgs) { const principal = await context.auth.require(request); - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const usersSnap = await context.hsLive.get(usersResource, api); diff --git a/app/routes/ssh/console.tsx b/app/routes/ssh/console.tsx index d703f1c..8198f40 100644 --- a/app/routes/ssh/console.tsx +++ b/app/routes/ssh/console.tsx @@ -41,7 +41,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { throw data("Only OAuth users are allowed to use WebSSH", 403); } - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const users = await api.getUsers(); diff --git a/app/routes/users/overview.tsx b/app/routes/users/overview.tsx index d0d8a62..25b2d79 100644 --- a/app/routes/users/overview.tsx +++ b/app/routes/users/overview.tsx @@ -54,7 +54,7 @@ export async function loader({ request, context }: Route.LoaderArgs) { let apiError: string | undefined; try { - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const [nodesSnap, usersSnap] = await Promise.all([ context.hsLive.get(nodesResource, api), diff --git a/app/routes/users/user-actions.ts b/app/routes/users/user-actions.ts index c261496..33c1a21 100644 --- a/app/routes/users/user-actions.ts +++ b/app/routes/users/user-actions.ts @@ -24,7 +24,7 @@ export async function userAction({ request, context }: Route.ActionArgs) { }); } - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); switch (action) { case "create_user": { diff --git a/app/routes/util/live.ts b/app/routes/util/live.ts index 5f72120..9d7a38c 100644 --- a/app/routes/util/live.ts +++ b/app/routes/util/live.ts @@ -5,7 +5,7 @@ import type { Route } from "./+types/live"; export async function loader({ request, context }: Route.LoaderArgs) { const principal = await context.auth.require(request); - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); // Ensure resources are loaded before streaming diff --git a/app/server/db/pruner.ts b/app/server/db/pruner.ts index 0ad98fc..3d44c6a 100644 --- a/app/server/db/pruner.ts +++ b/app/server/db/pruner.ts @@ -18,7 +18,7 @@ export async function pruneEphemeralNodes({ context, request }: Route.LoaderArgs return; } - const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); + const apiKey = context.auth.getHeadscaleApiKey(principal); const api = context.hsApi.getRuntimeClient(apiKey); const nodes = await api.getNodes(); const toPrune = nodes.filter((node) => { diff --git a/app/server/index.ts b/app/server/index.ts index bf3c825..9389640 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -84,6 +84,7 @@ const appLoadContext = { auth: createAuthService({ secret: config.server.cookie_secret, + headscaleApiKey, db, cookie: { name: "_hp_auth", @@ -93,13 +94,13 @@ const appLoadContext = { }, }), + headscaleApiKey, hsApi, agents, integration: await loadIntegration(config.integration), oidc: config.oidc && config.oidc.enabled !== false && headscaleApiKey ? { - apiKey: headscaleApiKey, connector: createLazyOidcConnector( config.server.base_url, config.oidc, diff --git a/app/server/web/auth.ts b/app/server/web/auth.ts index 539c252..8e489c7 100644 --- a/app/server/web/auth.ts +++ b/app/server/web/auth.ts @@ -58,6 +58,7 @@ interface CookiePayload { export interface AuthServiceOptions { secret: string; + headscaleApiKey?: string; db: NodeSQLiteDatabase; cookie: { name: string; @@ -230,21 +231,16 @@ export class AuthService { return this.encodeCookie({ sid, api_key: apiKey }, Math.floor(maxAge / 1000)); } - /** - * Get the Headscale API key for making API calls. - * OIDC sessions use the configured oidc.headscale_api_key. - * API key sessions use the user-provided key stored in the cookie. - */ - getHeadscaleApiKey(principal: Principal, oidcApiKey?: string): string { + getHeadscaleApiKey(principal: Principal): string { if (principal.kind === "api_key") { return principal.apiKey; } - if (!oidcApiKey) { - throw new Error("OIDC sessions require oidc.headscale_api_key"); + if (!this.opts.headscaleApiKey) { + throw new Error("OIDC sessions require headscale.api_key to be configured"); } - return oidcApiKey; + return this.opts.headscaleApiKey; } /**