This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package authapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/auth"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFlowTTL = 10 * time.Minute
|
||||
defaultMaxFlows = 1024
|
||||
flowIDByteLength = 32
|
||||
)
|
||||
|
||||
// flow is the server-side state of one in-progress authorization code flow. Only
|
||||
// an opaque identifier for it ever reaches the browser.
|
||||
type flow struct {
|
||||
authorization auth.Authorization
|
||||
redirect string
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
// flowStore keeps pending flows in memory. It is bounded by TTL and by a maximum
|
||||
// entry count so an unauthenticated caller cannot grow it without limit, and it is
|
||||
// safe for concurrent use.
|
||||
type flowStore struct {
|
||||
mu sync.Mutex
|
||||
flows map[string]flow
|
||||
ttl time.Duration
|
||||
max int
|
||||
}
|
||||
|
||||
func newFlowStore(ttl time.Duration, max int) *flowStore {
|
||||
if ttl <= 0 {
|
||||
ttl = defaultFlowTTL
|
||||
}
|
||||
if max <= 0 {
|
||||
max = defaultMaxFlows
|
||||
}
|
||||
return &flowStore{flows: make(map[string]flow), ttl: ttl, max: max}
|
||||
}
|
||||
|
||||
// create stores one pending flow and returns its opaque identifier. Expired entries
|
||||
// are removed first; if the store is still at capacity the oldest entry is dropped
|
||||
// so a flood of abandoned flows cannot deny logins permanently.
|
||||
func (store *flowStore) create(entry flow, now time.Time) (string, error) {
|
||||
id, err := randomFlowID()
|
||||
if err != nil {
|
||||
return "", errors.New("generate authorization flow identifier")
|
||||
}
|
||||
entry.createdAt = now
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
store.purge(now)
|
||||
for len(store.flows) >= store.max && store.evictOldest() {
|
||||
}
|
||||
store.flows[id] = entry
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// take returns a pending flow and always removes it, so a flow identifier can be
|
||||
// used at most once. An unknown, replayed or expired identifier returns false.
|
||||
func (store *flowStore) take(id string, now time.Time) (flow, bool) {
|
||||
if id == "" {
|
||||
return flow{}, false
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
entry, ok := store.flows[id]
|
||||
delete(store.flows, id)
|
||||
if !ok || !now.Before(entry.createdAt.Add(store.ttl)) {
|
||||
return flow{}, false
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (store *flowStore) size() int {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
return len(store.flows)
|
||||
}
|
||||
|
||||
func (store *flowStore) purge(now time.Time) {
|
||||
for id, entry := range store.flows {
|
||||
if !now.Before(entry.createdAt.Add(store.ttl)) {
|
||||
delete(store.flows, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (store *flowStore) evictOldest() bool {
|
||||
oldest := ""
|
||||
var oldestAt time.Time
|
||||
for id, entry := range store.flows {
|
||||
if oldest == "" || entry.createdAt.Before(oldestAt) {
|
||||
oldest, oldestAt = id, entry.createdAt
|
||||
}
|
||||
}
|
||||
if oldest == "" {
|
||||
return false
|
||||
}
|
||||
delete(store.flows, oldest)
|
||||
return true
|
||||
}
|
||||
|
||||
func randomFlowID() (string, error) {
|
||||
buffer := make([]byte, flowIDByteLength)
|
||||
if _, err := rand.Read(buffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(buffer), nil
|
||||
}
|
||||
Reference in New Issue
Block a user