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>
This commit is contained in:
134
internal/handler/verify.go
Normal file
134
internal/handler/verify.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user