mirror of
https://github.com/tale/headplane.git
synced 2026-08-21 02:06:37 +00:00
feat: rework oidc to be more resilient
This includes setting a custom redirect URI, handling errors, and using a better library. As an API decision I've also disabled per session API keys as it clutters up too much.
This commit is contained in:
@@ -13,10 +13,15 @@ import TextField from '~/components/TextField';
|
||||
import type { Key } from '~/types';
|
||||
import { loadContext } from '~/utils/config/headplane';
|
||||
import { pull } from '~/utils/headscale';
|
||||
import { startOidc } from '~/utils/oidc';
|
||||
import {
|
||||
startOidc,
|
||||
beginAuthFlow,
|
||||
getRedirectUri
|
||||
} from '~/utils/oidc';
|
||||
import { commitSession, getSession } from '~/utils/sessions.server';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
if (session.has('hsApiKey')) {
|
||||
return redirect('/machines', {
|
||||
@@ -30,7 +35,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// Only set if OIDC is properly enabled anyways
|
||||
if (context.oidc?.disableKeyLogin) {
|
||||
return startOidc(context.oidc, request);
|
||||
return redirect('/oidc/start');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -42,6 +47,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const oidcStart = formData.get('oidc-start');
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
|
||||
if (oidcStart) {
|
||||
const context = await loadContext();
|
||||
@@ -50,12 +56,10 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
throw new Error('An invalid OIDC configuration was provided');
|
||||
}
|
||||
|
||||
// We know it exists here because this action only happens on OIDC
|
||||
return startOidc(context.oidc, request);
|
||||
return redirect('/oidc/start');
|
||||
}
|
||||
|
||||
const apiKey = String(formData.get('api-key'));
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
|
||||
// Test the API key
|
||||
try {
|
||||
@@ -71,6 +75,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
session.set('hsApiKey', apiKey);
|
||||
session.set('user', {
|
||||
subject: 'unknown-non-oauth',
|
||||
name: key.prefix,
|
||||
email: `${expiresDays.toString()} days`,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,77 @@
|
||||
import { type LoaderFunctionArgs, data } from 'react-router';
|
||||
import { type LoaderFunctionArgs, redirect } from 'react-router';
|
||||
import { loadContext } from '~/utils/config/headplane';
|
||||
import { finishOidc } from '~/utils/oidc';
|
||||
import { getSession, commitSession } from '~/utils/sessions.server';
|
||||
import { finishAuthFlow, getRedirectUri, formatError } from '~/utils/oidc';
|
||||
import { send } from '~/utils/res';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const context = await loadContext();
|
||||
if (!context.oidc) {
|
||||
throw new Error('An invalid OIDC configuration was provided');
|
||||
}
|
||||
// Check if we have 0 query parameters
|
||||
const url = new URL(request.url);
|
||||
if (url.searchParams.toString().length === 0) {
|
||||
return redirect('/machines');
|
||||
}
|
||||
|
||||
return finishOidc(context.oidc, request);
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
if (session.has('hsApiKey')) {
|
||||
return redirect('/machines')
|
||||
}
|
||||
|
||||
// This is a hold-over from the old code
|
||||
// TODO: Rewrite checkOIDC in the context loader
|
||||
const { oidc } = await loadContext();
|
||||
if (!oidc) {
|
||||
throw new Error('An invalid OIDC configuration was provided');
|
||||
}
|
||||
|
||||
const oidcConfig = {
|
||||
issuer: oidc.issuer,
|
||||
clientId: oidc.client,
|
||||
clientSecret: oidc.secret,
|
||||
redirectUri: oidc.redirectUri,
|
||||
tokenEndpointAuthMethod: oidc.method,
|
||||
}
|
||||
|
||||
const codeVerifier = session.get('oidc_code_verif');
|
||||
const state = session.get('oidc_state');
|
||||
const nonce = session.get('oidc_nonce');
|
||||
|
||||
if (!codeVerifier || !state || !nonce) {
|
||||
return send({ error: 'Missing OIDC state' }, { status: 400 });
|
||||
}
|
||||
|
||||
const flowOptions = {
|
||||
redirect_uri: request.url,
|
||||
codeVerifier,
|
||||
state,
|
||||
nonce: nonce === '<none>' ? undefined : nonce,
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await finishAuthFlow(oidcConfig, flowOptions);
|
||||
session.set('user', user);
|
||||
session.unset('oidc_code_verif');
|
||||
session.unset('oidc_state');
|
||||
session.unset('oidc_nonce');
|
||||
|
||||
// TODO: This is breaking, to stop the "over-generation" of API
|
||||
// keys because they are currently non-deletable in the headscale
|
||||
// database. Look at this in the future once we have a solution
|
||||
// or we have permissioned API keys.
|
||||
session.set('hsApiKey', oidc.rootKey);
|
||||
return redirect('/machines', {
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Gracefully present OIDC errors
|
||||
return data({ error }, { status: 500 });
|
||||
return new Response(
|
||||
JSON.stringify(formatError(error)),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type LoaderFunctionArgs, data, redirect } from 'react-router';
|
||||
import { commitSession, getSession } from '~/utils/sessions.server';
|
||||
import { send } from '~/utils/res';
|
||||
import { beginAuthFlow, getRedirectUri } from '~/utils/oidc';
|
||||
import { loadContext } from '~/utils/config/headplane';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const session = await getSession(request.headers.get('Cookie'));
|
||||
if (session.has('hsApiKey')) {
|
||||
return redirect('/machines')
|
||||
}
|
||||
|
||||
// This is a hold-over from the old code
|
||||
// TODO: Rewrite checkOIDC in the context loader
|
||||
const { oidc } = await loadContext();
|
||||
if (!oidc) {
|
||||
throw new Error('An invalid OIDC configuration was provided');
|
||||
}
|
||||
|
||||
const oidcConfig = {
|
||||
issuer: oidc.issuer,
|
||||
clientId: oidc.client,
|
||||
clientSecret: oidc.secret,
|
||||
redirectUri: oidc.redirectUri,
|
||||
tokenEndpointAuthMethod: oidc.method,
|
||||
}
|
||||
|
||||
const redirectUri = oidcConfig.redirectUri ?? getRedirectUri(request);
|
||||
const data = await beginAuthFlow(oidcConfig, redirectUri);
|
||||
session.set('oidc_code_verif', data.codeVerifier);
|
||||
session.set('oidc_state', data.state);
|
||||
session.set('oidc_nonce', data.nonce);
|
||||
|
||||
return redirect(data.url, {
|
||||
status: 302,
|
||||
headers: {
|
||||
'Set-Cookie': await commitSession(session),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user