store.go raw

   1  package oauth2
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/hex"
   6  	"sync"
   7  	"time"
   8  )
   9  
  10  // AuthCode represents an OAuth2 authorization code
  11  type AuthCode struct {
  12  	Code        string
  13  	ClientID    string
  14  	RedirectURI string
  15  	Pubkey      string // Nostr public key (hex)
  16  	State       string
  17  	CreatedAt   time.Time
  18  	ExpiresAt   time.Time
  19  }
  20  
  21  // Challenge represents a Nostr authentication challenge
  22  type Challenge struct {
  23  	Nonce     string
  24  	ClientID  string
  25  	State     string
  26  	RedirectURI string
  27  	CreatedAt time.Time
  28  	ExpiresAt time.Time
  29  }
  30  
  31  // AccessToken represents an issued access token
  32  type AccessToken struct {
  33  	Token     string
  34  	Pubkey    string
  35  	ClientID  string
  36  	CreatedAt time.Time
  37  	ExpiresAt time.Time
  38  }
  39  
  40  // Store interface for OAuth2 data persistence
  41  type Store interface {
  42  	// Challenge operations
  43  	CreateChallenge(clientID, state, redirectURI string, ttl time.Duration) (*Challenge, error)
  44  	GetChallenge(nonce string) (*Challenge, error)
  45  	DeleteChallenge(nonce string) error
  46  
  47  	// Auth code operations
  48  	CreateAuthCode(clientID, redirectURI, pubkey, state string) (*AuthCode, error)
  49  	GetAuthCode(code string) (*AuthCode, error)
  50  	DeleteAuthCode(code string) error
  51  
  52  	// Access token operations
  53  	CreateAccessToken(pubkey, clientID string) (*AccessToken, error)
  54  	GetAccessToken(token string) (*AccessToken, error)
  55  }
  56  
  57  // MemoryStore is an in-memory implementation of Store
  58  type MemoryStore struct {
  59  	challenges   map[string]*Challenge
  60  	authCodes    map[string]*AuthCode
  61  	accessTokens map[string]*AccessToken
  62  	mu           sync.RWMutex
  63  }
  64  
  65  func NewMemoryStore() *MemoryStore {
  66  	s := &MemoryStore{
  67  		challenges:   make(map[string]*Challenge),
  68  		authCodes:    make(map[string]*AuthCode),
  69  		accessTokens: make(map[string]*AccessToken),
  70  	}
  71  	go s.cleanup()
  72  	return s
  73  }
  74  
  75  func (s *MemoryStore) cleanup() {
  76  	ticker := time.NewTicker(time.Minute)
  77  	for range ticker.C {
  78  		s.mu.Lock()
  79  		now := time.Now()
  80  		for k, v := range s.challenges {
  81  			if now.After(v.ExpiresAt) {
  82  				delete(s.challenges, k)
  83  			}
  84  		}
  85  		for k, v := range s.authCodes {
  86  			if now.After(v.ExpiresAt) {
  87  				delete(s.authCodes, k)
  88  			}
  89  		}
  90  		for k, v := range s.accessTokens {
  91  			if now.After(v.ExpiresAt) {
  92  				delete(s.accessTokens, k)
  93  			}
  94  		}
  95  		s.mu.Unlock()
  96  	}
  97  }
  98  
  99  func generateToken(length int) (string, error) {
 100  	bytes := make([]byte, length)
 101  	if _, err := rand.Read(bytes); err != nil {
 102  		return "", err
 103  	}
 104  	return hex.EncodeToString(bytes), nil
 105  }
 106  
 107  func (s *MemoryStore) CreateChallenge(clientID, state, redirectURI string, ttl time.Duration) (*Challenge, error) {
 108  	nonce, err := generateToken(32)
 109  	if err != nil {
 110  		return nil, err
 111  	}
 112  
 113  	challenge := &Challenge{
 114  		Nonce:       nonce,
 115  		ClientID:    clientID,
 116  		State:       state,
 117  		RedirectURI: redirectURI,
 118  		CreatedAt:   time.Now(),
 119  		ExpiresAt:   time.Now().Add(ttl),
 120  	}
 121  
 122  	s.mu.Lock()
 123  	s.challenges[nonce] = challenge
 124  	s.mu.Unlock()
 125  
 126  	return challenge, nil
 127  }
 128  
 129  func (s *MemoryStore) GetChallenge(nonce string) (*Challenge, error) {
 130  	s.mu.RLock()
 131  	defer s.mu.RUnlock()
 132  
 133  	challenge, ok := s.challenges[nonce]
 134  	if !ok || time.Now().After(challenge.ExpiresAt) {
 135  		return nil, nil
 136  	}
 137  	return challenge, nil
 138  }
 139  
 140  func (s *MemoryStore) DeleteChallenge(nonce string) error {
 141  	s.mu.Lock()
 142  	delete(s.challenges, nonce)
 143  	s.mu.Unlock()
 144  	return nil
 145  }
 146  
 147  func (s *MemoryStore) CreateAuthCode(clientID, redirectURI, pubkey, state string) (*AuthCode, error) {
 148  	code, err := generateToken(32)
 149  	if err != nil {
 150  		return nil, err
 151  	}
 152  
 153  	authCode := &AuthCode{
 154  		Code:        code,
 155  		ClientID:    clientID,
 156  		RedirectURI: redirectURI,
 157  		Pubkey:      pubkey,
 158  		State:       state,
 159  		CreatedAt:   time.Now(),
 160  		ExpiresAt:   time.Now().Add(10 * time.Minute),
 161  	}
 162  
 163  	s.mu.Lock()
 164  	s.authCodes[code] = authCode
 165  	s.mu.Unlock()
 166  
 167  	return authCode, nil
 168  }
 169  
 170  func (s *MemoryStore) GetAuthCode(code string) (*AuthCode, error) {
 171  	s.mu.RLock()
 172  	defer s.mu.RUnlock()
 173  
 174  	authCode, ok := s.authCodes[code]
 175  	if !ok || time.Now().After(authCode.ExpiresAt) {
 176  		return nil, nil
 177  	}
 178  	return authCode, nil
 179  }
 180  
 181  func (s *MemoryStore) DeleteAuthCode(code string) error {
 182  	s.mu.Lock()
 183  	delete(s.authCodes, code)
 184  	s.mu.Unlock()
 185  	return nil
 186  }
 187  
 188  func (s *MemoryStore) CreateAccessToken(pubkey, clientID string) (*AccessToken, error) {
 189  	token, err := generateToken(32)
 190  	if err != nil {
 191  		return nil, err
 192  	}
 193  
 194  	accessToken := &AccessToken{
 195  		Token:     token,
 196  		Pubkey:    pubkey,
 197  		ClientID:  clientID,
 198  		CreatedAt: time.Now(),
 199  		ExpiresAt: time.Now().Add(24 * time.Hour),
 200  	}
 201  
 202  	s.mu.Lock()
 203  	s.accessTokens[token] = accessToken
 204  	s.mu.Unlock()
 205  
 206  	return accessToken, nil
 207  }
 208  
 209  func (s *MemoryStore) GetAccessToken(token string) (*AccessToken, error) {
 210  	s.mu.RLock()
 211  	defer s.mu.RUnlock()
 212  
 213  	accessToken, ok := s.accessTokens[token]
 214  	if !ok || time.Now().After(accessToken.ExpiresAt) {
 215  		return nil, nil
 216  	}
 217  	return accessToken, nil
 218  }
 219