mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
b961b339bb
Closes HP-407.
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
import { data, redirect } from "react-router";
|
|
|
|
import { findHeadscaleUserBySubject } from "~/server/web/headscale-identity";
|
|
import log from "~/utils/log";
|
|
import { createOidcStateCookie } from "~/utils/oidc-state";
|
|
|
|
import type { Route } from "./+types/oidc-callback";
|
|
|
|
export async function loader({ request, context }: Route.LoaderArgs) {
|
|
const service = context.oidc?.service;
|
|
if (!service) {
|
|
throw data("OIDC is not enabled or misconfigured", { status: 501 });
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
if (url.searchParams.toString().length === 0) {
|
|
return redirect("/login?s=error_no_query");
|
|
}
|
|
|
|
const cookie = createOidcStateCookie(context.config);
|
|
const oidcCookieState = await cookie.parse(request.headers.get("Cookie"));
|
|
|
|
if (oidcCookieState == null) {
|
|
log.warn("auth", "Called OIDC callback without session cookie");
|
|
return redirect("/login?s=error_no_session");
|
|
}
|
|
|
|
const { state, nonce, redirect_uri, verifier } = oidcCookieState;
|
|
if (!state || !nonce || !redirect_uri || !verifier) {
|
|
log.warn("auth", "OIDC session cookie is missing required fields");
|
|
return redirect("/login?s=error_invalid_session");
|
|
}
|
|
|
|
const flowState = {
|
|
state,
|
|
nonce,
|
|
codeVerifier: verifier,
|
|
redirectUri: redirect_uri,
|
|
};
|
|
|
|
const result = await service.handleCallback(url.searchParams, flowState);
|
|
if (!result.ok) {
|
|
log.error("auth", "OIDC callback failed [%s]: %s", result.error.code, result.error.message);
|
|
if (result.error.hint) {
|
|
log.error("auth", "Hint: %s", result.error.hint);
|
|
}
|
|
return redirect("/login?s=error_auth_failed");
|
|
}
|
|
|
|
const identity = result.value;
|
|
|
|
const userId = await context.auth.findOrCreateUser(identity.subject, {
|
|
name: identity.name,
|
|
email: identity.email,
|
|
picture: identity.picture,
|
|
});
|
|
|
|
try {
|
|
const hsApi = context.hsApi.getRuntimeClient(context.headscaleApiKey!);
|
|
const hsUsers = await hsApi.getUsers();
|
|
const hsUser = findHeadscaleUserBySubject(hsUsers, identity.subject, identity.email);
|
|
if (hsUser) {
|
|
await context.auth.linkHeadscaleUser(userId, hsUser.id);
|
|
}
|
|
} catch (error) {
|
|
log.warn("auth", "Failed to link Headscale user: %s", String(error));
|
|
}
|
|
|
|
// Only persist the id_token when RP-initiated logout is enabled — otherwise
|
|
// we'd be storing a credential we never use.
|
|
const idToken = context.oidc?.useEndSession ? identity.idToken : undefined;
|
|
|
|
return redirect("/", {
|
|
headers: {
|
|
"Set-Cookie": await context.auth.createOidcSession(
|
|
userId,
|
|
{
|
|
name: identity.name,
|
|
email: identity.email,
|
|
username: identity.username,
|
|
},
|
|
{ idToken },
|
|
),
|
|
},
|
|
});
|
|
}
|