feat(oidc): added in so we could use an oidc to login as well :D

This commit is contained in:
2026-04-23 07:09:49 -05:00
parent d6328ab764
commit f7276ca2d7
5 changed files with 236 additions and 10 deletions

View File

@@ -50,3 +50,11 @@ GP_PASSWORD=
# how often to check for new/updated queries in min # how often to check for new/updated queries in min
QUERY_TIME_TYPE=m #valid options are m, h QUERY_TIME_TYPE=m #valid options are m, h
QUERY_CHECK=1 QUERY_CHECK=1
# Oauth setup
PROVIDER=""
CLIENT_ID=""
CLIENT_SECRET=""
CLIENT_SCOPES="openid profile email groups"
DISCOVERY_URL=""

View File

@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { import {
admin as adminPlugin, admin as adminPlugin,
genericOAuth,
// apiKey, // apiKey,
// createAuthMiddleware, // createAuthMiddleware,
//customSession, //customSession,
@@ -16,6 +17,46 @@ import { ac, admin, systemAdmin, user } from "./auth.permissions.js";
import { allowedOrigins } from "./cors.utils.js"; import { allowedOrigins } from "./cors.utils.js";
import { sendEmail } from "./sendEmail.utils.js"; import { sendEmail } from "./sendEmail.utils.js";
function decodeJwtPayload<T = Record<string, unknown>>(jwt: string): T {
const parts = jwt.split(".");
if (parts.length < 2) {
throw new Error("Invalid JWT");
}
const payload = parts[1]?.replace(/-/g, "+").replace(/_/g, "/");
const padded = payload?.padEnd(
payload.length + ((4 - (payload.length % 4)) % 4),
"=",
);
const json = Buffer.from(padded ?? "", "base64").toString("utf8");
return JSON.parse(json) as T;
}
function normalizeGroups(groups?: unknown): string[] {
if (!Array.isArray(groups)) return [];
return groups
.filter((g): g is string => typeof g === "string")
.map((g) => g.trim().toLowerCase())
.filter((g) => g.length > 0);
}
type VoidAuthClaims = {
sub: string;
name?: string;
preferred_username?: string;
email?: string;
email_verified?: boolean;
groups?: string[];
picture?: string;
iss?: string;
aud?: string;
exp?: number;
iat?: number;
};
export const schema = { export const schema = {
user: rawSchema.user, user: rawSchema.user,
session: rawSchema.session, session: rawSchema.session,
@@ -25,9 +66,73 @@ export const schema = {
apiKey: rawSchema.apikey, // 🔑 rename to apiKey apiKey: rawSchema.apikey, // 🔑 rename to apiKey
}; };
const hasOAuth =
Boolean(process.env.PROVIDER) &&
Boolean(process.env.CLIENT_ID) &&
Boolean(process.env.CLIENT_SECRET) &&
Boolean(process.env.DISCOVERY_URL);
if (!hasOAuth) {
console.warn("Missing oauth data.");
}
const oauthPlugins = hasOAuth
? [
genericOAuth({
config: [
{
providerId: process.env.PROVIDER!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
discoveryUrl: process.env.DISCOVERY_URL!,
scopes: (process.env.CLIENT_SCOPES ?? "")
.split(/[,\s]+/)
.filter(Boolean),
pkce: true,
requireIssuerValidation: true,
redirectURI: `${process.env.URL}/lst/api/auth/oauth2/callback/${process.env.PROVIDER!}`,
getUserInfo: async (tokens) => {
if (!tokens.idToken) {
throw new Error("VoidAuth did not return an idToken");
}
const claims = decodeJwtPayload<VoidAuthClaims>(tokens.idToken);
const groups = normalizeGroups(claims.groups);
return {
id: claims.sub,
email: claims.email ?? "",
name:
claims.name ??
claims.preferred_username ??
claims.email ??
"Unknown User",
image: claims.picture ?? null,
emailVerified: Boolean(claims.email_verified),
groups,
username: claims.preferred_username ?? null,
} as any;
},
mapProfileToUser: async (profile) => {
return {
name: profile.name,
role: profile.groups?.includes("lst_admins")
? "systemAdmin"
: profile.groups?.includes("admins")
? "admin"
: "user",
};
},
},
],
}),
]
: [];
export const auth = betterAuth({ export const auth = betterAuth({
appName: "lst", appName: "lst",
baseURL: process.env.URL, baseURL: `${process.env.URL}/lst/api/auth`,
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema, schema,
@@ -42,6 +147,14 @@ export const auth = betterAuth({
}, },
}, },
}, },
account: {
encryptOAuthTokens: true,
updateAccountOnSignIn: true,
accountLinking: {
enabled: true,
trustedProviders: ["voidauth"],
},
},
plugins: [ plugins: [
jwt({ jwt: { expirationTime: "1h" } }), jwt({ jwt: { expirationTime: "1h" } }),
//apiKey(), //apiKey(),
@@ -63,6 +176,7 @@ export const auth = betterAuth({
return true; return true;
}, },
}), }),
...oauthPlugins,
// customSession(async ({ user, session }) => { // customSession(async ({ user, session }) => {
// const roles = await db // const roles = await db
@@ -121,7 +235,7 @@ export const auth = betterAuth({
}, },
}, },
cookie: { cookie: {
path: "/lst/app", path: "/lst",
sameSite: "lax", sameSite: "lax",
secure: false, secure: false,
httpOnly: true, httpOnly: true,

View File

@@ -1,4 +1,4 @@
import { adminClient } from "better-auth/client/plugins"; import { adminClient, genericOAuthClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { ac, admin, systemAdmin, user } from "./auth-permissions"; import { ac, admin, systemAdmin, user } from "./auth-permissions";
@@ -13,6 +13,7 @@ export const authClient = createAuthClient({
systemAdmin, systemAdmin,
}, },
}), }),
genericOAuthClient(),
], ],
}); });

