188 lines
4.7 KiB
Go
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
|
||
|
}
|