Add OIDC provider for Outline wiki SSO
Add oidc-provider with MongoDB adapter so ghostguild.org can act as the identity provider for the self-hosted Outline wiki. Members authenticate via the existing magic-link flow, with automatic SSO when an active session exists. Includes interaction routes, well-known discovery endpoint, and login page.
This commit is contained in:
parent
a232a7bbf8
commit
8a529a8e7c
13 changed files with 1258 additions and 2 deletions
75
server/routes/oidc/interaction/verify.get.ts
Normal file
75
server/routes/oidc/interaction/verify.get.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Verify magic link token and complete the OIDC login interaction.
|
||||
*
|
||||
* GET /oidc/interaction/verify?token=...
|
||||
*
|
||||
* This is the endpoint the magic link email points to. It:
|
||||
* 1. Verifies the JWT token
|
||||
* 2. Sets the Ghost Guild session cookie (so future logins are SSO)
|
||||
* 3. Completes the OIDC interaction so the user is redirected back to Outline
|
||||
*/
|
||||
import jwt from "jsonwebtoken";
|
||||
import Member from "../../../models/member.js";
|
||||
import { connectDB } from "../../../utils/mongoose.js";
|
||||
import { getOidcProvider } from "../../../utils/oidc-provider.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { token } = getQuery(event);
|
||||
|
||||
if (!token) {
|
||||
throw createError({ statusCode: 400, statusMessage: "Token is required" });
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig(event);
|
||||
|
||||
let decoded: { memberId: string; oidcUid: string };
|
||||
try {
|
||||
decoded = jwt.verify(token as string, config.jwtSecret) as typeof decoded;
|
||||
} catch {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid or expired token",
|
||||
});
|
||||
}
|
||||
|
||||
await connectDB();
|
||||
const member = await (Member as any).findById(decoded.memberId);
|
||||
|
||||
if (!member) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Member not found" });
|
||||
}
|
||||
|
||||
if (member.status === "suspended" || member.status === "cancelled") {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: `Account is ${member.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Set Ghost Guild session cookie for future SSO
|
||||
const sessionToken = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
config.jwtSecret,
|
||||
{ expiresIn: "7d" }
|
||||
);
|
||||
|
||||
setCookie(event, "auth-token", sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
// Complete the OIDC interaction
|
||||
const provider = await getOidcProvider();
|
||||
const result = {
|
||||
login: { accountId: member._id.toString() },
|
||||
};
|
||||
|
||||
await provider.interactionFinished(
|
||||
event.node.req,
|
||||
event.node.res,
|
||||
result,
|
||||
{ mergeWithLastSubmission: false }
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue