feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset, email verification, social sign-in) to Ory Kratos. The Go API now acts as a resource server: the new KratosAuth middleware validates sessions against the Kratos whoami endpoint, writes the local User mirror into Echo context, and all existing domain handlers continue working unchanged. Hand-rolled token auth, AuthToken model, apple_auth/ google_auth services, and the auth refresh flow are removed. Tests are updated to use the fake-token middleware pattern so existing integration assertions require no rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user