/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ // All the any rules are disabled because passport typesense with fastify is really poor import { Authenticator, Strategy } from "@fastify/passport"; import fastifySession from "@fastify/session"; import RedisStore from "connect-redis"; import { Redis } from "ioredis"; import { z } from "zod"; import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types"; import { getConfig } from "@app/lib/config/env"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; export const registerOidcRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); const redis = new Redis(appCfg.REDIS_URL); const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); /* - OIDC protocol cannot work without sessions: https://github.com/panva/node-openid-client/issues/190 - Current redis usage is not ideal and will eventually have to be refactored to use a better structure - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js */ const redisStore = new RedisStore({ client: redis, prefix: "oidc-session:", ttl: 600 // 10 minutes }); await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY, store: redisStore, cookie: { secure: appCfg.HTTPS_ENABLED, sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server } }); await server.register(passport.initialize()); await server.register(passport.secureSession()); // redirect to IDP for login server.route({ url: "/login", method: "GET", config: { rateLimit: authRateLimit }, schema: { querystring: z.object({ orgSlug: z.string().trim(), callbackPort: z.string().trim().optional() }) }, preValidation: [ async (req, res) => { const { orgSlug, callbackPort } = req.query; // ensure fresh session state per login attempt await req.session.regenerate(); req.session.set("oidcOrgSlug", orgSlug); if (callbackPort) { req.session.set("callbackPort", callbackPort); } const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug, callbackPort); return ( passport.authenticate(oidcStrategy as Strategy, { scope: "profile email openid" }) as any )(req, res); } ], handler: () => {} }); // callback route after login from IDP server.route({ url: "/callback", method: "GET", preValidation: [ async (req, res) => { const oidcOrgSlug = req.session.get("oidcOrgSlug"); const callbackPort = req.session.get("callbackPort"); const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug, callbackPort); return ( passport.authenticate(oidcStrategy as Strategy, { failureRedirect: "/api/v1/sso/oidc/login/error", session: false, failureMessage: true }) as any )(req, res); } ], handler: async (req, res) => { await req.session.destroy(); if (req.passportUser.isUserCompleted) { return res.redirect( `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` ); } // signup return res.redirect( `${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` ); } }); server.route({ url: "/login/error", method: "GET", handler: async (req, res) => { await req.session.destroy(); return res.status(500).send({ error: "Authentication error", details: req.query }); } }); server.route({ url: "/config", method: "GET", config: { rateLimit: readLimit }, onRequest: verifyAuth([AuthMode.JWT]), schema: { querystring: z.object({ orgSlug: z.string().trim() }), response: { 200: OidcConfigsSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, jwksUri: true, tokenEndpoint: true, userinfoEndpoint: true, configurationType: true, discoveryURL: true, isActive: true, orgId: true, allowedEmailDomains: true }).extend({ clientId: z.string(), clientSecret: z.string() }) } }, handler: async (req) => { const { orgSlug } = req.query; const oidc = await server.services.oidc.getOidc({ orgSlug, type: "external", actor: req.permission.type, actorId: req.permission.id, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod }); return oidc; } }); server.route({ method: "PATCH", url: "/config", config: { rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z .object({ allowedEmailDomains: z .string() .trim() .optional() .default("") .transform((data) => { if (data === "") return ""; // Trim each ID and join with ', ' to ensure formatting return data .split(",") .map((id) => id.trim()) .join(", "); }), discoveryURL: z.string().trim(), configurationType: z.nativeEnum(OIDCConfigurationType), issuer: z.string().trim(), authorizationEndpoint: z.string().trim(), jwksUri: z.string().trim(), tokenEndpoint: z.string().trim(), userinfoEndpoint: z.string().trim(), clientId: z.string().trim(), clientSecret: z.string().trim(), isActive: z.boolean() }) .partial() .merge(z.object({ orgSlug: z.string() })), response: { 200: OidcConfigsSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, configurationType: true, discoveryURL: true, jwksUri: true, tokenEndpoint: true, userinfoEndpoint: true, orgId: true, allowedEmailDomains: true, isActive: true }) } }, handler: async (req) => { const oidc = await server.services.oidc.updateOidcCfg({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); return oidc; } }); server.route({ method: "POST", url: "/config", config: { rateLimit: writeLimit }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z .object({ allowedEmailDomains: z .string() .trim() .optional() .default("") .transform((data) => { if (data === "") return ""; // Trim each ID and join with ', ' to ensure formatting return data .split(",") .map((id) => id.trim()) .join(", "); }), configurationType: z.nativeEnum(OIDCConfigurationType), issuer: z.string().trim().optional().default(""), discoveryURL: z.string().trim().optional().default(""), authorizationEndpoint: z.string().trim().optional().default(""), jwksUri: z.string().trim().optional().default(""), tokenEndpoint: z.string().trim().optional().default(""), userinfoEndpoint: z.string().trim().optional().default(""), clientId: z.string().trim(), clientSecret: z.string().trim(), isActive: z.boolean(), orgSlug: z.string().trim() }) .superRefine((data, ctx) => { if (data.configurationType === OIDCConfigurationType.CUSTOM) { if (!data.issuer) { ctx.addIssue({ path: ["issuer"], message: "Issuer is required", code: z.ZodIssueCode.custom }); } if (!data.authorizationEndpoint) { ctx.addIssue({ path: ["authorizationEndpoint"], message: "Authorization endpoint is required", code: z.ZodIssueCode.custom }); } if (!data.jwksUri) { ctx.addIssue({ path: ["jwksUri"], message: "JWKS URI is required", code: z.ZodIssueCode.custom }); } if (!data.tokenEndpoint) { ctx.addIssue({ path: ["tokenEndpoint"], message: "Token endpoint is required", code: z.ZodIssueCode.custom }); } if (!data.userinfoEndpoint) { ctx.addIssue({ path: ["userinfoEndpoint"], message: "Userinfo endpoint is required", code: z.ZodIssueCode.custom }); } } else { // eslint-disable-next-line no-lonely-if if (!data.discoveryURL) { ctx.addIssue({ path: ["discoveryURL"], message: "Discovery URL is required", code: z.ZodIssueCode.custom }); } } }), response: { 200: OidcConfigsSchema.pick({ id: true, issuer: true, authorizationEndpoint: true, configurationType: true, discoveryURL: true, jwksUri: true, tokenEndpoint: true, userinfoEndpoint: true, orgId: true, isActive: true, allowedEmailDomains: true }) } }, handler: async (req) => { const oidc = await server.services.oidc.createOidcCfg({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, ...req.body }); return oidc; } }); };