1
21import { Inject, Injectable } from "@nestjs/common";
22import { PrismaService } from "../../common/prisma.service.js";
23import { ApiError } from "../../common/errors.js";
24import { loadEnv } from "../../config/env.js";
25import { aes256gcmDecrypt, aes256gcmEncrypt } from "../../lib/crypto.js";
26
27export interface SsoSecrets {
28 certificate?: string;
29 previousCertificate?: string;
30 clientSecret?: string;
31}
32
33@Injectable()
34export class SsoSecretsService {
35 constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
36
37 private key(): string {
38 const k = loadEnv().MASTER_ENCRYPTION_KEY;
39 if (!k) {
40 throw new ApiError(
41 "INTERNAL_ERROR",
42 500,
43 "MASTER_ENCRYPTION_KEY is not configured — SSO secrets cannot be stored."
44 );
45 }
46 return k;
47 }
48
49 private readSsoConfig(orgId: string): Promise<Record<string, unknown> | undefined> {
50 return this.prisma.organization
51 .findUnique({ where: { id: orgId }, select: { ssoConfig: true } })
52 .then((o) => (o?.ssoConfig as Record<string, unknown> | null) ?? undefined);
53 }
54
55
60 async saveSecrets(
61 orgId: string,
62 input: {
63 certificate?: string | null;
64 previousCertificate?: string | null;
65 clientSecret?: string | null
66 }
67 ): Promise<SsoSecrets> {
68 const cfg = (await this.readSsoConfig(orgId)) ?? {};
69 if (input.certificate !== undefined) {
70 cfg.certificateEncrypted =
71 input.certificate === null ? undefined : aes256gcmEncrypt(input.certificate, this.key());
72 }
73 if (input.previousCertificate !== undefined) {
74 cfg.previousCertificateEncrypted =
75 input.previousCertificate === null
76 ? undefined
77 : aes256gcmEncrypt(input.previousCertificate, this.key());
78 }
79 if (input.clientSecret !== undefined) {
80 cfg.clientSecretEncrypted =
81 input.clientSecret === null ? undefined : aes256gcmEncrypt(input.clientSecret, this.key());
82 }
83
84 const next: Record<string, unknown> = {};
85 for (const [k, v] of Object.entries(cfg)) if (v !== undefined) next[k] = v;
86 await this.prisma.organization.update({
87 where: { id: orgId },
88 data: { ssoConfig: next as object }
89 });
90 return this.getSecrets(orgId);
91 }
92
93
94 async getSecrets(orgId: string): Promise<SsoSecrets> {
95 const cfg = (await this.readSsoConfig(orgId)) ?? {};
96 const k = this.key();
97 const dec = (v: unknown): string | undefined => {
98 if (typeof v !== "string" || !v) return undefined;
99 try {
100 return aes256gcmDecrypt(v, k);
101 } catch {
102 return undefined;
103 }
104 };
105 return {
106 certificate: dec(cfg.certificateEncrypted),
107 previousCertificate: dec(cfg.previousCertificateEncrypted),
108 clientSecret: dec(cfg.clientSecretEncrypted)
109 };
110 }
111
112
113 async hasClientSecret(orgId: string): Promise<boolean> {
114 const cfg = (await this.readSsoConfig(orgId)) ?? {};
115 return typeof cfg.clientSecretEncrypted === "string" && cfg.clientSecretEncrypted.length > 0;
116 }
117}
118