@open-condo/miniapp-utils
Version:
A set of helper functions / components / hooks used to build new condo apps fast
219 lines (215 loc) • 8.67 kB
JavaScript
// src/helpers/oidc.ts
import { generators, Issuer } from "openid-client";
import { z } from "zod";
// src/helpers/ip/utils.ts
var v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])";
var v4Str = `(${v4Seg}[.]){3}${v4Seg}`;
var IPv4Reg = new RegExp(`^${v4Str}$`);
var v6Seg = "(?:[0-9a-fA-F]{1,4})";
var IPv6Reg = new RegExp(
`^((?:${v6Seg}:){7}(?:${v6Seg}|:)|(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:)))(%[0-9a-zA-Z]{1,})?$`
);
// src/helpers/urls.ts
var REGEXP_ESCAPE_CHARS = /[\\^$.*+?()[\]{}|]/g;
var WILDCARD_REGEXP_PART = "([a-zA-Z0-9-]{1,63})";
var WILDCARD_REGEXP_PART_ESCAPED = _escapeRegexp(WILDCARD_REGEXP_PART);
function isSafeUrl(url) {
if (!url || typeof url !== "string") return false;
let decodedUrl;
try {
decodedUrl = decodeURI(url);
} catch (error) {
return false;
}
const normalizedUrl = decodedUrl.replace(/[\u0000-\u001F\s]/g, "").toLowerCase();
return !normalizedUrl.includes("javascript:");
}
function _escapeRegexp(source) {
return source.replace(REGEXP_ESCAPE_CHARS, "\\$&");
}
// src/helpers/uuid.ts
import { randomBytes } from "crypto";
function generateUUIDv4() {
let randomValues;
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
} else if (typeof window !== "undefined" && window.crypto && window.crypto.getRandomValues) {
randomValues = new Uint8Array(16);
window.crypto.getRandomValues(randomValues);
} else {
randomValues = randomBytes(16);
}
randomValues[6] = randomValues[6] & 15 | 64;
randomValues[8] = randomValues[8] & 63 | 128;
return [...randomValues].map((value, index) => {
const hex = value.toString(16).padStart(2, "0");
if (index === 4 || index === 6 || index === 8 || index === 10) {
return `-${hex}`;
}
return hex;
}).join("");
}
// src/helpers/oidc.ts
var _OIDCMiddleware = class _OIDCMiddleware {
static getQueryParams(req) {
return new URL(req.url || "/", "https://_").searchParams;
}
constructor({
getSession,
oidcConfig,
redirectUri,
logger,
onAuthSuccess,
onError,
middlewareOptions
}) {
this.getSession = getSession;
const { serverUrl, clientId, clientSecret, clientOptions, issuerOptions, scope } = oidcConfig;
const issuer = new Issuer({
authorization_endpoint: `${serverUrl}/oidc/auth`,
token_endpoint: `${serverUrl}/oidc/token`,
end_session_endpoint: `${serverUrl}/oidc/session/end`,
jwks_uri: `${serverUrl}/oidc/jwks`,
revocation_endpoint: `${serverUrl}/oidc/token/revocation`,
userinfo_endpoint: `${serverUrl}/oidc/me`,
issuer: serverUrl,
...issuerOptions || {}
});
this.redirectUris = Array.isArray(redirectUri) ? redirectUri : [redirectUri];
this.middlewareOptions = middlewareOptions;
this.onAuthSuccess = onAuthSuccess;
this.onError = onError;
this.scope = scope || "openid";
this.client = new issuer.Client({
client_id: clientId,
client_secret: clientSecret,
redirect_uris: this.redirectUris,
response_types: ["code"],
token_endpoint_auth_method: "client_secret_basic",
...clientOptions || {}
});
this.logger = logger || console;
this.sendError = this.sendError.bind(this);
this.getAuthHandler = this.getAuthHandler.bind(this);
this.getCallbackHandler = this.getCallbackHandler.bind(this);
this.prepareMiddleware = this.prepareMiddleware.bind(this);
}
sendError(err, req, res, next) {
if (next && this.onError) {
this.onError(err, req, res, next);
return;
}
if (next) {
next(err);
return;
}
const errId = generateUUIDv4();
this.logger.error({ msg: "oidc auth error", errId, err });
res.writeHead(500, { "Content-Type": "text/plain" });
res.end(`OIDC auth error: ${errId}`);
}
getAuthHandler() {
const sessionGetter = this.getSession;
const client = this.client;
const sendError = this.sendError;
const scope = this.scope;
const redirectUris = this.redirectUris;
return async function authHandler(req, res, next) {
const session = await sessionGetter(req, res);
try {
const query = _OIDCMiddleware.getQueryParams(req);
const queryRedirectUri = query.get("redirect_uri");
const next2 = query.get("next");
if (next2 && isSafeUrl(next2)) {
session[_OIDCMiddleware.OIDC_NEXT_URL_KEY] = next2;
} else {
delete session[_OIDCMiddleware.OIDC_NEXT_URL_KEY];
}
const redirectUri = redirectUris.find((uri) => uri === queryRedirectUri) ?? redirectUris[0];
const checks = { nonce: generators.nonce(), state: generators.state() };
session[_OIDCMiddleware.OIDC_CHECKS_KEY] = { ...checks };
session[_OIDCMiddleware.OIDC_REDIRECT_URI_KEY] = redirectUri;
await session.save();
const authUrl = client.authorizationUrl({
scope,
redirect_uri: redirectUri,
...checks
});
res.writeHead(302, { Location: authUrl });
res.end();
} catch (err) {
delete session[_OIDCMiddleware.OIDC_CHECKS_KEY];
delete session[_OIDCMiddleware.OIDC_NEXT_URL_KEY];
await session.save();
return sendError(err, req, res, next);
}
};
}
getCallbackHandler() {
const sessionGetter = this.getSession;
const sendError = this.sendError;
const client = this.client;
const onAuthSuccess = this.onAuthSuccess;
const redirectUris = this.redirectUris;
return async function callbackHandler(req, res, next) {
let session = await sessionGetter(req, res);
try {
const { success, data: checks } = _OIDCMiddleware.CHECK_SCHEMA.safeParse(session[_OIDCMiddleware.OIDC_CHECKS_KEY]);
const nextUrl = session[_OIDCMiddleware.OIDC_NEXT_URL_KEY];
const redirectUri = session[_OIDCMiddleware.OIDC_REDIRECT_URI_KEY];
if (typeof redirectUri !== "string" || !redirectUris.includes(redirectUri)) {
return sendError(new Error("Invalid redirect URI"), req, res, next);
}
if (!success) {
return sendError(new Error("Invalid nonce or state"), req, res, next);
}
const params = client.callbackParams(req);
const { access_token: accessToken, refresh_token: refreshToken, id_token: idToken } = await client.callback(redirectUri, params, checks);
let userInfo;
if (accessToken) {
userInfo = await client.userinfo(accessToken);
}
delete session[_OIDCMiddleware.OIDC_CHECKS_KEY];
delete session[_OIDCMiddleware.OIDC_NEXT_URL_KEY];
await session.save();
if (onAuthSuccess) {
await onAuthSuccess(req, res, { accessToken, refreshToken, idToken, userInfo });
session = await sessionGetter(req, res);
}
session[_OIDCMiddleware.OIDC_ID_TOKEN_KEY] = idToken;
session[_OIDCMiddleware.OIDC_ACCESS_TOKEN_KEY] = accessToken;
session[_OIDCMiddleware.OIDC_REFRESH_TOKEN_KEY] = refreshToken;
await session.save();
const location = typeof nextUrl === "string" && isSafeUrl(nextUrl) ? nextUrl : "/";
res.writeHead(302, { Location: location });
res.end();
} catch (err) {
delete session[_OIDCMiddleware.OIDC_CHECKS_KEY];
delete session[_OIDCMiddleware.OIDC_NEXT_URL_KEY];
await session.save();
return sendError(err, req, res, next);
}
};
}
prepareMiddleware() {
if (!this.middlewareOptions) {
return null;
}
const { app, apiPrefix = "/api/oidc" } = this.middlewareOptions;
app.get(`${apiPrefix}/auth`, this.getAuthHandler());
app.get(`${apiPrefix}/callback`, this.getCallbackHandler());
return app;
}
};
_OIDCMiddleware.OIDC_ID_TOKEN_KEY = "oidcIdToken";
_OIDCMiddleware.OIDC_ACCESS_TOKEN_KEY = "oidcAccessToken";
_OIDCMiddleware.OIDC_REFRESH_TOKEN_KEY = "oidcRefreshToken";
_OIDCMiddleware.OIDC_NEXT_URL_KEY = "oidcNextUrl";
_OIDCMiddleware.OIDC_CHECKS_KEY = "oidcChecks";
_OIDCMiddleware.OIDC_REDIRECT_URI_KEY = "oidcRedirectUri";
_OIDCMiddleware.CHECK_SCHEMA = z.object({ nonce: z.string(), state: z.string() });
var OIDCMiddleware = _OIDCMiddleware;
export {
OIDCMiddleware
};
//# sourceMappingURL=oidc.mjs.map