Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
5.2 kB · 151 lines
Go
at local-dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152package oauth
import ( "context" "encoding/json" "fmt" "net/http" "net/url"
atoauth "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax")
// DevStartAuthFlow is a dev-mode alternative to ClientApp.StartAuthFlow that// relaxes the HTTPS and port restrictions on OAuth URLs. The upstream indigo// library's Resolver requires all auth server URLs to be https:// with no// explicit port, which prevents local development against http://localhost:PORT.//// This reimplements the StartAuthFlow logic: identity lookup -> fetch protected// resource metadata -> fetch auth server metadata -> send PAR request, but// fetches and parses the well-known documents directly without URL validation.func (o *OAuth) DevStartAuthFlow(ctx context.Context, identifier string) (string, error) { app := o.ClientApp
atid, err := syntax.ParseAtIdentifier(identifier) if err != nil { return "", fmt.Errorf("not a valid account identifier (%s): %w", identifier, err) }
ident, err := app.Dir.Lookup(ctx, *atid) if err != nil { return "", fmt.Errorf("failed to resolve username (%s): %w", identifier, err) }
accountDID := ident.DID host := ident.PDSEndpoint() if host == "" { return "", fmt.Errorf("identity does not link to an atproto host (PDS)") }
logger := o.Logger.With("did", ident.DID, "handle", ident.Handle, "host", host) logger.Debug("dev: resolving auth server metadata (relaxed URL validation)")
// Fetch protected resource metadata to find the auth server URL. authserverURL, err := devResolveAuthServerURL(ctx, app.Resolver.Client, host) if err != nil { return "", fmt.Errorf("dev: resolving auth server: %w", err) }
// Fetch auth server metadata without HTTPS validation. authserverMeta, err := devResolveAuthServerMetadata(ctx, app.Resolver.Client, authserverURL) if err != nil { return "", fmt.Errorf("dev: fetching auth server metadata: %w", err) }
info, err := app.SendAuthRequest(ctx, authserverMeta, app.Config.Scopes, identifier) if err != nil { return "", fmt.Errorf("auth request failed: %w", err) }
info.AccountDID = &accountDID app.Store.SaveAuthRequestInfo(ctx, *info)
params := url.Values{} params.Set("client_id", app.Config.ClientID) params.Set("request_uri", info.RequestURI) redirectURL := fmt.Sprintf("%s?%s", authserverMeta.AuthorizationEndpoint, params.Encode()) return redirectURL, nil}
// devResolveAuthServerURL fetches /.well-known/oauth-protected-resource from// the PDS host without requiring HTTPS or rejecting port numbers.func devResolveAuthServerURL(ctx context.Context, client *http.Client, hostURL string) (string, error) { u, err := url.Parse(hostURL) if err != nil { return "", err }
docURL := fmt.Sprintf("%s://%s/.well-known/oauth-protected-resource", u.Scheme, u.Host) req, err := http.NewRequestWithContext(ctx, "GET", docURL, nil) if err != nil { return "", err }
resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fetching protected resource document: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("HTTP error fetching protected resource document: %d", resp.StatusCode) }
var body atoauth.ProtectedResourceMetadata if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return "", fmt.Errorf("invalid protected resource document: %w", err) } if len(body.AuthorizationServers) < 1 { return "", fmt.Errorf("no auth server URL in protected resource document") } return body.AuthorizationServers[0], nil}
// devResolveAuthServerMetadata fetches /.well-known/oauth-authorization-server// from the auth server without requiring HTTPS or rejecting port numbers.// It also skips the Validate() call which enforces HTTPS on the issuer.func devResolveAuthServerMetadata(ctx context.Context, client *http.Client, serverURL string) (*atoauth.AuthServerMetadata, error) { u, err := url.Parse(serverURL) if err != nil { return nil, err }
docURL := fmt.Sprintf("%s://%s/.well-known/oauth-authorization-server", u.Scheme, u.Host) req, err := http.NewRequestWithContext(ctx, "GET", docURL, nil) if err != nil { return nil, err }
resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("fetching auth server metadata: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP error fetching auth server metadata: %d", resp.StatusCode) }
var body atoauth.AuthServerMetadata if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return nil, fmt.Errorf("invalid auth server metadata document: %w", err) }
// Skip body.Validate(serverURL) - it requires https:// and no port. // Do minimal sanity checks instead. if body.Issuer == "" { return nil, fmt.Errorf("empty issuer in auth server metadata") } if body.PushedAuthorizationRequestEndpoint == "" { return nil, fmt.Errorf("missing pushed_authorization_request_endpoint") } if body.AuthorizationEndpoint == "" { return nil, fmt.Errorf("missing authorization_endpoint") } if body.TokenEndpoint == "" { return nil, fmt.Errorf("missing token_endpoint") }
return &body, nil}