- 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.
117 lines
2.5 KiB
Go
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
|
|
}
|