// Package kratos is a thin client for the Ory Kratos public API. honeyDue // delegates all identity concerns (credentials, sessions, verification, // recovery, social sign-in) to Kratos; this client only validates sessions. package kratos import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" ) // ErrUnauthorized is returned by Whoami when the session is missing, invalid, // or inactive — the caller should respond 401. var ErrUnauthorized = errors.New("kratos: session invalid or inactive") // Client talks to the Ory Kratos public API. type Client struct { publicURL string http *http.Client } // NewClient builds a Kratos client for the given public-API base URL // (e.g. http://kratos:4433 in-cluster). func NewClient(publicURL string) *Client { return &Client{ publicURL: strings.TrimRight(publicURL, "/"), http: &http.Client{Timeout: 5 * time.Second}, } } // Identity is the subset of a Kratos identity honeyDue consumes. It mirrors // the identity schema in deploy-k3s/manifests/kratos/configmap.yaml. type Identity struct { ID string `json:"id"` // UUID — the stable identity identifier Traits struct { Email string `json:"email"` Name struct { First string `json:"first"` Last string `json:"last"` } `json:"name"` } `json:"traits"` VerifiableAddresses []struct { Value string `json:"value"` Verified bool `json:"verified"` } `json:"verifiable_addresses"` } // Session is a Kratos session as returned by GET /sessions/whoami. type Session struct { ID string `json:"id"` Active bool `json:"active"` Identity Identity `json:"identity"` } // EmailVerified reports whether any of the identity's email addresses is // verified — the source of truth for honeyDue's RequireVerified gate. func (s *Session) EmailVerified() bool { for _, a := range s.Identity.VerifiableAddresses { if a.Verified { return true } } return false } // Whoami validates a session against Kratos. Supply the mobile session token // (sessionToken) OR the browser cookie header (cookie) — whichever is // non-empty is forwarded to Kratos. Returns ErrUnauthorized for an invalid or // inactive session. func (c *Client) Whoami(ctx context.Context, sessionToken, cookie string) (*Session, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.publicURL+"/sessions/whoami", nil) if err != nil { return nil, err } if sessionToken != "" { req.Header.Set("X-Session-Token", sessionToken) } if cookie != "" { req.Header.Set("Cookie", cookie) } resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("kratos whoami: %w", err) } defer resp.Body.Close() switch { case resp.StatusCode == http.StatusUnauthorized, resp.StatusCode == http.StatusForbidden: return nil, ErrUnauthorized case resp.StatusCode != http.StatusOK: return nil, fmt.Errorf("kratos whoami: unexpected status %d", resp.StatusCode) } var s Session if err := json.NewDecoder(resp.Body).Decode(&s); err != nil { return nil, fmt.Errorf("kratos whoami: decode: %w", err) } if !s.Active || s.Identity.ID == "" { return nil, ErrUnauthorized } return &s, nil }