View File

@@ -1,4 +1,5 @@
import { Link, useNavigate } from "@tanstack/react-router"; import { Link, useNavigate } from "@tanstack/react-router";
import { Cat, LogIn } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
Card, Card,
@@ -9,13 +10,23 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { useAppForm } from "@/lib/formSutff"; import { useAppForm } from "@/lib/formSutff";
import { Button } from "../../../components/ui/button";
import socket from "../../../lib/socket.io"; import socket from "../../../lib/socket.io";
export default function LoginForm({ redirectPath }: { redirectPath: string }) { export default function LoginForm({ redirectPath }: { redirectPath: string }) {
const loginEmail = localStorage.getItem("loginEmail") || ""; const loginEmail = localStorage.getItem("loginEmail") || "";
const rememberMe = localStorage.getItem("rememberMe") === "true"; const rememberMe = localStorage.getItem("rememberMe") === "true";
const navigate = useNavigate(); const navigate = useNavigate();
const oauthLogin = async () => {
await authClient.signIn.oauth2({
providerId: "voidauth",
callbackURL: "/lst/app",
errorCallbackURL: "/lst/app/login",
});
};
const form = useAppForm({ const form = useAppForm({
defaultValues: { defaultValues: {
email: loginEmail, email: loginEmail,
@@ -26,7 +37,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
// set remember me incase we want it later // set remember me incase we want it later
if (value.rememberMe) { if (value.rememberMe) {
localStorage.setItem("rememberMe", value.rememberMe.toString()); localStorage.setItem("rememberMe", value.rememberMe.toString());
localStorage.setItem("loginEmail", value.email); localStorage.setItem("loginEmail", value.email.toLocaleLowerCase());
} else { } else {
localStorage.removeItem("rememberMe"); localStorage.removeItem("rememberMe");
localStorage.removeItem("loginEmail"); localStorage.removeItem("loginEmail");
@@ -62,7 +73,17 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
<div> <div>
<Card className="p-3 w-96"> <Card className="p-3 w-96">
<CardHeader> <CardHeader>
<CardTitle>Login to your account</CardTitle> <CardTitle>
<div className="flex flex-row justify-center">
<Button onClick={oauthLogin} size="lg" variant="ghost">
<Cat />
</Button>
<span className="mt-2">Login to your account</span>{" "}
<Button size="lg" variant="ghost">
<Cat />
</Button>
</div>
</CardTitle>
<CardDescription> <CardDescription>
Enter your username and password below Enter your username and password below
</CardDescription> </CardDescription>
@@ -76,12 +97,19 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
> >
<form.AppField name="email"> <form.AppField name="email">
{(field) => ( {(field) => (
<field.InputField label="Email" inputType="email" required /> <field.InputField
label="Email"
inputType="email"
required={rememberMe}
/>
)} )}
</form.AppField> </form.AppField>
<form.AppField name="password"> <form.AppField name="password">
{(field) => ( {(field) => (
<field.InputPasswordField label="Password" required={true} /> <field.InputPasswordField
label="Password"
required={rememberMe}
/>
)} )}
</form.AppField> </form.AppField>
@@ -98,7 +126,7 @@ export default function LoginForm({ redirectPath }: { redirectPath: string }) {
</Link> </Link>
</div> </div>
<div className="flex justify-end mt-2"> <div className="flex justify-between mt-2 ">
<form.AppForm> <form.AppForm>
<form.SubmitButton>Login</form.SubmitButton> <form.SubmitButton>Login</form.SubmitButton>
</form.AppForm> </form.AppForm>

View File

@@ -23,6 +23,8 @@ $password = $ADM_PASSWORD
$securePass = ConvertTo-SecureString $password -AsPlainText -Force $securePass = ConvertTo-SecureString $password -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential($username, $securePass) $credentials = New-Object System.Management.Automation.PSCredential($username, $securePass)
function Update-Server { function Update-Server {
param ( param (
[string]$Destination, [string]$Destination,
@@ -84,6 +86,75 @@ function Update-Server {
$AppUpdate = { $AppUpdate = {
param ($Server, $Token, $Destination, $BuildFile) param ($Server, $Token, $Destination, $BuildFile)
function Fix-Env {
$envFile = ".env"
if (-not (Test-Path $envFile)) {
Write-Host ".env not found, creating..."
New-Item -ItemType File -Path $envFile | Out-Null
}
$envContent = Get-Content $envFile -Raw
if ([string]::IsNullOrWhiteSpace($envContent)) {
$envContent = ""
}
$envVarsToAdd = @{
"PROVIDER" = "voidauth"
"CLIENT_ID" = "crIVcUilFWIS6ME3"
"CLIENT_SECRET" = "zsJeyjMN2yDDqfyzSsh96OtlA2714F5d"
"CLIENT_SCOPES" = "openid profile email groups"
"DISCOVERY_URL" = "https://auth.tuffraid.net/oidc/.well-known/openid-configuration"
}
$linesToAppend = @()
foreach ($key in $envVarsToAdd.Keys) {
$escapedKey = [regex]::Escape($key)
if ($envContent -notmatch "(?m)^$escapedKey=") {
$linesToAppend += "$key=$($envVarsToAdd[$key])"
Write-Host "Adding missing env: $key"
}
else {
Write-Host "Env exists, skipping add: $key"
}
}
###### to replace the values of mistakens or something fun where we need to fix across all 17 servers put it here.
$envVarsToReplace = @{
# "PORT" = "3000"
#"URL" = "https://$($Token)prod.alpla.net/lst"
}
foreach ($key in $envVarsToReplace.Keys) {
$value = $envVarsToReplace[$key]
$escapedKey = [regex]::Escape($key)
if ($envContent -match "(?m)^$escapedKey=") {
Write-Host "Replacing env: $key -> $value"
$envContent = $envContent -replace "(?m)^$escapedKey=.*", "$key=$value"
}
else {
Write-Host "Env not found for replace, skipping: $key"
}
}
if ($linesToAppend.Count -gt 0) {
if ($envContent.Length -gt 0 -and -not $envContent.EndsWith("`n")) {
$envContent += "`r`n"
}
$envContent += "`r`n# ---- VoidAuth Config ----`r`n"
$envContent += ($linesToAppend -join "`r`n")
Write-Host "Appending new env vars."
}
Set-Content -Path $envFile -Value $envContent
Write-Host "Env update completed."
}
#convert everything to the server fun #convert everything to the server fun
$LocalPath = $Destination -replace '\$', ':' $LocalPath = $Destination -replace '\$', ':'
$BuildFileLoc = "$LocalPath\$BuildFile" $BuildFileLoc = "$LocalPath\$BuildFile"
@@ -121,8 +192,12 @@ function Update-Server {
Write-Host "Running install/update in: $LocalPath" Write-Host "Running install/update in: $LocalPath"
npm install --omit=dev npm install --omit=dev
Start-Sleep -Seconds 3 Start-Sleep -Seconds 3
Write-Host "Install/update completed." Write-Host "Install/update completed."
# do the migrations
# update the env to include the new and missing things silly people and wanting things fixed :(
Fix-Env #-Path $LocalPath
# do the migrations
# Push-Location $LocalPath # Push-Location $LocalPath
Write-Host "Running migrations" Write-Host "Running migrations"
npm run dev:db:migrate npm run dev:db:migrate