diff --git a/internal/auth/discover.go b/internal/auth/discover.go new file mode 100644 index 0000000..4ac158e --- /dev/null +++ b/internal/auth/discover.go @@ -0,0 +1,61 @@ +package auth + +import ( + "fmt" + "io" + "net" + "net/http" + "strings" +) + +// ref: https://atproto.com/specs/oauth#server-metadata +type AuthServerMeta struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` // where to redirect the user + TokenEndpoint string `json:"token_endpoint"` // where to exchange the code for tokens + PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint"` // where to POST the PAR request first + ScopesSupported []string `json:"scopes_supported"` // to verify the server supports atproto + DPopSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported"` // to confirm ES256 is support before we send a DPoP proof +} + +type DiscoveredSession struct { + DID string + PDSUrl string + AuthMeta AuthServerMeta +} + +// we expect handle to be clean of leading @ +func DiscoverFromhandle(handle string) (DiscoveredSession, error) { + // todo +} + +func resolveHandleToDID(handle string) (string, error) { + // dns resolution, lookup _atproto.{handle} TXT record + records, err := net.LookupTXT("_atproto." + handle) + if err == nil { + for _, r := range records { + if strings.HasPrefix(r, "did=") { + return strings.TrimPrefix(r, "did="), nil + } + } + } + + // http fallback + resp, err := http.Get("https://" + handle + "/.well-known/atproto-did") + if err != nil { + return "", fmt.Errorf("handle resolution failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + did := strings.TrimSpace(string(body)) + + if !strings.HasPrefix(did, "did:") { + return "", fmt.Errorf("invalid DID returned: %s", did) + } + + return did, nil +}