feat(auth): implement OAuth 2.0 authorization code flow and dynamic client registration

- Add OAuth 2.0 support with authorization code flow and dynamic client registration.
- Introduce new handlers for OAuth metadata, client registration, authorization, and token issuance.
- Enhance authentication middleware to support OAuth client credentials.
- Create in-memory stores for authorization codes and tokens.
- Update configuration to include OAuth client details.
- Ensure validation checks for OAuth clients in the configuration.
This commit is contained in:
2026-03-26 21:17:55 +02:00
parent ed05d390b7
commit 56c84df342
19 changed files with 970 additions and 40 deletions

View File

@@ -0,0 +1,76 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"sync"
"time"
)
const authCodeTTL = 10 * time.Minute
// AuthCode holds a pending authorization code and its associated PKCE data.
type AuthCode struct {
ClientID string
RedirectURI string
Scope string
CodeChallenge string
CodeChallengeMethod string
KeyID string
ExpiresAt time.Time
}
// AuthCodeStore issues single-use authorization codes for the OAuth 2.0
// Authorization Code flow.
type AuthCodeStore struct {
mu sync.Mutex
codes map[string]AuthCode
}
func NewAuthCodeStore() *AuthCodeStore {
s := &AuthCodeStore{codes: make(map[string]AuthCode)}
go s.sweepLoop()
return s
}
// Issue stores the entry and returns the raw authorization code.
func (s *AuthCodeStore) Issue(entry AuthCode) (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
raw := hex.EncodeToString(b)
entry.ExpiresAt = time.Now().Add(authCodeTTL)
s.mu.Lock()
s.codes[raw] = entry
s.mu.Unlock()
return raw, nil
}
// Consume validates and removes the code, returning the associated entry.
func (s *AuthCodeStore) Consume(code string) (AuthCode, bool) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.codes[code]
if !ok || time.Now().After(entry.ExpiresAt) {
delete(s.codes, code)
return AuthCode{}, false
}
delete(s.codes, code)
return entry, true
}
func (s *AuthCodeStore) sweepLoop() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
s.mu.Lock()
for code, entry := range s.codes {
if now.After(entry.ExpiresAt) {
delete(s.codes, code)
}
}
s.mu.Unlock()
}
}