migrate to OIDC for auth

This commit is contained in:
Grant 2024-05-23 14:59:54 -06:00
parent 732feacd5b
commit 8801efc9a3
6 changed files with 96 additions and 29 deletions

View File

@ -9,7 +9,13 @@ export const User = () => {
<div className="user-name">{user.user.username}</div>
<div className="user-instance">{user.service.instance.hostname}</div>
</div>
<img src={user.user.profile} alt="User Avatar" className="user-avatar" />
{user.user.picture_url && (
<img
src={user.user.picture_url}
alt="User Avatar"
className="user-avatar"
/>
)}
</div>
) : (
<></>

View File

@ -40,6 +40,8 @@ export interface IAppContext {
settingsSidebar: boolean;
setSettingsSidebar: (v: boolean) => void;
undo?: { available: true; expireAt: number };
loadChat: boolean;
setLoadChat: (v: boolean) => void;
}
export interface IPalleteContext {
@ -114,13 +116,19 @@ export type AuthSession = {
software: {
name: string;
version: string;
logo_uri?: string;
repository?: string;
homepage?: string;
};
instance: {
hostname: string;
logo_uri?: string;
banner_uri?: string;
name?: string;
};
};
user: {
username: string;
profile: string;
picture_url?: string;
};
};

View File

@ -1,44 +1,57 @@
import { Router } from "express";
import { prisma } from "./lib/prisma";
import { OpenID } from "./lib/oidc";
const app = Router();
const { AUTH_ENDPOINT, AUTH_CLIENT, AUTH_SECRET } = process.env;
app.get("/me", (req, res) => {
res.json(req.session);
});
app.get("/login", (req, res) => {
const params = new URLSearchParams();
params.set("service", "canvas");
res.redirect(AUTH_ENDPOINT + "/login?" + params);
res.redirect(
OpenID.client.authorizationUrl({
prompt: "consent",
scope: "openid instance",
})
);
});
// TODO: logout endpoint
app.get("/callback", async (req, res) => {
const { code } = req.query;
// const { code } = req.query;
const who = await fetch(AUTH_ENDPOINT + "/api/auth/identify", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${AUTH_CLIENT}:${AUTH_SECRET}`,
},
body: JSON.stringify({
code,
}),
}).then((a) => a.json());
if (!who.success) {
res.json({
error: "AUTHENTICATION FAILED",
error_message: who.error || "no error specified",
const params = OpenID.client.callbackParams(req);
const exchange = await OpenID.client.callback(
OpenID.getRedirectUrl(),
params
);
if (!exchange || !exchange.access_token) {
return res.status(400).json({
success: false,
error: "FAILED TOKEN EXCHANGE",
});
return;
}
const [username, hostname] = who.user.sub.split("@");
const whoami = await OpenID.client.userinfo<{
instance: {
software: {
name: string;
version: string;
logo_uri?: string;
repository?: string;
homepage?: string;
};
instance: {
logo_uri?: string;
banner_uri?: string;
name?: string;
};
};
}>(exchange.access_token);
const [username, hostname] = whoami.sub.split("@");
await prisma.user.upsert({
where: {
@ -52,14 +65,14 @@ app.get("/callback", async (req, res) => {
req.session.user = {
service: {
...who.service,
...whoami.instance,
instance: {
...who.service.instance,
...whoami.instance.instance,
hostname,
},
},
user: {
profile: who.user.profile,
picture_url: whoami.picture,
username,
},
};

View File

@ -4,6 +4,7 @@ import { Redis } from "./lib/redis";
import { Logger } from "./lib/Logger";
import { ExpressServer } from "./lib/Express";
import { SocketServer } from "./lib/SocketServer";
import { OpenID } from "./lib/oidc";
// Validate environment variables
@ -51,7 +52,15 @@ if (!process.env.AUTH_SECRET) {
process.exit(1);
}
if (!process.env.OIDC_CALLBACK_HOST) {
Logger.error("OIDC_CALLBACK_HOST is not defined");
process.exit(1);
}
Redis.connect();
OpenID.setup().then(() => {
Logger.info("Setup OpenID");
});
const express = new ExpressServer();
new SocketServer(express.httpServer);

View File

@ -0,0 +1,24 @@
import { BaseClient, Issuer } from "openid-client";
class OpenID_ {
issuer: Issuer<BaseClient> = {} as any;
client: BaseClient = {} as any;
async setup() {
const { AUTH_ENDPOINT, AUTH_CLIENT, AUTH_SECRET } = process.env;
this.issuer = await Issuer.discover(AUTH_ENDPOINT);
this.client = new this.issuer.Client({
client_id: AUTH_CLIENT,
client_secret: AUTH_SECRET,
response_types: ["code"],
redirect_uris: [this.getRedirectUrl()],
});
}
getRedirectUrl() {
return process.env.OIDC_CALLBACK_HOST + "/api/callback";
}
}
export const OpenID = new OpenID_();

View File

@ -25,6 +25,13 @@ declare global {
REDIS_HOST: string;
REDIS_SESSION_PREFIX: string;
/**
* hostname that is used in the callback
*
* @example http://localhost:3000
* @example https://canvas.com
*/
OIDC_CALLBACK_HOST: string;
/**
* If this is set, enable socket.io CORS to this origin
*