package handler import ( "encoding/json" "log" "net/http" "net/url" "time" "github.com/nbd-wtf/go-nostr" ) type VerifyRequest struct { ID string `json:"id"` PubKey string `json:"pubkey"` CreatedAt int64 `json:"created_at"` Kind int `json:"kind"` Tags [][]string `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` } type VerifyResponse struct { Success bool `json:"success"` RedirectURL string `json:"redirect_url,omitempty"` Error string `json:"error,omitempty"` } func (h *Handler) Verify(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var req VerifyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { json.NewEncoder(w).Encode(VerifyResponse{Error: "invalid request body"}) return } // Convert to go-nostr Event for verification event := nostr.Event{ ID: req.ID, PubKey: req.PubKey, CreatedAt: nostr.Timestamp(req.CreatedAt), Kind: req.Kind, Tags: make(nostr.Tags, len(req.Tags)), Content: req.Content, Sig: req.Sig, } for i, tag := range req.Tags { event.Tags[i] = tag } // Verify event kind if event.Kind != 27235 { json.NewEncoder(w).Encode(VerifyResponse{Error: "invalid event kind, expected 27235"}) return } // Verify timestamp is within window eventTime := time.Unix(int64(event.CreatedAt), 0) if time.Since(eventTime) > h.cfg.Nostr.ChallengeTTL { json.NewEncoder(w).Encode(VerifyResponse{Error: "event timestamp too old"}) return } if eventTime.After(time.Now().Add(time.Minute)) { json.NewEncoder(w).Encode(VerifyResponse{Error: "event timestamp in future"}) return } // Verify signature ok, err := event.CheckSignature() if err != nil || !ok { log.Printf("signature verification failed: %v", err) json.NewEncoder(w).Encode(VerifyResponse{Error: "invalid signature"}) return } // Extract challenge from tags var challengeNonce string for _, tag := range event.Tags { if len(tag) >= 2 && tag[0] == "challenge" { challengeNonce = tag[1] break } } if challengeNonce == "" { json.NewEncoder(w).Encode(VerifyResponse{Error: "missing challenge tag"}) return } // Look up challenge challenge, err := h.store.GetChallenge(challengeNonce) if err != nil || challenge == nil { json.NewEncoder(w).Encode(VerifyResponse{Error: "invalid or expired challenge"}) return } // Delete challenge (single use) h.store.DeleteChallenge(challengeNonce) // Create authorization code authCode, err := h.store.CreateAuthCode( challenge.ClientID, challenge.RedirectURI, event.PubKey, challenge.State, ) if err != nil { log.Printf("failed to create auth code: %v", err) json.NewEncoder(w).Encode(VerifyResponse{Error: "internal error"}) return } // Build redirect URL redirectURL, err := url.Parse(challenge.RedirectURI) if err != nil { log.Printf("invalid redirect URI: %v", err) json.NewEncoder(w).Encode(VerifyResponse{Error: "internal error"}) return } q := redirectURL.Query() q.Set("code", authCode.Code) if challenge.State != "" { q.Set("state", challenge.State) } redirectURL.RawQuery = q.Encode() json.NewEncoder(w).Encode(VerifyResponse{ Success: true, RedirectURL: redirectURL.String(), }) }