Files
at-container-registry/cmd/oauth-helper/main.go
T

136 lines
3.8 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"atcr.io/pkg/atproto"
"atcr.io/pkg/auth/oauth"
indigo_oauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
)
func main() {
handle := flag.String("handle", "", "Your Bluesky handle (e.g., yourname.bsky.social)")
holdURL := flag.String("hold-url", "http://localhost:8080", "Hold service URL")
repo := flag.String("repo", "", "Repository DID (e.g., did:web:172.28.0.3:8080)")
collection := flag.String("collection", "io.atcr.hold.crew", "Collection to delete from")
rkey := flag.String("rkey", "", "Record key to delete")
flag.Parse()
if *handle == "" {
fmt.Println("Usage: oauth-helper --handle yourname.bsky.social [options]")
fmt.Println("\nOptions:")
flag.PrintDefaults()
os.Exit(1)
}
ctx := context.Background()
fmt.Printf("🔐 Starting OAuth flow for %s...\n\n", *handle)
// Create a simple HTTP server for the callback
mux := http.NewServeMux()
server := &http.Server{
Addr: ":8765",
Handler: mux,
}
// Channel to receive the result
resultChan := make(chan *oauth.InteractiveResult, 1)
errorChan := make(chan error, 1)
// Register callback handler
registerCallback := func(handler http.HandlerFunc) error {
mux.HandleFunc("/auth/oauth/callback", handler)
return nil
}
// Display auth URL (will open browser)
displayAuthURL := func(authURL string) error {
fmt.Printf("🌐 Opening browser for authorization...\n")
fmt.Printf(" URL: %s\n\n", authURL)
fmt.Printf(" If the browser doesn't open, visit the URL above.\n\n")
return oauth.OpenBrowser(authURL)
}
// Start server in background
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
errorChan <- fmt.Errorf("server error: %w", err)
}
}()
// Give server time to start
time.Sleep(100 * time.Millisecond)
// Run interactive OAuth flow
go func() {
result, err := oauth.InteractiveFlowWithCallback(
ctx,
"http://localhost:8765",
*handle,
nil, // Use default scopes
registerCallback,
displayAuthURL,
)
if err != nil {
errorChan <- err
return
}
resultChan <- result
}()
// Wait for result
var result *oauth.InteractiveResult
select {
case result = <-resultChan:
fmt.Printf("✅ OAuth successful!\n\n")
case err := <-errorChan:
log.Fatalf("❌ OAuth failed: %v\n", err)
case <-time.After(5 * time.Minute):
log.Fatalf("❌ OAuth timed out\n")
}
// Shutdown server
server.Shutdown(ctx)
// Print session information
fmt.Printf("DID: %s\n", result.SessionData.AccountDID)
fmt.Printf("Access Token: %s\n", result.SessionData.AccessToken)
fmt.Printf("DPoP Key: %s\n\n", result.SessionData.DPoPPrivateKeyMultibase)
// Generate DPoP proof for deleteRecord endpoint if all params provided
if *repo != "" && *rkey != "" {
deleteURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&rkey=%s",
*holdURL, atproto.RepoDeleteRecord, *repo, *collection, *rkey)
dpopProof, err := generateDPoPProof(result.Session, "POST", deleteURL)
if err != nil {
log.Fatalf("❌ Failed to generate DPoP proof: %v\n", err)
}
fmt.Printf("📋 Ready-to-use curl command:\n\n")
fmt.Printf("curl -X POST \\\n")
fmt.Printf(" -H \"Authorization: DPoP %s\" \\\n", result.SessionData.AccessToken)
fmt.Printf(" -H \"DPoP: %s\" \\\n", dpopProof)
fmt.Printf(" \"%s\"\n", deleteURL)
} else {
fmt.Printf("💡 To generate a curl command for deleteRecord, provide:\n")
fmt.Printf(" --repo <did>\n")
fmt.Printf(" --collection <collection>\n")
fmt.Printf(" --rkey <rkey>\n")
}
}
// generateDPoPProof generates a DPoP proof JWT for a specific request
func generateDPoPProof(session *indigo_oauth.ClientSession, method, reqURL string) (string, error) {
// Use the session's NewHostDPoP method to generate the proof
return session.NewHostDPoP(method, reqURL)
}