93 lines
2.5 KiB
Go
93 lines
2.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// SimpleTokenValidator is a concurrent in-memory allowlist of opaque tokens.
|
|
// Its zero value is ready to use. Tokens have no automatic expiration; use JWT
|
|
// for signed expiry or RemoveToken for explicit revocation.
|
|
type SimpleTokenValidator struct {
|
|
tokens map[[32]byte]struct{} // keyed by SHA-256(token)
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewSimpleTokenValidator creates token validator
|
|
func NewSimpleTokenValidator() *SimpleTokenValidator {
|
|
return &SimpleTokenValidator{
|
|
tokens: make(map[[32]byte]struct{}),
|
|
}
|
|
}
|
|
|
|
// ValidateToken checks if token is valid
|
|
func (v *SimpleTokenValidator) ValidateToken(token string) bool {
|
|
if token == "" || len(token) > MaxTokenLen {
|
|
return false
|
|
}
|
|
h := sha256.Sum256([]byte(token))
|
|
v.mu.RLock()
|
|
defer v.mu.RUnlock()
|
|
_, ok := v.tokens[h]
|
|
return ok
|
|
}
|
|
|
|
// AddToken adds a token to the allowlist. Empty and oversized tokens are ignored.
|
|
// Provision high-entropy tokens, e.g. with crypto/rand.Text; do not use passwords.
|
|
func (v *SimpleTokenValidator) AddToken(token string) {
|
|
if token == "" || len(token) > MaxTokenLen {
|
|
return
|
|
}
|
|
h := sha256.Sum256([]byte(token))
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
if v.tokens == nil {
|
|
v.tokens = make(map[[32]byte]struct{})
|
|
}
|
|
v.tokens[h] = struct{}{}
|
|
}
|
|
|
|
// RemoveToken removes token from validator
|
|
func (v *SimpleTokenValidator) RemoveToken(token string) {
|
|
if token == "" || len(token) > MaxTokenLen {
|
|
return
|
|
}
|
|
h := sha256.Sum256([]byte(token))
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
delete(v.tokens, h)
|
|
}
|
|
|
|
// ParseBearerToken parses an RFC 6750 Bearer authorization value. The scheme is
|
|
// case-insensitive; tokens must use the b64token alphabet and contain no spaces.
|
|
// Parsing does not validate the token's signature or grant authorization.
|
|
func ParseBearerToken(header string) (string, error) {
|
|
// Bound before scanning, including an allowance for separator spaces.
|
|
if len(header) > MaxTokenLen+16 {
|
|
return "", ErrTokenTooLong
|
|
}
|
|
scheme, token, ok := strings.Cut(header, " ")
|
|
if !ok || !strings.EqualFold(scheme, "Bearer") {
|
|
return "", ErrAuthInvalidBearerFormat
|
|
}
|
|
token = strings.TrimLeft(token, " ")
|
|
if token == "" {
|
|
return "", ErrAuthEmptyBearerToken
|
|
}
|
|
if len(token) > MaxTokenLen {
|
|
return "", ErrTokenTooLong
|
|
}
|
|
padding := false
|
|
for i, ch := range []byte(token) {
|
|
if ch == '=' && i > 0 {
|
|
padding = true
|
|
continue
|
|
}
|
|
if padding || !(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' || strings.ContainsRune("-._~+/", rune(ch))) {
|
|
return "", ErrAuthInvalidBearerFormat
|
|
}
|
|
}
|
|
return token, nil
|
|
}
|