Fix Tailscale auth security issues in arc-web

- redirect-home: only check GitHub app config when provider is "github"
- app-shell: hide sign-out button when provider is "tailscale"
- auth-logout: skip session destroy and redirect to /start for Tailscale

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 00:18:58 -05:00
parent 4693c7373c
commit c574fb7416
3 changed files with 51 additions and 42 deletions

View file

@ -44,7 +44,7 @@ export async function loader({ request }: Route.LoaderArgs) {
throw redirect("/setup");
}
const user = await requireUser(request);
return { user };
return { user, provider };
}
const navigation = [
@ -62,7 +62,7 @@ function classNames(...classes: Array<string | false | null | undefined>) {
}
export default function AppShell({ loaderData }: Route.ComponentProps) {
const { user } = loaderData;
const { user, provider } = loaderData;
const { pathname } = useLocation();
const matches = useMatches();
const { theme, toggle } = useTheme();
@ -123,33 +123,35 @@ export default function AppShell({ loaderData }: Route.ComponentProps) {
<ThemeIcon className="size-5" aria-hidden="true" />
<span className="sr-only">Toggle theme</span>
</button>
<Menu as="div" className="relative">
<MenuButton className="relative flex max-w-xs items-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500">
<span className="absolute -inset-1.5" />
<span className="sr-only">Open user menu</span>
<img
alt=""
src={user.avatarUrl}
className="size-8 rounded-full outline -outline-offset-1 outline-line-strong"
/>
</MenuButton>
{provider !== "tailscale" && (
<Menu as="div" className="relative">
<MenuButton className="relative flex max-w-xs items-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500">
<span className="absolute -inset-1.5" />
<span className="sr-only">Open user menu</span>
<img
alt=""
src={user.avatarUrl}
className="size-8 rounded-full outline -outline-offset-1 outline-line-strong"
/>
</MenuButton>
<MenuItems
transition
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<Form method="POST" action="/auth/logout">
<button
type="submit"
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
Sign out
</button>
</Form>
</MenuItem>
</MenuItems>
</Menu>
<MenuItems
transition
className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-panel py-1 outline-1 -outline-offset-1 outline-line-strong transition data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
>
<MenuItem>
<Form method="POST" action="/auth/logout">
<button
type="submit"
className="block w-full px-4 py-2 text-left text-sm text-fg-3 data-focus:bg-overlay data-focus:outline-hidden"
>
Sign out
</button>
</Form>
</MenuItem>
</MenuItems>
</Menu>
)}
</div>
</div>
<div className="-mr-2 flex md:hidden">
@ -219,17 +221,19 @@ export default function AppShell({ loaderData }: Route.ComponentProps) {
<span className="sr-only">Toggle theme</span>
</button>
</div>
<div className="mt-3 space-y-1 px-2">
<Form method="POST" action="/auth/logout">
<DisclosureButton
as="button"
type="submit"
className="block w-full rounded-md px-3 py-2 text-left text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
>
Sign out
</DisclosureButton>
</Form>
</div>
{provider !== "tailscale" && (
<div className="mt-3 space-y-1 px-2">
<Form method="POST" action="/auth/logout">
<DisclosureButton
as="button"
type="submit"
className="block w-full rounded-md px-3 py-2 text-left text-base font-medium text-fg-muted hover:bg-overlay hover:text-fg"
>
Sign out
</DisclosureButton>
</Form>
</div>
)}
</div>
</DisclosurePanel>
</Disclosure>

View file

@ -1,8 +1,13 @@
import { redirect } from "react-router";
import { getAppConfig } from "../lib/config.server";
import { getSession, destroySession } from "../lib/session.server";
import type { Route } from "./+types/auth-logout";
export async function action({ request }: Route.ActionArgs) {
const { provider } = getAppConfig().web.auth;
if (provider === "tailscale") {
return redirect("/start");
}
const session = await getSession(request);
return redirect("/auth/login", {
headers: { "Set-Cookie": await destroySession(session) },

View file

@ -5,11 +5,11 @@ import { getUser } from "../lib/session.server";
import type { Route } from "./+types/redirect-home";
export async function loader({ request }: Route.LoaderArgs) {
const authDisabled = getAppConfig().web.auth.provider === "insecure_disabled";
if (!authDisabled && !isGitHubAppConfigured()) {
const { provider } = getAppConfig().web.auth;
if (provider === "github" && !isGitHubAppConfigured()) {
return redirect("/setup");
}
if (!authDisabled) {
if (provider !== "insecure_disabled") {
const user = await getUser(request);
if (!user) {
return redirect("/auth/login");