Skip to content

x/samlsp: add Assertion.SubjectKey, a stable key for the user - #999

Merged
crewjam merged 2 commits into
mainfrom
claude/great-pasteur-pr0lje-samlsp-subject-key
Nov 13, 2026
Merged

crewjam merged 2 commits into
mainfrom
claude/great-pasteur-pr0lje-samlsp-subject-key

Conversation

@crewjam

@crewjam crewjam commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Summary

samlsp.Assertion exposed only Subject (the NameID value), which does not safely identify a user across logins.

  • x/samlsp requests a transient NameID by default, which changes at every login.
  • Even a persistent NameID is unique only together with the IdP that issued it and its qualifiers (saml-core §2.2.2, §8.3.7).

So an app that keys accounts on Subject can merge users from different IdPs or lose accounts.

Changes

  • Assertion.SubjectKeyParts() (SubjectKeyParts, error) returns a comparable {Issuer, NameQualifier, SPNameQualifier, Format, Value}, filling in the defaults:
    • NameQualifier defaults to the Issuer (§8.3.7).
    • SPNameQualifier defaults to the SP entity ID.
    • Format defaults to unspecified (§8.3.1).
    • SPProvidedID is left out: it is an alias the SP sets, not part of the name.
  • Assertion.SubjectKey() (string, error) encodes those parts as samlsp-subject-v1 followed by :<byte length>:<field> for each field. The encoding is fixed, documented, unambiguous whatever the field contents, and versioned.
  • The Issuer is always part of the key, even when a NameQualifier is present. The NameQualifier is only the IdP's claim, so one IdP cannot name another IdP's users with it.
  • Refused, with an error matching ErrNoSubjectKey:
    • a transient NameID;
    • a BaseID, whose xsi:type can't always be resolved reliably;
    • an empty value or Issuer;
    • an omitted SPNameQualifier when the SP entity ID is unknown.
  • Email and unspecified formats are allowed. The docs say they are only as stable as the IdP keeps them, and recommend persistent.
  • New field Assertion.SPEntityID, filled in by ParseResponse.
  • Docs: the Subject doc now points to SubjectKey. doc.go says to key accounts with SubjectKey, and to qualify any attribute used as a key with the Issuer.

Compatibility notes

  • Additive only.
  • Assertions built by hand have an empty SPEntityID, so SubjectKey refuses them unless the NameID carries an SPNameQualifier.
  • Values are compared byte for byte, with no case or URI normalization.

Testing

  • go build ./..., go vet ./..., go test ./... and go test -race ./x/samlsp/... pass.
  • golangci-lint v2.15 reports 0 issues.
  • New file x/samlsp/subject_key_test.go covers:
    • golden keys, which must never change;
    • omitted defaults producing the same key as explicit ones;
    • different keys for each differing part, including an IdP that claims another IdP as its NameQualifier;
    • unambiguity with values that contain : and digits;
    • the refused cases;
    • end-to-end through ParseResponse.
  • New example: ExampleAssertion_SubjectKey.

Generated by Claude Code

Assertion offered Subject, the NameID's value, and nothing that safely
identifies a user across logins. x/samlsp requests a transient NameID by
default, which changes with every login, and even a persistent NameID is
unique only together with the IdP that issued it and its qualifiers
(saml-core §2.2.2, §8.3.7). An application that keys accounts by Subject
can merge users of different IdPs or lose accounts.

SubjectKeyParts returns Issuer, NameQualifier (Issuer if omitted),
SPNameQualifier (the SP's entity ID if omitted), Format (unspecified if
omitted) and Value, as a comparable struct; SubjectKey encodes them as
"samlsp-subject-v1" followed by ":<length>:<field>" for each, a fixed,
documented and unambiguous encoding that can be stored. Issuer is always
part of the key, so that one IdP cannot name another's users through
NameQualifier. SPProvidedID is left out. A transient NameID, a BaseID
(whose xsi:type QName cannot always be resolved once parsed) and a
missing value or Issuer give an error that matches ErrNoSubjectKey.

To default SPNameQualifier, Assertion gains SPEntityID, the entity ID of
the service provider that accepted the assertion, which ParseResponse
fills in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QFDLnyEpWvYnSDCnCib9sD
…pr0lje-samlsp-subject-key

# Conflicts:
#	x/samlsp/assertion.go
@crewjam
crewjam merged commit 4e6cbd6 into main Nov 13, 2026
11 checks passed
@crewjam
crewjam deleted the claude/great-pasteur-pr0lje-samlsp-subject-key branch November 13, 2026 12:39
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.

2 participants