1acdbb5519
Build and Test / verify (push) Failing after 1m22s
- Change CI container image to debian:trixie-slim and set GOPROXY. - Update Go version to 1.26 in Dockerfile and go.mod. - Refactor token validation to use SHA-256 hashes instead of plain tokens. - Add network policy to restrict access to the service. - Enhance README with new configuration details and usage examples. - Add tests for new token hash validation logic.
190 lines
4.3 KiB
Go
190 lines
4.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const EnvTokenHashes = "AUTH_PROXY_TOKEN_HASHES"
|
|
|
|
var (
|
|
ErrMissingAuthHeader = errors.New("missing authorization header")
|
|
ErrInvalidAuthHeader = errors.New("invalid authorization header format")
|
|
ErrEmptyTokenSet = errors.New("no token hashes configured")
|
|
ErrTokenDirUnreadable = errors.New("unable to read token hash directory")
|
|
ErrInvalidTokenHash = errors.New("invalid token hash format")
|
|
)
|
|
|
|
type Validator struct {
|
|
tokenDigests [][sha256.Size]byte
|
|
}
|
|
|
|
func NewValidator(tokenHashes []string) (*Validator, error) {
|
|
if len(tokenHashes) == 0 {
|
|
return nil, ErrEmptyTokenSet
|
|
}
|
|
|
|
unique := make(map[[sha256.Size]byte]struct{}, len(tokenHashes))
|
|
digests := make([][sha256.Size]byte, 0, len(tokenHashes))
|
|
for _, hashValue := range tokenHashes {
|
|
hashValue = normalizeHash(hashValue)
|
|
if hashValue == "" {
|
|
continue
|
|
}
|
|
digest, err := decodeHash(hashValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, exists := unique[digest]; exists {
|
|
continue
|
|
}
|
|
unique[digest] = struct{}{}
|
|
digests = append(digests, digest)
|
|
}
|
|
|
|
if len(digests) == 0 {
|
|
return nil, ErrEmptyTokenSet
|
|
}
|
|
|
|
return &Validator{tokenDigests: digests}, nil
|
|
}
|
|
|
|
func (v *Validator) IsValid(token string) bool {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" || v == nil {
|
|
return false
|
|
}
|
|
|
|
provided := sha256.Sum256([]byte(token))
|
|
matched := 0
|
|
for _, allowed := range v.tokenDigests {
|
|
matched |= subtle.ConstantTimeCompare(provided[:], allowed[:])
|
|
}
|
|
|
|
return matched == 1
|
|
}
|
|
|
|
func (v *Validator) TokenCount() int {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return len(v.tokenDigests)
|
|
}
|
|
|
|
func ParseBearerToken(authHeader string) (string, error) {
|
|
authHeader = strings.TrimSpace(authHeader)
|
|
if authHeader == "" {
|
|
return "", ErrMissingAuthHeader
|
|
}
|
|
|
|
parts := strings.Fields(authHeader)
|
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) == "" {
|
|
return "", ErrInvalidAuthHeader
|
|
}
|
|
|
|
return parts[1], nil
|
|
}
|
|
|
|
func LoadTokenHashes(hashesDir, hashesCSV string) ([]string, error) {
|
|
hashes := make([]string, 0)
|
|
|
|
dirHashes, err := readHashesFromDir(hashesDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hashes = append(hashes, dirHashes...)
|
|
hashes = append(hashes, parseHashesCSV(hashesCSV)...)
|
|
|
|
cleaned := make([]string, 0, len(hashes))
|
|
for _, hashValue := range hashes {
|
|
h := normalizeHash(hashValue)
|
|
if h != "" {
|
|
cleaned = append(cleaned, h)
|
|
}
|
|
}
|
|
|
|
if len(cleaned) == 0 {
|
|
return nil, ErrEmptyTokenSet
|
|
}
|
|
|
|
return cleaned, nil
|
|
}
|
|
|
|
func readHashesFromDir(hashesDir string) ([]string, error) {
|
|
if strings.TrimSpace(hashesDir) == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(hashesDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w %q: %v", ErrTokenDirUnreadable, hashesDir, err)
|
|
}
|
|
|
|
hashes := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
path := filepath.Join(hashesDir, entry.Name())
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read hash file %q: %w", path, err)
|
|
}
|
|
hashes = append(hashes, strings.TrimSpace(string(content)))
|
|
}
|
|
|
|
return hashes, nil
|
|
}
|
|
|
|
func parseHashesCSV(hashesCSV string) []string {
|
|
if strings.TrimSpace(hashesCSV) == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(hashesCSV, ",")
|
|
hashes := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
h := normalizeHash(part)
|
|
if h != "" {
|
|
hashes = append(hashes, h)
|
|
}
|
|
}
|
|
return hashes
|
|
}
|
|
|
|
func normalizeHash(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if strings.HasPrefix(strings.ToLower(value), "sha256:") {
|
|
value = strings.TrimSpace(value[len("sha256:"):])
|
|
}
|
|
return strings.ToLower(value)
|
|
}
|
|
|
|
func decodeHash(value string) ([sha256.Size]byte, error) {
|
|
var digest [sha256.Size]byte
|
|
if len(value) != sha256.Size*2 {
|
|
return digest, fmt.Errorf("%w: expected %d hex chars, got %d", ErrInvalidTokenHash, sha256.Size*2, len(value))
|
|
}
|
|
|
|
decoded, err := hex.DecodeString(value)
|
|
if err != nil {
|
|
return digest, fmt.Errorf("%w: %v", ErrInvalidTokenHash, err)
|
|
}
|
|
copy(digest[:], decoded)
|
|
return digest, nil
|
|
}
|
|
|
|
func TokenFingerprint(token string) string {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256([]byte(token))
|
|
return fmt.Sprintf("sha256:%x", sum[:6])
|
|
}
|