~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
103 lines · 2.9 KB Raw History
package oauthclient

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
)

func BindEntrywayAccount(ctx context.Context, client *http.Client, directory identity.Directory, data SessionData) (SessionData, error) {
	entryway := strings.TrimRight(data.AuthServerURL, "/")
	if directory == nil {
		return SessionData{}, fmt.Errorf("oauthclient: no identity directory resolves the account the entryway %s signed in", entryway)
	}
	did, err := syntax.ParseDID(data.AccountDID)
	if err != nil {
		return SessionData{}, fmt.Errorf("oauthclient: the entryway account DID: %w", err)
	}
	ident, err := directory.LookupDID(ctx, did)
	if err != nil {
		return SessionData{}, fmt.Errorf("oauthclient: resolving %s, the account the entryway %s signed in: %w", did, entryway, err)
	}
	pds := strings.TrimRight(strings.TrimSpace(ident.PDSEndpoint()), "/")
	if pds == "" {
		return SessionData{}, fmt.Errorf("oauthclient: %s names no PDS", did)
	}
	authServer, tokenEndpoint, revokeEndpoint, err := DiscoverAuthServer(ctx, client, pds)
	if err != nil {
		return SessionData{}, fmt.Errorf("oauthclient: the PDS %s of %s: %w", pds, did, err)
	}
	if authServer != entryway {
		return SessionData{}, fmt.Errorf("oauthclient: the PDS %s of %s names the authorization server %s, not the entryway %s", pds, did, authServer, entryway)
	}
	if err := checkAudience(data.AccessToken, pds); err != nil {
		return SessionData{}, err
	}
	data.HostURL = pds
	data.SessionID = SessionID(did.String(), pds)
	data.AuthServerURL = authServer
	data.AuthServerTokenEndpoint = tokenEndpoint
	data.AuthServerRevocationEndpoint = revokeEndpoint
	data.DPoPHostNonce = ""
	return data, nil
}

func checkAudience(token, pds string) error {
	audiences := tokenAudiences(token)
	host, err := url.Parse(pds)
	if err != nil || host.Host == "" {
		return fmt.Errorf("oauthclient: the PDS %q is not an absolute URL", pds)
	}
	checked := false
	for _, aud := range audiences {
		name, ok := strings.CutPrefix(aud, "did:web:")
		if !ok {
			continue
		}
		decoded, err := url.PathUnescape(name)
		if err != nil {
			continue
		}
		if strings.EqualFold(decoded, host.Host) {
			return nil
		}
		checked = true
	}
	if checked {
		return fmt.Errorf("oauthclient: the access token is for %s, not the PDS %s", strings.Join(audiences, ", "), pds)
	}
	return nil
}

func tokenAudiences(token string) []string {
	parts := strings.Split(token, ".")
	if len(parts) < 2 {
		return nil
	}
	raw, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return nil
	}
	var claims struct {
		Audience json.RawMessage `json:"aud"`
	}
	if json.Unmarshal(raw, &claims) != nil || len(claims.Audience) == 0 {
		return nil
	}
	var one string
	if json.Unmarshal(claims.Audience, &one) == nil {
		return []string{one}
	}
	var many []string
	if json.Unmarshal(claims.Audience, &many) == nil {
		return many
	}
	return nil
}