From 3cc66e3bc0fe59f714e37c008610fedb0ba7bbf7 Mon Sep 17 00:00:00 2001 From: hectorsector Date: Wed, 25 Mar 2026 21:37:31 -0400 Subject: [PATCH] token exchange --- cmd/server/main.go | 3 +- internal/auth/oauth.go | 103 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 internal/auth/oauth.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 09d4899..b8072bc 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,6 +10,8 @@ import ( ) func main() { + godotenv.Load() + r := chi.NewRouter() r.Use(middleware.Logger) @@ -41,7 +43,6 @@ func main() { w.Write([]byte(`{"status": "ok"}`)) }) - godotenv.Load() port := os.Getenv("PORT") if port == "" { diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go new file mode 100644 index 0000000..37bb64c --- /dev/null +++ b/internal/auth/oauth.go @@ -0,0 +1,103 @@ +package auth + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + atProtoScope = "atproto transition:generic" + codeChallengeMethod = "S256" + responseType = "code" + authContentType = "application/x-www-form-urlencoded" +) + +type PARResponse struct { + RequestURI string `json:"request_uri"` + ExpiresIn int `json:"expires_in"` +} + +func GenerateState() (string, error) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "", fmt.Errorf("failed to generate state: %w", err) + } + + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func GeneratePKCE() (string, string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", "", fmt.Errorf("failed to generate pkce: %w", err) + } + + codeVerifier := base64.RawURLEncoding.EncodeToString(b) + + h := sha256.Sum256([]byte(codeVerifier)) + codeChallenge := base64.RawURLEncoding.EncodeToString(h[:]) + + return codeVerifier, codeChallenge, nil +} + +func SendPAR(ctx context.Context, authMeta AuthServerMeta, clientID, redirectURI, state, codeChallenge string) (string, error) { + params := url.Values{} + params.Set("client_id", clientID) + params.Set("redirect_uri", redirectURI) + params.Set("scope", atProtoScope) + params.Set("state", state) + params.Set("code_challenge", codeChallenge) + params.Set("code_challenge_method", codeChallengeMethod) + params.Set("response_type", responseType) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, authMeta.PushedAuthorizationRequestEndpoint, strings.NewReader(params.Encode())) + if err != nil { + return "", fmt.Errorf("failed to build par request: %w", err) + } + req.Header.Set("Content-Type", authContentType) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to send par request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("got unexpected return code from par request: %d", resp.StatusCode) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response from par request: %w", err) + } + + var parr PARResponse + if err := json.Unmarshal(b, &parr); err != nil { + return "", fmt.Errorf("failed to parse par response: %w", err) + } + + if parr.RequestURI == "" { + return "", fmt.Errorf("requestURI came back empty") + } + + return parr.RequestURI, nil +} + +func BuildAuthURL(authMeta AuthServerMeta, clientID string, requestURI string) string { + params := url.Values{} + params.Set("client_id", clientID) + params.Set("request_uri", requestURI) + + return authMeta.AuthorizationEndpoint + "?" + params.Encode() + +} -- 2.51.2