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.
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
/**
|
|
* OIDC interaction handler — checks for an existing Ghost Guild session.
|
|
*
|
|
* Flow:
|
|
* 1. Outline redirects user to /oidc/auth
|
|
* 2. oidc-provider creates an interaction and redirects here
|
|
* 3. If the user has a valid auth-token cookie → complete the interaction (SSO)
|
|
* 4. Otherwise → redirect to the OIDC login page
|
|
*/
|
|
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 provider = await getOidcProvider();
|
|
const uid = getRouterParam(event, "uid")!;
|
|
|
|
// Load the interaction details from oidc-provider
|
|
const interactionDetails = await provider.interactionDetails(
|
|
event.node.req,
|
|
event.node.res
|
|
);
|
|
const { prompt } = interactionDetails;
|
|
|
|
// ----- Login prompt -----
|
|
if (prompt.name === "login") {
|
|
// Check for existing Ghost Guild session
|
|
const token = getCookie(event, "auth-token");
|
|
|
|
if (token) {
|
|
try {
|
|
const config = useRuntimeConfig();
|
|
const decoded = jwt.verify(token, config.jwtSecret) as {
|
|
memberId: string;
|
|
};
|
|
|
|
await connectDB();
|
|
const member = await (Member as any).findById(decoded.memberId);
|
|
|
|
if (
|
|
member &&
|
|
member.status !== "suspended" &&
|
|
member.status !== "cancelled"
|
|
) {
|
|
// Auto-complete the login interaction (SSO)
|
|
const result = {
|
|
login: { accountId: member._id.toString() },
|
|
};
|
|
await provider.interactionFinished(
|
|
event.node.req,
|
|
event.node.res,
|
|
result,
|
|
{ mergeWithLastSubmission: false }
|
|
);
|
|
return;
|
|
}
|
|
} catch {
|
|
// Token invalid — fall through to login page
|
|
}
|
|
}
|
|
|
|
// No valid session — redirect to login page
|
|
return sendRedirect(event, `/oidc/login?uid=${uid}`, 302);
|
|
}
|
|
|
|
// ----- Consent prompt -----
|
|
if (prompt.name === "consent") {
|
|
// Auto-approve consent for our first-party client
|
|
const grant = interactionDetails.grantId
|
|
? await provider.Grant.find(interactionDetails.grantId)
|
|
: new provider.Grant({
|
|
accountId: interactionDetails.session!.accountId,
|
|
clientId: interactionDetails.params.client_id as string,
|
|
});
|
|
|
|
if (grant) {
|
|
grant.addOIDCScope("openid profile email");
|
|
await grant.save();
|
|
|
|
const result = { consent: { grantId: grant.jti } };
|
|
await provider.interactionFinished(
|
|
event.node.req,
|
|
event.node.res,
|
|
result,
|
|
{ mergeWithLastSubmission: true }
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback — shouldn't reach here normally
|
|
throw createError({ statusCode: 400, statusMessage: "Unknown interaction" });
|
|
});
|