Files
gitea-nostr-auth/internal/handler/verify.go
mleku 896a7599a0 Release v0.0.1 - Initial OAuth2 server implementation
- Add Nostr OAuth2 server with NIP-98 authentication support
- Implement OAuth2 authorization and token endpoints
- Add .well-known/openid-configuration discovery endpoint
- Include Dockerfile for containerized deployment
- Add Claude Code release command for version management
- Create example configuration file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 09:37:26 +01:00

135 lines
3.3 KiB
Go

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(),
})
}