~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
138 lines · 4.0 KB Raw History
package ingress

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/atcrypto"
	"github.com/bluesky-social/indigo/atproto/auth"
	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/golang-jwt/jwt/v5"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/didkey"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
)

var serviceAuthMethods = []string{"ES256K", "ES256"}

func NewDirectory(plcURL string) identity.Directory {
	if plcURL == "" {
		return identity.DefaultDirectory()
	}
	base := &identity.BaseDirectory{
		PLCURL:                 strings.TrimSuffix(plcURL, "/"),
		HTTPClient:             http.Client{Timeout: 10 * time.Second},
		PLCClient:              &http.Client{Timeout: 10 * time.Second},
		SkipHandleVerification: true,
	}
	return identity.NewCacheDirectory(base, 100_000, time.Hour, 2*time.Minute, 5*time.Minute)
}

func BearerToken(authHeader, queryToken string) (string, error) {
	if authHeader != "" {
		parts := strings.Split(authHeader, " ")
		if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
			return "", errors.New("Authorization header must be Bearer <token>")
		}
		return parts[1], nil
	}
	if queryToken != "" {
		return queryToken, nil
	}
	return "", errors.New("missing Authorization header")
}

type ServiceAuth struct {
	Issuer string

	ExpiresAt time.Time
}

func VerifyServiceAuth(ctx context.Context, token, aud, lxm string, dir identity.Directory) (ServiceAuth, error) {
	if token == "" {
		return ServiceAuth{}, errors.New("missing Authorization header")
	}
	claims := jwt.MapClaims{}
	opts := []jwt.ParserOption{
		jwt.WithValidMethods(serviceAuthMethods),
		jwt.WithAudience(aud),
		jwt.WithExpirationRequired(),
		jwt.WithLeeway(5 * time.Second),
	}
	keyFunc := func(t *jwt.Token) (any, error) {
		iss, err := claims.GetIssuer()
		if err != nil {
			return nil, fmt.Errorf("missing iss claim: %w", err)
		}
		return resolveSigningKey(ctx, iss, dir)
	}
	parsed, err := jwt.ParseWithClaims(token, claims, keyFunc, opts...)
	if err != nil && errors.Is(err, jwt.ErrTokenSignatureInvalid) {
		insecure := jwt.MapClaims{}
		if _, _, uerr := jwt.NewParser().ParseUnverified(token, insecure); uerr == nil {
			if iss, ierr := insecure.GetIssuer(); ierr == nil && dir != nil {
				iss, _, _ = strings.Cut(iss, "#")
				if did, derr := syntax.ParseDID(iss); derr == nil {
					_ = dir.Purge(ctx, did.AtIdentifier())
				}
			}
			claims = jwt.MapClaims{}
			parsed, err = jwt.ParseWithClaims(token, claims, keyFunc, opts...)
		}
	}
	if err != nil {
		return ServiceAuth{}, err
	}
	if !parsed.Valid {
		return ServiceAuth{}, errors.New("service auth token is not valid")
	}
	gotLXM, _ := claims["lxm"].(string)
	if gotLXM != lxm {
		return ServiceAuth{}, fmt.Errorf("lxm mismatch: expected %s, got %s", lxm, gotLXM)
	}
	iss, err := claims.GetIssuer()
	if err != nil {
		return ServiceAuth{}, err
	}
	exp, err := claims.GetExpirationTime()
	if err != nil {
		return ServiceAuth{}, fmt.Errorf("missing exp claim: %w", err)
	}
	if exp == nil {
		return ServiceAuth{}, errors.New("missing exp claim")
	}
	return ServiceAuth{Issuer: iss, ExpiresAt: exp.Time}, nil
}

func resolveSigningKey(ctx context.Context, did string, dir identity.Directory) (atcrypto.PublicKey, error) {
	did, _, _ = strings.Cut(did, "#")
	if strings.HasPrefix(did, didkey.Prefix) {
		return didkey.ParsePublic(did)
	}
	if dir == nil {
		return nil, fmt.Errorf("cannot resolve signing key for %s", did)
	}
	parsed, err := syntax.ParseDID(did)
	if err != nil {
		return nil, err
	}
	ident, err := dir.LookupDID(ctx, parsed)
	if err != nil {
		return nil, fmt.Errorf("resolving DID (%s): %w", did, err)
	}
	return ident.PublicKey()
}

func SignServiceAuth(priv atcrypto.PrivateKey, iss, aud, lxm string, ttl time.Duration) (string, error) {
	nsid, err := syntax.ParseNSID(lxm)
	if err != nil {
		return "", err
	}
	ttl = opt.Positive(ttl, 5*time.Minute)
	return auth.SignServiceAuth(syntax.DID(iss), aud, ttl, &nsid, priv)
}