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
94
server/routes/oidc/interaction/[uid].get.ts
Normal file
94
server/routes/oidc/interaction/[uid].get.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* 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" });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue