package oidcstub
import (
"crypto/rand"
"encoding/base64"
"sync"
"time"
)
// codeEntry holds the data stored with an authorization code.
type codeEntry struct {
Sub string
CodeChallenge string
RedirectURI string
ExpiresAt time.Time
}
// codeStore is a thread-safe in-memory store for single-use authorization codes.
// now is injected for deterministic testing; nil defaults to time.Now.
type codeStore struct {
mu sync.Mutex
entries map[string]codeEntry
now func() time.Time
}
// newCodeStore constructs a codeStore. now may be nil, in which case time.Now
// is used.
func newCodeStore(now func() time.Time) *codeStore {
if now == nil {
now = time.Now
}
return &codeStore{
entries: make(map[string]codeEntry),
now: now,
}
}
// Issue generates an opaque base64url 32-byte authorization code, stores it
// with the supplied sub, code_challenge, redirect_uri, and TTL, and returns
// the code string. The code is URL-safe (base64.RawURLEncoding — no +, /, =).
func (s *codeStore) Issue(sub, challenge, redirect string, ttl time.Duration) string {
var buf [32]byte
if _, err := rand.Read(buf[:]); err != nil {
panic("oidcstub: codeStore.Issue: crypto/rand.Read: " + err.Error())
}
code := base64.RawURLEncoding.EncodeToString(buf[:])
s.mu.Lock()
defer s.mu.Unlock()
s.entries[code] = codeEntry{
Sub: sub,
CodeChallenge: challenge,
RedirectURI: redirect,
ExpiresAt: s.now().Add(ttl),
}
return code
}
// Consume retrieves and deletes the entry for code. Returns (entry, true) on
// first call for a valid, unexpired code; (zero, false) on miss, expiry, or
// any subsequent call (IV2, IV3).
func (s *codeStore) Consume(code string) (codeEntry, bool) {
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.entries[code]
if !ok {
return codeEntry{}, false
}
// Always delete — even expired entries must not be reusable.
delete(s.entries, code)
if s.now().After(entry.ExpiresAt) {
return codeEntry{}, false
}
return entry, true
}