Files
summercms/modules/wristband/oidc_subject.go
Jakub Zych e25e73378c feat(19.4-01): OIDC token, keys, subjects and opt-in refresh
- ES256 keys, PKCS#1/SEC1 parsing fallbacks, EC JWKs, mixed RS256/ES256 rings
- Pairwise subjects per sector (SubjectSector, ErrSectorRequired)
- Opt-in refresh_token grant (OIDCOptions.RefreshEnabled): OfflineAccess clients
  only, rotation, lineage revocation on replay, fresh id_token without nonce
- IssueOIDCClientCredentials with the PHP oas_/oas_cs_ shapes
- Smoke tests: token errors, replay revocation, userinfo 401s, mixed key ring,
  pairwise subjects, refresh opt-in
2026-10-19 18:24:45 +02:00

69 lines
2.0 KiB
Go

package wristband
import (
"context"
"errors"
"net/url"
"strings"
"uuid"
)
// The subject types a client can have (D-15).
const (
// SubjectTypePublic gives a user the same sub at every public client.
SubjectTypePublic = "public"
// SubjectTypePairwise gives a user a different sub per sector.
SubjectTypePairwise = "pairwise"
)
// ErrSectorRequired is returned by SubjectSector for a pairwise client
// without a sector identifier whose redirect URIs do not share one host:
// an administrator must set OIDCClientRecord.SectorIdentifier (D-15).
var ErrSectorRequired = errors.New("wristband: pairwise client needs a sector identifier")
// SubjectSector returns the subject sector of a client: "" for a public
// client; for a pairwise client its trimmed SectorIdentifier when set,
// otherwise the host of its redirect URIs when they all share one host,
// otherwise ErrSectorRequired. No remote sector_identifier_uri is fetched.
func SubjectSector(c *OIDCClientRecord) (string, error) {
if c == nil {
return "", errors.New("wristband: no client")
}
switch c.SubjectType {
case "", SubjectTypePublic:
return "", nil
case SubjectTypePairwise:
if sector := strings.TrimSpace(c.SectorIdentifier); sector != "" {
return sector, nil
}
host := ""
for _, uri := range c.RedirectURIs {
u, err := url.Parse(uri)
if err != nil || u.Hostname() == "" {
return "", ErrSectorRequired
}
h := strings.ToLower(u.Hostname())
if host != "" && h != host {
return "", ErrSectorRequired
}
host = h
}
if host == "" {
return "", ErrSectorRequired
}
return host, nil
}
return "", errors.New("wristband: unknown subject type")
}
// subjectFor allocates (or reads) the subject of userID at client: a random
// UUIDv4 per (user, sector), never derived from an account attribute
// (T-19.4-13).
func subjectFor(ctx context.Context, tx OIDCTx, client *OIDCClientRecord, userID uint) (string, error) {
sector, err := SubjectSector(client)
if err != nil {
return "", err
}
return tx.SubjectFor(ctx, userID, sector, uuid.NewV4().String())
}