feat!: replace auth with OIDC
This commit is contained in:
51
src/auth.ts
51
src/auth.ts
@ -1,15 +1,38 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { username } from "better-auth/plugins";
|
||||
import { pool } from "./db";
|
||||
import { decode, verify, type Algorithm } from "jsonwebtoken";
|
||||
import jwkToPem, { type JWK } from "jwk-to-pem";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: pool,
|
||||
emailAndPassword: {
|
||||
enabled: true
|
||||
},
|
||||
plugins: [
|
||||
username({
|
||||
minUsernameLength: 3
|
||||
})
|
||||
]
|
||||
});
|
||||
const JWKS = "https://auth.lab.picoscratch.de/.well-known/jwks.json";
|
||||
|
||||
type JWKSResponse = {
|
||||
keys: Array<{ kid: string; kty: string; use: string; alg: Algorithm; n: string; e: string }>;
|
||||
}
|
||||
|
||||
export async function verifyToken(token: string): Promise<boolean> {
|
||||
const decoded = decode(token, { complete: true });
|
||||
|
||||
const jwks = await fetch(JWKS)
|
||||
.then(res => res.json() as Promise<JWKSResponse>);
|
||||
if (!decoded || !decoded.header || !decoded.header.kid) {
|
||||
return false;
|
||||
}
|
||||
const key = jwks.keys.find(k => k.kid === decoded.header.kid);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
const pem = jwkToPem(key as JWK);
|
||||
try {
|
||||
const res = verify(token, pem, { algorithms: [key.alg] });
|
||||
console.log(res);
|
||||
return typeof res === "object" && "sub" in res;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTokenUID(token: string): string | null {
|
||||
const decoded = decode(token);
|
||||
if (typeof decoded === "object" && decoded !== null && "sub" in decoded) {
|
||||
return decoded.sub as string;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
56
src/main.ts
56
src/main.ts
@ -1,19 +1,14 @@
|
||||
import { Hono } from "hono";
|
||||
import { auth } from "./auth";
|
||||
import { cors } from "hono/cors";
|
||||
import { pool } from "./db";
|
||||
import { post } from "./ai";
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import { getTokenUID, verifyToken } from "./auth";
|
||||
import { createBunWebSocket } from "hono/bun";
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import type { WSContext } from "hono/ws";
|
||||
|
||||
const app = new Hono<{
|
||||
Variables: {
|
||||
user: typeof auth.$Infer.Session.user | null;
|
||||
session: typeof auth.$Infer.Session.session | null
|
||||
}
|
||||
}>();
|
||||
const app = new Hono();
|
||||
const { upgradeWebSocket, websocket } = createBunWebSocket<ServerWebSocket>();
|
||||
|
||||
async function setupDB() {
|
||||
@ -33,20 +28,6 @@ async function setupDB() {
|
||||
|
||||
await setupDB();
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||
|
||||
if (!session) {
|
||||
c.set("user", null);
|
||||
c.set("session", null);
|
||||
return next();
|
||||
}
|
||||
|
||||
c.set("user", session.user);
|
||||
c.set("session", session.session);
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(
|
||||
"/api/*", // or replace with "*" to enable cors for all routes
|
||||
cors({
|
||||
@ -73,12 +54,15 @@ app.get("/api/config", (c) => {
|
||||
return c.json({
|
||||
name: "TrafficCue Server",
|
||||
version: "0",
|
||||
capabilities
|
||||
capabilities,
|
||||
oidc: process.env.OIDC_ENABLED ? {
|
||||
AUTH_URL: process.env.OIDC_AUTH_URL,
|
||||
CLIENT_ID: process.env.OIDC_CLIENT_ID,
|
||||
TOKEN_URL: process.env.OIDC_TOKEN_URL
|
||||
} : undefined
|
||||
})
|
||||
})
|
||||
|
||||
app.on(["GET", "POST"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
||||
|
||||
app.get("/api/reviews", async (c) => {
|
||||
let {lat, lon} = c.req.query();
|
||||
if (!lat || !lon) {
|
||||
@ -99,10 +83,7 @@ app.get("/api/reviews", async (c) => {
|
||||
rating: row.rating,
|
||||
comment: row.comment,
|
||||
created_at: row.created_at,
|
||||
username: await pool.query(
|
||||
"SELECT username FROM \"user\" WHERE id = $1",
|
||||
[row.user_id],
|
||||
).then(res => res.rows[0]?.username || "Unknown"),
|
||||
username: "Me" // TODO: Sync OIDC users with the database
|
||||
};
|
||||
})));
|
||||
});
|
||||
@ -113,14 +94,27 @@ app.post("/api/review", async (c) => {
|
||||
return c.json({ error: "Rating, latitude, and longitude are required" }, 400);
|
||||
}
|
||||
|
||||
const user = c.get("user");
|
||||
if (!user) {
|
||||
const authHeader = c.req.header("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
const token = authHeader.split(" ")[1];
|
||||
if (!token) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
const isValid = await verifyToken(token);
|
||||
if (!isValid) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const uid = await getTokenUID(token);
|
||||
if (!uid) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const res = await pool.query(
|
||||
"INSERT INTO reviews (user_id, latitude, longitude, rating, comment) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[user.id, lat, lon, rating, comment],
|
||||
[uid, lat, lon, rating, comment],
|
||||
);
|
||||
|
||||
return c.json(res.rows[0]);
|
||||
|
Reference in New Issue
Block a user