Public source validation / validate (push) Failing after 3m8s
117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
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
|
|
}
|