// internal/auth/auth.go package auth import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "strings" "time" "github.com/nbd-wtf/go-nostr" "git.sovbit.dev/Enki/nostr-poster/internal/db" "go.uber.org/zap" ) var ( // ErrInvalidSignature is returned when a signature is invalid ErrInvalidSignature = errors.New("invalid signature") // ErrTokenExpired is returned when a token has expired ErrTokenExpired = errors.New("token expired") // ErrInvalidToken is returned when a token is invalid ErrInvalidToken = errors.New("invalid token") ) // Token represents an authentication token type Token struct { Pubkey string `json:"pubkey"` IssuedAt time.Time `json:"issued_at"` ExpiresAt time.Time `json:"expires_at"` Nonce string `json:"nonce"` } // Service provides authentication functionality type Service struct { db *db.DB logger *zap.Logger secretKey []byte tokenDuration time.Duration } // NewService creates a new authentication service func NewService(db *db.DB, logger *zap.Logger, secretKey string, tokenDuration time.Duration) *Service { // If no secret key is provided, generate a secure random one decodedKey := []byte(secretKey) if secretKey == "" { // Generate a secure random key key := make([]byte, 32) if _, err := rand.Read(key); err != nil { logger.Fatal("Failed to generate random key for auth service", zap.Error(err)) } decodedKey = key } return &Service{ db: db, logger: logger, secretKey: decodedKey, tokenDuration: tokenDuration, } } // Login handles user login with a Nostr signature func (s *Service) Login(pubkey, signature, eventJSON string) (string, error) { // Parse the event var event nostr.Event if err := json.Unmarshal([]byte(eventJSON), &event); err != nil { return "", fmt.Errorf("failed to parse event: %w", err) } // Verify the event if event.PubKey != pubkey { return "", errors.New("pubkey mismatch in event") } // Verify the signature ok, err := event.CheckSignature() if err != nil { return "", fmt.Errorf("failed to check signature: %w", err) } if !ok { return "", ErrInvalidSignature } // Check if the event was created recently now := time.Now() eventTime := time.Unix(int64(event.CreatedAt), 0) if now.Sub(eventTime) > 5*time.Minute || eventTime.After(now.Add(5*time.Minute)) { return "", errors.New("event timestamp is too far from current time") } // Generate a token token, err := s.createToken(pubkey) if err != nil { return "", fmt.Errorf("failed to create token: %w", err) } return token, nil } // VerifyToken validates an authentication token func (s *Service) VerifyToken(tokenStr string) (string, error) { // Remove the "Bearer " prefix if present tokenStr = strings.TrimPrefix(tokenStr, "Bearer ") // Decode the token tokenData, err := base64.StdEncoding.DecodeString(tokenStr) if err != nil { return "", ErrInvalidToken } // Parse the token var token Token if err := json.Unmarshal(tokenData, &token); err != nil { return "", ErrInvalidToken } // Check if the token has expired if time.Now().After(token.ExpiresAt) { return "", ErrTokenExpired } return token.Pubkey, nil } // CreateNIP07Challenge creates a challenge for NIP-07 authentication func (s *Service) CreateNIP07Challenge(pubkey string) (string, error) { // Generate a nonce nonce := make([]byte, 16) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("failed to generate nonce: %w", err) } nonceStr := hex.EncodeToString(nonce) // Create the challenge event event := nostr.Event{ PubKey: pubkey, CreatedAt: nostr.Timestamp(time.Now().Unix()), Kind: 22242, // Ephemeral event for authentication Tags: []nostr.Tag{{"challenge", nonceStr}}, Content: "Please sign this event to authenticate with Nostr Poster", } // Set the ID event.ID = event.GetID() // Convert to JSON eventJSON, err := json.Marshal(event) if err != nil { return "", fmt.Errorf("failed to serialize event: %w", err) } return string(eventJSON), nil } // createToken creates an authentication token func (s *Service) createToken(pubkey string) (string, error) { // Generate a nonce nonce := make([]byte, 8) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("failed to generate nonce: %w", err) } nonceStr := hex.EncodeToString(nonce) // Create the token now := time.Now() token := Token{ Pubkey: pubkey, IssuedAt: now, ExpiresAt: now.Add(s.tokenDuration), Nonce: nonceStr, } // Serialize the token tokenData, err := json.Marshal(token) if err != nil { return "", fmt.Errorf("failed to serialize token: %w", err) } // Encode the token tokenStr := base64.StdEncoding.EncodeToString(tokenData) return tokenStr, nil }