UNPKG

@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
// 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