188 lines
4.7 KiB
Go

// 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
}