Skip to content

fix(api): build SAML entityID and SSO endpoints from our own host, not Slack's - #161

Merged
themightychris merged 3 commits into
developfrom
fix/saml-self-host
Sep 9, 2026
Merged

fix(api): build SAML entityID and SSO endpoints from our own host, not Slack's#161
themightychris merged 3 commits into
developfrom
fix/saml-self-host

Conversation

@themightychris

Copy link
Copy Markdown
Member

Problem

The SAML IdP metadata at https://next.codeforphilly.org/api/saml/slack/metadata advertised

entityID="https://codeforphilly.slack.com/api/saml/slack/metadata"
Location="https://codeforphilly.slack.com/api/saml/slack/sso"

because getSamlContext built both from SLACK_TEAM_HOST — Slack's host, not ours. plans/saml-idp.md recorded this as a known follow-up; specs/api/saml.md already said the entityID is our own URL.

Change

Spec first (specs/api/saml.md gains an "IdP identity and hosts" section), then code:

Value Source Where it appears
IdP entity ID SAML_ENTITY_ID (new, optional; default https://codeforphilly.org/api/saml/slack/metadata) metadata entityID; <saml:Issuer> on the Response and the Assertion
SSO endpoint URLs https://<CFP_SITE_HOST>/api/saml/slack/sso metadata SingleSignOnService/@Location (POST + Redirect)
Slack host SLACK_TEAM_HOST (unchanged) ACS URL / form action / Destination / Recipient, NameID NameQualifier, /launch + /chat redirects

SAML_ENTITY_ID is deliberately independent of CFP_SITE_HOST: Slack stores the issuer at setup time, so the pre-cutover next.codeforphilly.org deploy and the post-cutover codeforphilly.org deploy must present the same identifier. Leave it unset everywhere Slack should trust the production IdP identity; only set it when registering a separate IdP (e.g. a sandbox against a test workspace). The endpoint Locations do follow CFP_SITE_HOST, so at cutover Slack's IdP config needs a metadata refresh for the URLs — but the trust relationship is untouched.

The entity ID and the assertion Issuer flow from the same SamlIdpSettings.entityId (buildSlackSamlEntities → samlify IdentityProvider({ entityID }) for metadata; SlackSamlEntities.entityIdissuerEntityId{Issuer} in the response template). Verified rather than assumed; config.ts only gained doc comments.

Files

  • specs/api/saml.md, specs/architecture.md — the rules + env table rows
  • docs/operations/deploy.md, docs/operations/secrets.md, .env.example — operator-facing description of SAML_ENTITY_ID and SLACK_TEAM_HOST
  • apps/api/src/env.tsSAML_ENTITY_ID (zod + JSON schema), doc comments on SLACK_TEAM_HOST / CFP_SITE_HOST
  • apps/api/src/routes/saml.tsgetSamlContext builds from SAML_ENTITY_ID + CFP_SITE_HOST
  • apps/api/src/saml/config.ts — doc comments only
  • apps/api/tests/saml.test.ts — updated entityID expectation; new assertions for Locations, Issuer; new describe covering CFP_SITE_HOST=next.example.org (Locations move, entityID stays) and an explicit SAML_ENTITY_ID override (metadata + Issuer both follow)
  • plans/saml-self-host.md — plan, closed out in the last commit

Validation

  • npm run type-check + npm run lint clean
  • npm test: api 34 files / 429 tests pass (saml.test.ts 11/11). One unrelated web test (ProjectEdit.test.tsx) hit its 5s timeout under full-suite load and passes in isolation.

Operator note

If the next. deployment's metadata was already uploaded to Slack, Slack has the wrong issuer on file and will reject assertions after this ships until its SAML config is re-synced from the metadata URL. Coordinate with the workspace admin before deploying.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr

themightychris added a commit that referenced this pull request Sep 9, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
themightychris and others added 3 commits September 8, 2026 20:14
The live IdP metadata advertises entityID and SingleSignOnService
Locations on the Slack team host because the route derived both from
SLACK_TEAM_HOST. Spec now separates the three values by source: the
entity ID (metadata entityID + assertion Issuer) is a stable logical
identifier from a new optional SAML_ENTITY_ID env var, defaulting to
https://codeforphilly.org/api/saml/slack/metadata and deliberately
independent of CFP_SITE_HOST so the pre-/post-cutover host flip doesn't
invalidate the issuer Slack stored at setup; endpoint Locations follow
CFP_SITE_HOST; SLACK_TEAM_HOST keeps only its Slack-side roles.

Env tables in architecture.md, deploy.md, secrets.md and .env.example
gain the new var. Adds plans/saml-self-host.md (in-progress) to carry
the code change, closing the follow-up left open by plans/saml-idp.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
getSamlContext derived the IdP entity ID and both SingleSignOnService
Locations from SLACK_TEAM_HOST, so the live metadata advertised
https://codeforphilly.slack.com/api/saml/slack/... — Slack's host, not
ours. Per specs/api/saml.md#idp-identity-and-hosts:

- entityId (metadata entityID + assertion Issuer, one value flowing
  through SlackSamlEntities.entityId → issuerEntityId) now comes from the
  new SAML_ENTITY_ID env var, defaulting to
  https://codeforphilly.org/api/saml/slack/metadata. It is a stable
  logical identifier and intentionally does not follow CFP_SITE_HOST, so
  the next.codeforphilly.org → codeforphilly.org flip at cutover leaves
  the issuer Slack stored at setup untouched.
- ssoLoginPostUrl / ssoLoginRedirectUrl are built on CFP_SITE_HOST so
  the metadata points Slack at the host actually serving the API.
- SLACK_TEAM_HOST keeps only its Slack-side roles (ACS URL, NameID
  NameQualifier, launch redirect).

Tests assert the default entityID, the CFP_SITE_HOST-driven Locations,
Issuer == entityID on both Response and Assertion, that
CFP_SITE_HOST=next.example.org moves the Locations without moving the
entityID, and that an explicit SAML_ENTITY_ID flows to both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
@themightychris
themightychris merged commit 2d4fabd into develop Sep 9, 2026
1 check failed
@themightychris
themightychris deleted the fix/saml-self-host branch September 9, 2026 00:15
@themightychris themightychris mentioned this pull request Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant