~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
363 lines · 8.8 KB Raw History
package identity

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"maps"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/oauthclient"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/pdsclient"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/xrpc"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/pds"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/strs"
)

const callTimeout = 30 * time.Second

var (
	accessKeys = []string{"accessJwt", "accessToken", "access_token", "token", "value"}

	refreshKeys = []string{"refreshJwt", "refreshToken", "refresh_token"}
)

var ErrNotHeld = errors.New("identity: no account session is held")

type Store interface {
	Read(ctx context.Context) (map[string]any, error)

	Write(ctx context.Context, data map[string]any) error
}

type Authenticator struct {
	mu sync.Mutex

	did string

	session *oauthclient.Session

	bearer *Bearer

	proxy *Proxy

	auth xrpc.Auth
}

type Proxy struct {
	PdsURL string

	HTTP *http.Client

	Prover xrpc.DPoPProver
}

var (
	_ pds.Authenticator = (*Authenticator)(nil)
	_ pds.ServiceAuth   = (*Authenticator)(nil)
	_ pds.ScopeGrants   = (*Authenticator)(nil)
)

func NewOAuth() *Authenticator {
	a := &Authenticator{}
	source := oauthSource{a: a}
	a.auth = pdsclient.TokenAuth(source, source)
	return a
}

func NewProxied() *Authenticator {
	a := &Authenticator{}
	a.auth = &xrpc.DPoP{Prover: proxySource{a: a}}
	return a
}

func NewBearer() *Authenticator {
	a := &Authenticator{}
	a.auth = pdsclient.TokenAuth(bearerSource{a: a}, nil)
	return a
}

func (a *Authenticator) Hold(session *oauthclient.Session) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.session, a.bearer, a.proxy, a.did = session, nil, nil, session.DID()
}

func (a *Authenticator) HoldBearer(did string, bearer *Bearer) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.session, a.bearer, a.proxy, a.did = nil, bearer, nil, did
}

func (a *Authenticator) HoldProxy(did string, proxy *Proxy) {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.session, a.bearer, a.proxy, a.did = nil, nil, proxy, did
}

func (a *Authenticator) Drop() {
	a.mu.Lock()
	defer a.mu.Unlock()
	a.session, a.bearer, a.proxy = nil, nil, nil
}

func (a *Authenticator) Proxied() *Proxy {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.proxy
}

func (a *Authenticator) Session() *oauthclient.Session {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.session
}

func (a *Authenticator) Bearer() *Bearer {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.bearer
}

func (a *Authenticator) DID() string {
	a.mu.Lock()
	defer a.mu.Unlock()
	return a.did
}

func (a *Authenticator) Apply(req *http.Request) error { return a.auth.Apply(req) }

func (a *Authenticator) Invalidate() { a.auth.Invalidate() }

func (a *Authenticator) Challenge(resp *http.Response) pds.Verdict {
	if challenger, ok := a.auth.(xrpc.Challenger); ok {
		switch challenger.Challenge(resp) {
		case xrpc.RetryNonce:
			return pds.VerdictRetryNonce
		case xrpc.RetryAuth:
			return pds.VerdictRetryAuth
		}
		return pds.VerdictDone
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return pds.VerdictRetryAuth
	}
	return pds.VerdictDone
}

func (a *Authenticator) ServiceAuth(ctx context.Context, did, audience, lxm string, expires time.Time) (string, error) {
	a.mu.Lock()
	held, session, bearer, proxy := a.did, a.session, a.bearer, a.proxy
	a.mu.Unlock()
	if did != held {
		return "", fmt.Errorf("identity: the held session signs for %q, not %s", held, did)
	}
	switch {
	case session != nil:
		return session.ServiceAuth(ctx, audience, lxm, expires)
	case bearer != nil:
		client := &xrpc.Client{Host: bearer.pdsURL, HTTP: bearer.http, Auth: a.auth}
		return xrpc.GetServiceAuth(ctx, client, audience, lxm, expires)
	case proxy != nil:
		client := &xrpc.Client{Host: strings.TrimRight(proxy.PdsURL, "/"), HTTP: proxy.HTTP, Auth: a.auth}
		return xrpc.GetServiceAuth(ctx, client, audience, lxm, expires)
	}
	return "", fmt.Errorf("%w for %s", ErrNotHeld, did)
}

func (a *Authenticator) GrantedScopes(did string) ([]string, bool) {
	a.mu.Lock()
	held, session := a.did, a.session
	a.mu.Unlock()
	if did != held || session == nil {
		return nil, false
	}
	granted := session.Scopes()
	return granted, len(granted) > 0
}

func (a *Authenticator) heldSession() (*oauthclient.Session, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.session == nil {
		return nil, fmt.Errorf("%w: %w", pdsclient.ErrAuthenticationRequired, ErrNotHeld)
	}
	return a.session, nil
}

func (a *Authenticator) heldBearer() (*Bearer, error) {
	a.mu.Lock()
	defer a.mu.Unlock()
	if a.bearer == nil {
		return nil, fmt.Errorf("%w: %w", pdsclient.ErrAuthenticationRequired, ErrNotHeld)
	}
	return a.bearer, nil
}

type oauthSource struct {
	a *Authenticator
}

var (
	_ pdsclient.TokenSource     = oauthSource{}
	_ pdsclient.TokenRefresher  = oauthSource{}
	_ pdsclient.DPoPSource      = oauthSource{}
	_ pdsclient.DPoPNonceSetter = oauthSource{}
)

func (s oauthSource) AccessToken(ctx context.Context) (string, error) {
	session, err := s.a.heldSession()
	if err != nil {
		return "", err
	}
	return session.AccessToken(ctx)
}

func (s oauthSource) RefreshAccessToken(ctx context.Context) (string, error) {
	session, err := s.a.heldSession()
	if err != nil {
		return "", err
	}
	return session.RefreshAccessToken(ctx)
}

func (s oauthSource) DPoPProof(ctx context.Context, method, target, accessToken string) (string, error) {
	session, err := s.a.heldSession()
	if err != nil {
		return "", err
	}
	return session.DPoPProof(ctx, method, target, accessToken)
}

func (s oauthSource) SetDPoPNonce(ctx context.Context, host, nonce string) error {
	session, err := s.a.heldSession()
	if err != nil {
		return err
	}
	return session.SetDPoPNonce(ctx, host, nonce)
}

type proxySource struct {
	a *Authenticator
}

var _ xrpc.DPoPProver = proxySource{}

func (s proxySource) ProveDPoP(ctx context.Context, method, target, accessToken, nonce string) (xrpc.DPoPProof, error) {
	s.a.mu.Lock()
	proxy := s.a.proxy
	s.a.mu.Unlock()
	if proxy == nil || proxy.Prover == nil {
		return xrpc.DPoPProof{}, fmt.Errorf("%w: %w", pdsclient.ErrAuthenticationRequired, ErrNotHeld)
	}
	return proxy.Prover.ProveDPoP(ctx, method, target, accessToken, nonce)
}

type bearerSource struct {
	a *Authenticator
}

var (
	_ pdsclient.TokenSource    = bearerSource{}
	_ pdsclient.TokenRefresher = bearerSource{}
)

func (s bearerSource) AccessToken(ctx context.Context) (string, error) {
	bearer, err := s.a.heldBearer()
	if err != nil {
		return "", err
	}
	return bearer.AccessToken(ctx)
}

func (s bearerSource) RefreshAccessToken(ctx context.Context) (string, error) {
	bearer, err := s.a.heldBearer()
	if err != nil {
		return "", err
	}
	return bearer.RefreshAccessToken(ctx)
}

type Bearer struct {
	store Store

	pdsURL string

	http *http.Client

	log *slog.Logger

	mu sync.Mutex

	access string
}

var (
	_ pdsclient.TokenSource    = (*Bearer)(nil)
	_ pdsclient.TokenRefresher = (*Bearer)(nil)
)

func NewBearerTokens(store Store, pdsURL string, client *http.Client, log *slog.Logger) *Bearer {
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	return &Bearer{store: store, pdsURL: strings.TrimRight(strings.TrimSpace(pdsURL), "/"), http: client, log: log}
}

func (t *Bearer) AccessToken(ctx context.Context) (string, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if pdsclient.TokenLive(t.access) {
		return t.access, nil
	}
	return t.renewLocked(ctx, false)
}

func (t *Bearer) RefreshAccessToken(ctx context.Context) (string, error) {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.renewLocked(ctx, true)
}

func (t *Bearer) renewLocked(ctx context.Context, rejected bool) (string, error) {
	data, err := t.store.Read(ctx)
	if err != nil {
		return "", err
	}
	access := strs.Lookup(data, accessKeys...)
	refresh := strs.Lookup(data, refreshKeys...)
	if !rejected && pdsclient.TokenLive(access) {
		t.access = access
		return access, nil
	}
	if refresh == "" || t.pdsURL == "" {
		if access == "" {
			return "", fmt.Errorf("%w: the session holds no live token and no refresh token", pdsclient.ErrAuthenticationRequired)
		}
		t.access = access
		return access, nil
	}
	client, err := pdsclient.NewRemote(pdsclient.RemoteConfig{BaseURL: t.pdsURL, HTTPClient: t.http, Timeout: callTimeout})
	if err != nil {
		return "", err
	}
	session, err := client.RefreshSession(ctx, refresh)
	if err != nil {
		return "", fmt.Errorf("identity: refreshing the session: %w", err)
	}
	t.access = session.AccessJWT
	updated := maps.Clone(data)
	updated["accessJwt"] = session.AccessJWT
	updated["refreshJwt"] = session.RefreshJWT
	updated["access_token"] = session.AccessJWT
	updated["refresh_token"] = session.RefreshJWT
	if err := t.store.Write(ctx, updated); err != nil {
		return "", fmt.Errorf("identity: storing the refreshed session: %w", err)
	}
	t.log.Info("pds_session_refreshed", "did", session.DID.String(), "pds", t.pdsURL, "rejected", rejected)
	return t.access, nil
}