package auth import ( "context" "errors" "log/slog" "sync/atomic" "time" ) var ErrVerifierUnavailable = errors.New("verifier unavailable") // Holder keeps an OIDC verifier available, retrying in the background when Authentik // is not ready at ultid startup (common in local Docker compose). type Holder struct { v atomic.Pointer[Verifier] issuerURL string clientID string discoveryHost string } func NewHolder(initial *Verifier) *Holder { h := &Holder{} if initial != nil { h.v.Store(initial) } return h } func NewHolderPending(issuerURL, clientID, discoveryHost string) *Holder { return &Holder{ issuerURL: issuerURL, clientID: clientID, discoveryHost: discoveryHost, } } func (h *Holder) Ready() bool { return h != nil && h.v.Load() != nil } func (h *Holder) Get() *Verifier { if h == nil { return nil } return h.v.Load() } func (h *Holder) Verify(ctx context.Context, rawToken string) (*Claims, error) { v := h.Get() if v == nil { return nil, ErrVerifierUnavailable } return v.Verify(ctx, rawToken) } // StartBackgroundRetry keeps trying until the verifier is ready or ctx is cancelled. func (h *Holder) StartBackgroundRetry(ctx context.Context, interval time.Duration) { if h == nil || h.Ready() || h.issuerURL == "" || h.clientID == "" { return } if interval <= 0 { interval = 5 * time.Second } go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { v, err := NewVerifier(ctx, h.issuerURL, h.clientID, h.discoveryHost) if err == nil { h.v.Store(v) slog.Info("OIDC verifier ready (background retry)") return } slog.Warn("OIDC verifier background retry", "error", err) select { case <-ctx.Done(): return case <-ticker.C: } } }() }