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()) }