- 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>
219 lines
4.7 KiB
Go
219 lines
4.7 KiB
Go
package oauth2
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// AuthCode represents an OAuth2 authorization code
|
|
type AuthCode struct {
|
|
Code string
|
|
ClientID string
|
|
RedirectURI string
|
|
Pubkey string // Nostr public key (hex)
|
|
State string
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// Challenge represents a Nostr authentication challenge
|
|
type Challenge struct {
|
|
Nonce string
|
|
ClientID string
|
|
State string
|
|
RedirectURI string
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// AccessToken represents an issued access token
|
|
type AccessToken struct {
|
|
Token string
|
|
Pubkey string
|
|
ClientID string
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// Store interface for OAuth2 data persistence
|
|
type Store interface {
|
|
// Challenge operations
|
|
CreateChallenge(clientID, state, redirectURI string, ttl time.Duration) (*Challenge, error)
|
|
GetChallenge(nonce string) (*Challenge, error)
|
|
DeleteChallenge(nonce string) error
|
|
|
|
// Auth code operations
|
|
CreateAuthCode(clientID, redirectURI, pubkey, state string) (*AuthCode, error)
|
|
GetAuthCode(code string) (*AuthCode, error)
|
|
DeleteAuthCode(code string) error
|
|
|
|
// Access token operations
|
|
CreateAccessToken(pubkey, clientID string) (*AccessToken, error)
|
|
GetAccessToken(token string) (*AccessToken, error)
|
|
}
|
|
|
|
// MemoryStore is an in-memory implementation of Store
|
|
type MemoryStore struct {
|
|
challenges map[string]*Challenge
|
|
authCodes map[string]*AuthCode
|
|
accessTokens map[string]*AccessToken
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewMemoryStore() *MemoryStore {
|
|
s := &MemoryStore{
|
|
challenges: make(map[string]*Challenge),
|
|
authCodes: make(map[string]*AuthCode),
|
|
accessTokens: make(map[string]*AccessToken),
|
|
}
|
|
go s.cleanup()
|
|
return s
|
|
}
|
|
|
|
func (s *MemoryStore) cleanup() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
for range ticker.C {
|
|
s.mu.Lock()
|
|
now := time.Now()
|
|
for k, v := range s.challenges {
|
|
if now.After(v.ExpiresAt) {
|
|
delete(s.challenges, k)
|
|
}
|
|
}
|
|
for k, v := range s.authCodes {
|
|
if now.After(v.ExpiresAt) {
|
|
delete(s.authCodes, k)
|
|
}
|
|
}
|
|
for k, v := range s.accessTokens {
|
|
if now.After(v.ExpiresAt) {
|
|
delete(s.accessTokens, k)
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
func generateToken(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func (s *MemoryStore) CreateChallenge(clientID, state, redirectURI string, ttl time.Duration) (*Challenge, error) {
|
|
nonce, err := generateToken(32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
challenge := &Challenge{
|
|
Nonce: nonce,
|
|
ClientID: clientID,
|
|
State: state,
|
|
RedirectURI: redirectURI,
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(ttl),
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.challenges[nonce] = challenge
|
|
s.mu.Unlock()
|
|
|
|
return challenge, nil
|
|
}
|
|
|
|
func (s *MemoryStore) GetChallenge(nonce string) (*Challenge, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
challenge, ok := s.challenges[nonce]
|
|
if !ok || time.Now().After(challenge.ExpiresAt) {
|
|
return nil, nil
|
|
}
|
|
return challenge, nil
|
|
}
|
|
|
|
func (s *MemoryStore) DeleteChallenge(nonce string) error {
|
|
s.mu.Lock()
|
|
delete(s.challenges, nonce)
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryStore) CreateAuthCode(clientID, redirectURI, pubkey, state string) (*AuthCode, error) {
|
|
code, err := generateToken(32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authCode := &AuthCode{
|
|
Code: code,
|
|
ClientID: clientID,
|
|
RedirectURI: redirectURI,
|
|
Pubkey: pubkey,
|
|
State: state,
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(10 * time.Minute),
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.authCodes[code] = authCode
|
|
s.mu.Unlock()
|
|
|
|
return authCode, nil
|
|
}
|
|
|
|
func (s *MemoryStore) GetAuthCode(code string) (*AuthCode, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
authCode, ok := s.authCodes[code]
|
|
if !ok || time.Now().After(authCode.ExpiresAt) {
|
|
return nil, nil
|
|
}
|
|
return authCode, nil
|
|
}
|
|
|
|
func (s *MemoryStore) DeleteAuthCode(code string) error {
|
|
s.mu.Lock()
|
|
delete(s.authCodes, code)
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryStore) CreateAccessToken(pubkey, clientID string) (*AccessToken, error) {
|
|
token, err := generateToken(32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
accessToken := &AccessToken{
|
|
Token: token,
|
|
Pubkey: pubkey,
|
|
ClientID: clientID,
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.accessTokens[token] = accessToken
|
|
s.mu.Unlock()
|
|
|
|
return accessToken, nil
|
|
}
|
|
|
|
func (s *MemoryStore) GetAccessToken(token string) (*AccessToken, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
accessToken, ok := s.accessTokens[token]
|
|
if !ok || time.Now().After(accessToken.ExpiresAt) {
|
|
return nil, nil
|
|
}
|
|
return accessToken, nil
|
|
}
|