Skip to content

Commit

Permalink
traefik forward auth
Browse files Browse the repository at this point in the history
  • Loading branch information
NextFire committed Apr 9, 2024
1 parent 38bb7f1 commit 9ab1198
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 26 deletions.
35 changes: 35 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
services:
traefik:
image: traefik:latest
command:
- --log.level=INFO
- --accesslog=true
- --api.dashboard=true
- --providers.docker=true
- --entrypoints.web.address=:80
ports:
- "8080:80/tcp"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- traefik.http.routers.traefik.rule=Host(`traefik.localhost`)
- traefik.http.routers.traefik.service=api@internal

nanaoidc:
image: node:lts-alpine
command: sleep infinity
working_dir: /app
volumes:
- .:/app
labels:
- traefik.http.routers.nanaoidc.rule=Host(`nanaoidc.localhost`) || Path(`/_oauth`)
- traefik.http.services.nanaoidc.loadbalancer.server.port=3000
- traefik.http.middlewares.nanaoidc.forwardAuth.address=http://nanaoidc:3000/traefik
- traefik.http.middlewares.nanaoidc.forwardAuth.trustForwardHeader=true

whoami:
image: traefik/whoami:latest
labels:
- traefik.http.routers.whoami.rule=Host(`whoami.localhost`)
- traefik.http.routers.whoami.middlewares=nanaoidc
- traefik.http.services.whoami.loadbalancer.server.port=80
9 changes: 7 additions & 2 deletions nanaoidc.example.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"publicUrl": "",
"sessionPassword": "",
"publicUrl": "http://localhost:3000",
"sessionPassword": "openssl rand -hex 32",
"baseGroup": "",
"discord": {
"clientId": "",
Expand All @@ -10,6 +10,11 @@
"1234567890": "private-group"
}
},
"forwardAuth": {
"hosts": {
"sub.localhost": ["group1", "orGroup2"]
}
},
"clients": [
{
"client_id": "client",
Expand Down
File renamed without changes.
7 changes: 3 additions & 4 deletions server/api/discord/callback.get.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import assert from "node:assert/strict";

export default eventHandler(async (event) => {
const session = await useTypedSession(event);

const query = getQuery(event);
const { code } = query;
assert(typeof code === "string");
await session.update({ code });

const session = await useTypedSession(event);
const redirect = session.data.redirect || "/";
return sendRedirect(event, redirect);
const params = new URLSearchParams({ code });
return sendRedirect(event, `${redirect}?${params}`);
});
27 changes: 27 additions & 0 deletions server/routes/_oauth.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";

export default eventHandler(async (event) => {
const session = await useTypedSession(event);
const query = getQuery(event);
const { code, forwardAuthRedirect } = query;
if (forwardAuthRedirect) {
assert(typeof forwardAuthRedirect === "string");
await session.update({ redirect: ".", forwardAuthRedirect });
return sendRedirect(event, `${userConfig.publicUrl}/api/discord/auth`);
} else if (code) {
assert(typeof code === "string");
const resp = await exchangeCode(code);
const { member } = await fetchUserinfo(resp.access_token);
const groups = [userConfig.baseGroup];
for (const role of member.roles) {
const mapped = userConfig.discord.roles[role];
if (mapped) {
groups.push(mapped);
}
}
await session.update({ forwardAuthGroups: groups });
return sendRedirect(event, session.data.forwardAuthRedirect);
} else {
throw createError({ status: 400, message: "Missing required query" });
}
});
22 changes: 6 additions & 16 deletions server/routes/interaction/[uid]/callback.get.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import { REST } from "@discordjs/rest";
import {
Routes,
type RESTGetAPICurrentUserResult,
type RESTGetCurrentUserGuildMemberResult,
} from "discord-api-types/v10";
import assert from "node:assert/strict";
import type { InteractionResults } from "oidc-provider";

export default eventHandler(async (event) => {
const session = await useTypedSession(event);
const query = getQuery(event);
const { code } = query;
assert(typeof code === "string");

const resp = await exchangeCode(session.data.code);
const rest = new REST({ authPrefix: "Bearer" }).setToken(resp.access_token);
const [user, member] = (await Promise.all([
rest.get(Routes.user()),
rest.get(Routes.userGuildMember(userConfig.discord.guildId)),
])) as [
user: RESTGetAPICurrentUserResult,
member: RESTGetCurrentUserGuildMemberResult
];
const resp = await exchangeCode(code);
const { user, member } = await fetchUserinfo(resp.access_token);
userStore.set(user.id, { user, member });

const result: InteractionResults = {
Expand Down
2 changes: 1 addition & 1 deletion server/routes/interaction/[uid]/login.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export default eventHandler(async (event) => {
const session = await useTypedSession(event);
await session.update({ redirect: `${event.path}/../callback` });

return sendRedirect(event, "/api/discord/login");
return sendRedirect(event, "/api/discord/auth");
});
23 changes: 23 additions & 0 deletions server/routes/traefik.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default eventHandler(async (event) => {
const headers = event.headers;
const proto = headers.get("x-forwarded-proto");
const host = headers.get("x-forwarded-host");
const uri = headers.get("x-forwarded-uri");

const session = await useTypedSession(event);
const groups = session.data.forwardAuthGroups;

if (groups !== undefined) {
const required = userConfig.forwardAuth.hosts[host];
if (!required || required.some((req) => groups.includes(req))) {
return;
} else {
await session.clear();
throw createError({ status: 401, message: "Missing required group" });
}
} else {
const forwardAuthRedirect = `${proto}://${host}${uri}`;
const params = new URLSearchParams({ forwardAuthRedirect });
return sendRedirect(event, `http://${host}/_oauth?${params}`);
}
});
3 changes: 3 additions & 0 deletions server/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export interface UserConfig {
guildId: Snowflake;
roles: Record<Snowflake, string>;
};
forwardAuth: {
hosts: Record<string, string[]>;
};
clients: ClientMetadata[];
}

Expand Down
17 changes: 16 additions & 1 deletion server/utils/discord.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { RESTPostOAuth2AccessTokenResult } from "discord-api-types/v10";
import { REST } from "@discordjs/rest";
import {
RESTPostOAuth2AccessTokenResult,
Routes,
type RESTGetAPICurrentUserResult,
type RESTGetCurrentUserGuildMemberResult,
} from "discord-api-types/v10";

const endpoints = {
authorization: "https://discord.com/oauth2/authorize",
Expand Down Expand Up @@ -34,3 +40,12 @@ export async function exchangeCode(
});
return await response.json();
}

export async function fetchUserinfo(accessToken: string) {
const rest = new REST({ authPrefix: "Bearer" }).setToken(accessToken);
const [user, member] = (await Promise.all([
rest.get(Routes.user()),
rest.get(Routes.userGuildMember(userConfig.discord.guildId)),
])) as [RESTGetAPICurrentUserResult, RESTGetCurrentUserGuildMemberResult];
return { user, member };
}
8 changes: 6 additions & 2 deletions server/utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { type H3Event } from "h3";

interface SessionData {
code: string;
redirect: string;
forwardAuthRedirect: string;
forwardAuthGroups: string[];
}

export const useTypedSession = (event: H3Event) =>
useSession<SessionData>(event, { password: userConfig.sessionPassword });
useSession<SessionData>(event, {
password: userConfig.sessionPassword,
maxAge: 60 * 60 * 24,
});

0 comments on commit 9ab1198

Please sign in to comment.