package authentik import ( "context" "crypto/rand" "encoding/hex" "sync" "time" ) const defaultFlowSessionTTL = 20 * time.Minute type flowSessionEntry struct { executor *FlowExecutor slug string createdAt time.Time } // FlowSessionStore keeps in-memory Authentik flow executor sessions. type FlowSessionStore struct { mu sync.Mutex baseURL string ttl time.Duration items map[string]*flowSessionEntry } func NewFlowSessionStore(baseURL string) *FlowSessionStore { return &FlowSessionStore{ baseURL: baseURL, ttl: defaultFlowSessionTTL, items: make(map[string]*flowSessionEntry), } } func (s *FlowSessionStore) Start(ctx context.Context, slug, query string) (sessionID string, challenge FlowChallenge, err error) { executor, err := NewFlowExecutor(s.baseURL, slug) if err != nil { return "", nil, err } challenge, err = executor.GetChallenge(ctx, query) if err != nil { return "", nil, err } id, err := randomSessionID() if err != nil { return "", nil, err } done, _ := FlowDone(challenge) s.mu.Lock() defer s.mu.Unlock() s.cleanupLocked(time.Now()) if !done { s.items[id] = &flowSessionEntry{ executor: executor, slug: slug, createdAt: time.Now(), } } return id, challenge, nil } func (s *FlowSessionStore) Respond(ctx context.Context, sessionID, slug, query string, payload map[string]any) (FlowChallenge, error) { entry, err := s.get(sessionID) if err != nil { return nil, err } if entry.slug != slug { return nil, ErrFlowSessionSlugMismatch } challenge, err := entry.executor.PostResponse(ctx, query, payload) if err != nil { return nil, err } done, _ := FlowDone(challenge) if done { s.deleteLocked(sessionID) } return challenge, nil } func (s *FlowSessionStore) Delete(sessionID string) { s.mu.Lock() defer s.mu.Unlock() s.deleteLocked(sessionID) } func (s *FlowSessionStore) deleteLocked(sessionID string) { delete(s.items, sessionID) } func (s *FlowSessionStore) get(sessionID string) (*flowSessionEntry, error) { s.mu.Lock() defer s.mu.Unlock() s.cleanupLocked(time.Now()) entry, ok := s.items[sessionID] if !ok { return nil, ErrFlowSessionNotFound } return entry, nil } func (s *FlowSessionStore) cleanupLocked(now time.Time) { for id, entry := range s.items { if now.Sub(entry.createdAt) > s.ttl { delete(s.items, id) } } } func randomSessionID() (string, error) { buf := make([]byte, 24) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil }