ultisuite-backend/internal/authentik/flow_session_store.go
R3D347HR4Y e4549f29b2
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(authentik): implement password recovery flow and API integration
- Added a new blueprint for password recovery (`05-ulti-recovery.yaml`) to facilitate user password reset via email.
- Introduced a new API handler for managing Authentik flow sessions, including starting and responding to flows.
- Implemented flow session management with in-memory storage for tracking user sessions during the recovery process.
- Enhanced error handling for flow session operations and added unit tests for the new functionalities.
- Updated README to include the new recovery flow in the Authentik blueprints documentation.
2026-06-19 22:34:29 +02:00

117 lines
2.5 KiB
Go

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
}