personal-oidc/provider.ts
2026-08-03 10:46:39 -06:00

112 lines
3.1 KiB
TypeScript

import express from "express";
import * as oidc from "oidc-provider";
import * as dotenv from "dotenv";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import Handlebars from "handlebars";
import assert from "assert";
import { readFileSync } from "fs";
dotenv.config({ quiet: true });
const AUTH_PROVIDER_PORT = Number(process.env.AUTH_PROVIDER_PORT) || 3000;
const ACCOUNT_USERNAME = String(process.env.ACCOUNT_USERNAME) || "unknown_user";
const PROVIDER_ISSUER = String(process.env.PROVIDER_ISSUER);
const provider_login_page = Handlebars.compile(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "provider.html"), {
encoding: "utf-8",
}),
);
const provider = new oidc.Provider(PROVIDER_ISSUER, {
features: {
registration: {
enabled: true,
initialAccessToken: false,
issueRegistrationAccessToken: true,
},
devInteractions: {
enabled: false,
},
},
async findAccount(ctx, id) {
assert(id == ACCOUNT_USERNAME);
return {
accountId: id,
async claims(use, scope) {
return { sub: id };
},
};
},
});
const app = express();
app.get("/interaction/:uid", async (req, res) => {
const details = await provider.interactionDetails(req, res);
const login = details.prompt.name == "login";
res.header("Content-Type", "text/html");
res.send(provider_login_page({ login, details: JSON.stringify(details, null, 3) }));
});
app.post("/interaction/:uid", async (req, res) => {
const interactionDetails = await provider.interactionDetails(req, res);
const details = interactionDetails.prompt.details;
let grant: oidc.Grant | undefined;
if (interactionDetails.grantId) {
grant = await provider.Grant.find(interactionDetails.grantId);
}
if (typeof grant == "undefined") {
if (
interactionDetails.session &&
typeof interactionDetails.params.client_id == "string"
) {
grant = new provider.Grant({
accountId: interactionDetails.session.accountId,
clientId: interactionDetails.params.client_id,
});
}
}
let consent: { consent: { grantId: string } } | {} = {};
if (typeof grant != "undefined") {
if (details.missingOIDCScope instanceof Array) {
grant.addOIDCScope(details.missingOIDCScope.join(" "));
}
if (details.missingOIDCClaims instanceof Array) {
grant.addOIDCClaims(details.missingOIDCClaims);
}
if (details.missingResourceScopes) {
for (const [indicator, scopes] of Object.entries(
details.missingResourceScopes,
)) {
grant.addResourceScope(indicator, scopes.join(" "));
}
}
const grantId = await grant.save();
if (interactionDetails.grantId != grantId) {
consent = {
consent: {
grantId,
},
};
}
}
const result = {
login: {
accountId: ACCOUNT_USERNAME,
},
...consent,
};
const error = {
error: "access_denied",
};
return provider.interactionFinished(req, res, result); // result object below
});
app.use(provider.callback());
app.listen(AUTH_PROVIDER_PORT, (err) => {
if (!err) {
console.log(`Auth provider listening on port ${AUTH_PROVIDER_PORT}`);
}
});