feat: completely overhaul the auth model

* Cookies are now encrypted JWTs (GHSA-wrqq-v7qw-r5w7)
* Authentication is stored in the SQLite database (auto-migrated)
* Session logic is much cleaner
This commit is contained in:
Aarnav Tale 2025-08-19 17:49:32 -04:00
parent 8cb91cd45b
commit d2c4f5eb2b
No known key found for this signature in database
29 changed files with 628 additions and 505 deletions

View file

@ -14,7 +14,7 @@ export async function machineAction({
);
const formData = await request.formData();
const apiKey = session.get('api_key')!;
const apiKey = session.api_key;
const action = formData.get('action_id')?.toString();
if (!action) {
@ -55,7 +55,7 @@ export async function machineAction({
}
if (
node.user.providerId?.split('/').pop() !== session.get('user')!.subject &&
node.user.providerId?.split('/').pop() !== session.user.subject &&
!check
) {
throw data('You do not have permission to act on this machine', {

View file

@ -39,9 +39,9 @@ export async function loader({
const [machine, { users }] = await Promise.all([
context.client.get<{ node: Machine }>(
`v1/node/${params.id}`,
session.get('api_key')!,
session.api_key,
),
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
context.client.get<{ users: User[] }>('v1/user', session.api_key),
]);
const lookup = await context.agents?.lookup([machine.node.nodeKey]);
@ -77,7 +77,7 @@ export default function Page() {
return (
<div>
<p className="mb-8 text-md">
<RemixLink to="/machines" className="font-medium">
<RemixLink className="font-medium" to="/machines">
All Machines
</RemixLink>
<span className="mx-2">/</span>
@ -91,9 +91,9 @@ export default function Page() {
>
<span className="flex items-baseline gap-x-4 text-sm">
<h1 className="text-2xl font-medium">{node.givenName}</h1>
<StatusCircle isOnline={node.online} className="w-4 h-4" />
<StatusCircle className="w-4 h-4" isOnline={node.online} />
</span>
<MenuOptions isFullButton node={node} users={users} magic={magic} />
<MenuOptions isFullButton magic={magic} node={node} users={users} />
</div>
<div className="flex gap-1 mb-4">
<div className="border-r border-headplane-100 dark:border-headplane-800 p-2 pr-4">
@ -123,14 +123,14 @@ export default function Page() {
</div>
</div>
</div>
<Routes node={node} isOpen={showRouting} setIsOpen={setShowRouting} />
<Routes isOpen={showRouting} node={node} setIsOpen={setShowRouting} />
<h2 className="text-xl font-medium mt-8">Subnets & Routing</h2>
<div className="flex items-center justify-between mb-4">
<p>
Subnets let you expose physical network routes onto Tailscale.{' '}
<Link
to="https://tailscale.com/kb/1019/subnets"
name="Tailscale Subnets Documentation"
to="https://tailscale.com/kb/1019/subnets"
>
Learn More
</Link>
@ -138,11 +138,11 @@ export default function Page() {
<Button onPress={() => setShowRouting(true)}>Review</Button>
</div>
<Card
variant="flat"
className={cn(
'w-full max-w-full grid sm:grid-cols-2',
'md:grid-cols-4 gap-8 mr-2 text-sm mb-8',
)}
variant="flat"
>
<div>
<span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
@ -166,11 +166,11 @@ export default function Page() {
)}
</div>
<Button
onPress={() => setShowRouting(true)}
className={cn(
'px-1.5 py-0.5 rounded-md mt-1.5',
'text-blue-500 dark:text-blue-400',
)}
onPress={() => setShowRouting(true)}
>
Edit
</Button>
@ -198,11 +198,11 @@ export default function Page() {
)}
</div>
<Button
onPress={() => setShowRouting(true)}
className={cn(
'px-1.5 py-0.5 rounded-md mt-1.5',
'text-blue-500 dark:text-blue-400',
)}
onPress={() => setShowRouting(true)}
>
Edit
</Button>
@ -233,11 +233,11 @@ export default function Page() {
)}
</div>
<Button
onPress={() => setShowRouting(true)}
className={cn(
'px-1.5 py-0.5 rounded-md mt-1.5',
'text-blue-500 dark:text-blue-400',
)}
onPress={() => setShowRouting(true)}
>
Edit
</Button>
@ -249,15 +249,15 @@ export default function Page() {
issues.
</p>
<Card
variant="flat"
className="w-full max-w-full grid grid-cols-1 lg:grid-cols-2 gap-y-2 sm:gap-x-12"
variant="flat"
>
<div className="flex flex-col gap-1">
<Attribute name="Creator" value={node.user.name || node.user.email} />
<Attribute name="Machine name" value={node.givenName} />
<Attribute
tooltip="OS hostname is published by the machines operating system and is used as the default name for the machine."
name="OS hostname"
tooltip="OS hostname is published by the machines operating system and is used as the default name for the machine."
value={node.name}
/>
{stats ? (
@ -267,14 +267,14 @@ export default function Page() {
</>
) : undefined}
<Attribute
tooltip="ID for this machine. Used in the Headscale API."
name="ID"
tooltip="ID for this machine. Used in the Headscale API."
value={node.id}
/>
<Attribute
isCopyable
tooltip="Public key which uniquely identifies this machine."
name="Node key"
tooltip="Public key which uniquely identifies this machine."
value={node.nodeKey}
/>
<Attribute
@ -311,27 +311,27 @@ export default function Page() {
</p>
<Attribute
isCopyable
tooltip="This machines IPv4 address within your tailnet (your private Tailscale network)."
name="Tailscale IPv4"
tooltip="This machines IPv4 address within your tailnet (your private Tailscale network)."
value={getIpv4Address(node.ipAddresses)}
/>
<Attribute
isCopyable
tooltip="This machines IPv6 address within your tailnet (your private Tailscale network). Connections within your tailnet support IPv6 even if your ISP does not."
name="Tailscale IPv6"
tooltip="This machines IPv6 address within your tailnet (your private Tailscale network). Connections within your tailnet support IPv6 even if your ISP does not."
value={getIpv6Address(node.ipAddresses)}
/>
<Attribute
isCopyable
tooltip="Users of your tailnet can use this DNS short name to access this machine."
name="Short domain"
tooltip="Users of your tailnet can use this DNS short name to access this machine."
value={node.givenName}
/>
{magic ? (
<Attribute
isCopyable
tooltip="Users of your tailnet can use this DNS name to access this machine."
name="Full domain"
tooltip="Users of your tailnet can use this DNS name to access this machine."
value={`${node.givenName}.${magic}`}
/>
) : undefined}
@ -341,13 +341,13 @@ export default function Page() {
Client Connectivity
</p>
<Attribute
tooltip="Whether the machine is behind a difficult NAT that varies the machines IP address depending on the destination."
name="Varies"
tooltip="Whether the machine is behind a difficult NAT that varies the machines IP address depending on the destination."
value={stats.NetInfo?.MappingVariesByDestIP ? 'Yes' : 'No'}
/>
<Attribute
tooltip="Whether the machine needs to traverse NATs with hairpinning."
name="Hairpinning"
tooltip="Whether the machine needs to traverse NATs with hairpinning."
value={stats.NetInfo?.HairPinning ? 'Yes' : 'No'}
/>
<Attribute

View file

@ -18,7 +18,7 @@ export async function loader({
context,
}: LoaderFunctionArgs<LoadContext>) {
const session = await context.sessions.auth(request);
const user = session.get('user');
const user = session.user;
if (!user) {
throw new Error('Missing user session. Please log in again.');
}
@ -41,11 +41,8 @@ export async function loader({
);
const [{ nodes }, { users }] = await Promise.all([
context.client.get<{ nodes: Machine[] }>(
'v1/node',
session.get('api_key')!,
),
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
context.client.get<{ nodes: Machine[] }>('v1/node', session.api_key),
context.client.get<{ users: User[] }>('v1/user', session.api_key),
]);
let magic: string | undefined;
@ -90,18 +87,18 @@ export default function Page() {
<p>
Manage the devices connected to your Tailnet.{' '}
<Link
to="https://tailscale.com/kb/1372/manage-devices"
name="Tailscale Manage Devices Documentation"
to="https://tailscale.com/kb/1372/manage-devices"
>
Learn more
</Link>
</p>
</div>
<NewMachine
disabledKeys={data.preAuth ? [] : ['pre-auth']}
isDisabled={!data.writable}
server={data.publicServer ?? data.server}
users={data.users}
isDisabled={!data.writable}
disabledKeys={data.preAuth ? [] : ['pre-auth']}
/>
</div>
<table className="table-auto w-full rounded-lg">
@ -141,16 +138,16 @@ export default function Page() {
>
{data.populatedNodes.map((machine) => (
<MachineRow
key={machine.id}
node={machine}
users={data.users}
magic={data.magic}
isAgent={data.agent ? data.agent === machine.nodeKey : undefined}
isDisabled={
data.writable
? false // If the user has write permissions, they can edit all machines
: machine.user.providerId?.split('/').pop() !== data.subject
}
key={machine.id}
magic={data.magic}
node={machine}
users={data.users}
/>
))}
</tbody>