fix(sso): encrypt SSO provider secrets at rest, and support encrypted SAML assertions - #8045
waleedlatif1 wants to merge 9 commits into
Conversation
The OIDC client secret, and the SAML signing and decryption keys, were stored as plain JSON in sso_provider while every sibling credential is encrypted with ENCRYPTION_KEY, so a copy of the database exposed them without the key. They are now encrypted field-by-field at the Better Auth adapter, which is the only seam the SSO plugin's reads and writes both pass through; the surrounding config stays readable JSON. Values written before this keep working and are encrypted the next time the provider is saved. The providers API also redacts the SAML key material it used to return in full.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
There was a problem hiding this comment.
All reported issues were addressed across 19 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Detecting ciphertext by its iv:ciphertext:authTag shape was ambiguous: a client secret is an arbitrary string chosen at the identity provider, so one shaped like an envelope would have been read back as ciphertext and broken that provider. Encrypted values now carry a versioned prefix. The providers list also lets a decryption failure surface instead of reporting the provider as having no config, and the helper moved next to the adapter that uses it.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 19 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…ion scripts Encrypted assertions are now configured in the settings UI and the registration API: the service provider certificate is published in the SP metadata Sim serves, and the matching private key is stored encrypted and handed to the SAML library for decryption. Request signing is deliberately absent — Better Auth's service provider neither signs requests nor publishes a signing certificate, so a key field for it would do nothing. The operator registration scripts are removed along with the env vars and docs that drove them; the UI covers registration, and the scripts were a second write path with their own copy of the crypto. The field they wrote for assertion decryption was never read by the SAML library, so it never worked. Also: run the new adapter PostgreSQL suite in CI, reject a stored OIDC config whose client secret is missing rather than saving a provider without one, and cover the nested service-provider keys in both encryption at rest and API redaction.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 27 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
The registration log emitted the resolved provider config with only the client secret and IdP certificate redacted, so enabling encrypted assertions wrote the service provider's private key to a log line. It is redacted now, with a test that fails if it ever reaches one again. Both halves of the encryption pair are also parsed and compared rather than checked for PEM markers: a valid certificate paired with a valid key from a different pair used to save cleanly and then fail every sign-in. The tests generate real key material per run instead of committing any.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 27 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
Comparing derived public keys accepted a private key PEM in the certificate field, since a private key satisfies that comparison — and the metadata document was then built by stripping the PEM armor off whatever was pasted, which would have published the private key as the service provider certificate. The certificate is now parsed with X509Certificate, which rejects key PEMs outright, and the document carries that parsed certificate's own DER bytes rather than re-serialized input.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
All reported issues were addressed across 27 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
…a legacy key Both SAML toggles now use ChipSwitch, the canonical control, instead of the legacy Switch primitive; the encrypted-assertions toggle is new here and the signed-assertions one beside it moves with it so the card stays consistent. An update that keeps the stored decryption key also falls back to the flat decryptionPvk that rows from the retired registration script carry, so an admin enabling encryption on such a provider is not asked to re-paste a key they already hold. The pair is validated against the submitted certificate either way.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 27 files
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
The chip switch takes no id, so the labels' htmlFor pointed at elements that no longer exist and clicking one did nothing. They are plain labels beside an aria-labelled control now, matching inbox-enable-toggle. The new field hint also uses the text-caption token rather than text-xs.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 27 files
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
An update that keeps the stored key returned 500 when the key could not be decrypted; it now returns the same kind of 400 the OIDC path does, asking for the key again. Replace gained the Keep saved action its client-secret counterpart has, so opening the field is no longer a one-way door, and the form recognizes a key left flat by the retired registration script, matching the fallback the server already had.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 27 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
…crypted Listing providers now reports an undecryptable config as a provider with no config rather than failing the request. The failure is already logged where it happens, and the form stays reachable — it still offers Replace, which is the only way back now that the operator scripts are gone. A 500 there would have taken the page down with no recovery path. Also reworks the client-secret reuse branch to validate after reading rather than throwing into its own catch, and covers SAML body validation now that its contract branch carries a refinement.
Summary
clientSecret, and the SAML service-provider keys, were stored as plain JSON insso_providerwhile every sibling credential is AES-256-GCM encrypted underENCRYPTION_KEY. A database copy exposed them without the key. They are now encrypted field-by-field at the Better Auth database adapter — the only seam the SSO plugin's reads and writes both pass through, since it loads the provider row itself and handsclientSecretstraight to the authorization URL and token exchangesim.sso.v1:prefix, so a secret that merely looks like ciphertext is never mistaken for itGET /api/auth/sso/providersredacts the key material it previously returned in fulldecryptionPvk) was never read by the SAML library, so it never workedType of Change
Testing
EncryptedAssertionthat the stored-config shape decrypted. No key material is committedlib/auth+app/api/auth/sso+ee/sso+ contracts: 1,639 passed.sim-setup170 passed.type-check(sim, db, sim-setup),lint,check:audits(47),docs-manifest:checkall passRollout
ENCRYPTION_KEYalso breaks SSO sign-in, where a plaintext secret previously survived it. Recovery is re-entering the client secret. The docs changes say soChecklist
🤖 Generated with Claude Code