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 }