package auth import ( "context" "fmt" "net" "net/http" "strings" "sync" "time" "github.com/patriceckhart/zot/internal/assets" ) // CallbackResult is what an OAuth callback server returns once the // browser hits the redirect URI. type CallbackResult struct { Code string State string Err error RawPath string } // CallbackServer is a single-shot OAuth callback listener bound to the // fixed port that the provider has whitelisted for its client. type CallbackServer struct { l net.Listener srv *http.Server provider OAuthProvider state string result chan CallbackResult once sync.Once } // NewCallbackServer starts a listener on p.RedirectPort/p.RedirectPath. // Returns an error if that port is already in use (e.g. another login // flow already running, or the official CLI is running concurrently). func NewCallbackServer(p OAuthProvider, expectedState string) (*CallbackServer, error) { addr := fmt.Sprintf("%s:%d", p.RedirectHost, p.RedirectPort) l, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("bind %s: %w (is another zot/claude-code/codex login already running?)", addr, err) } cs := &CallbackServer{ l: l, provider: p, state: expectedState, result: make(chan CallbackResult, 1), } mux := http.NewServeMux() mux.HandleFunc(p.RedirectPath, cs.handle) mux.HandleFunc("/logo.png", serveLogo) cs.srv = &http.Server{ Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, } go func() { _ = cs.srv.Serve(l) }() return cs, nil } // URL returns the full redirect URI this server is listening on. func (cs *CallbackServer) URL() string { return cs.provider.RedirectURI() } // Result blocks until the callback is received or ctx is cancelled. func (cs *CallbackServer) Result(ctx context.Context) (CallbackResult, error) { select { case r := <-cs.result: return r, nil case <-ctx.Done(): return CallbackResult{}, ctx.Err() } } // Shutdown stops the server. Safe to call more than once. func (cs *CallbackServer) Shutdown() { cs.once.Do(func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) _ = cs.srv.Shutdown(ctx) cancel() }) } func (cs *CallbackServer) handle(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() w.Header().Set("content-type", "text/html; charset=utf-8") if errParam := q.Get("error"); errParam != "" { msg := errParam if d := q.Get("error_description"); d != "" { msg += ": " + d } w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(oauthErrorHTML(msg))) cs.deliver(CallbackResult{Err: fmt.Errorf(msg), RawPath: r.URL.RequestURI()}) return } code := q.Get("code") state := q.Get("state") if code == "" { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(oauthErrorHTML("missing authorization code"))) cs.deliver(CallbackResult{Err: fmt.Errorf("missing code"), RawPath: r.URL.RequestURI()}) return } if cs.state != "" && state != cs.state { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(oauthErrorHTML("state mismatch"))) cs.deliver(CallbackResult{Err: fmt.Errorf("state mismatch"), RawPath: r.URL.RequestURI()}) return } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(oauthSuccessHTML(cs.provider.Name))) cs.deliver(CallbackResult{Code: code, State: state, RawPath: r.URL.RequestURI()}) } func (cs *CallbackServer) deliver(r CallbackResult) { select { case cs.result <- r: default: } } // ---- static HTML for the callback tab ---- // All zot-served pages share a single style: white background, black // text, the cyan pixel-art `z` logo at the top, thin black rules, // monospace type. const monoStyle = `` // logoTag is the element used at the top of every zot-served // page. The image bytes are served from /logo.png by the same server. const logoTag = `` // serveLogo writes the embedded PNG with appropriate caching headers. // Shared between the oauth callback server and the api-key login server. func serveLogo(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "image/png") w.Header().Set("cache-control", "public, max-age=86400") _, _ = w.Write(assets.LogoPNG) } func oauthSuccessHTML(provider string) string { p := strings.ToLower(provider) return `zot - logged in` + monoStyle + ` ` + logoTag + `

logged in to ` + p + `


zot received the callback. you can close this tab.

` } func oauthErrorHTML(msg string) string { return `zot · error` + monoStyle + ` ` + logoTag + `

login failed


` + htmlEscape(msg) + `

go back to zot and try again.

` } func htmlEscape(s string) string { r := strings.NewReplacer("&", "&", "<", "<", ">", ">", "\"", """) return r.Replace(s) }