Update build configuration and improve token handling
Build and Test / verify (push) Failing after 1m22s
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.
This commit is contained in:
+65
-38
@@ -3,6 +3,7 @@ package auth
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -10,32 +11,36 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const EnvTokens = "AUTH_PROXY_TOKENS"
|
||||
const EnvTokenHashes = "AUTH_PROXY_TOKEN_HASHES"
|
||||
|
||||
var (
|
||||
ErrMissingAuthHeader = errors.New("missing authorization header")
|
||||
ErrInvalidAuthHeader = errors.New("invalid authorization header format")
|
||||
ErrEmptyTokenSet = errors.New("no tokens configured")
|
||||
ErrTokenDirUnreadable = errors.New("unable to read token directory")
|
||||
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(tokens []string) (*Validator, error) {
|
||||
if len(tokens) == 0 {
|
||||
func NewValidator(tokenHashes []string) (*Validator, error) {
|
||||
if len(tokenHashes) == 0 {
|
||||
return nil, ErrEmptyTokenSet
|
||||
}
|
||||
|
||||
unique := make(map[[sha256.Size]byte]struct{}, len(tokens))
|
||||
digests := make([][sha256.Size]byte, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
t := strings.TrimSpace(token)
|
||||
if t == "" {
|
||||
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 := sha256.Sum256([]byte(t))
|
||||
digest, err := decodeHash(hashValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, exists := unique[digest]; exists {
|
||||
continue
|
||||
}
|
||||
@@ -86,21 +91,21 @@ func ParseBearerToken(authHeader string) (string, error) {
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
func LoadTokens(tokensDir, tokensCSV string) ([]string, error) {
|
||||
tokens := make([]string, 0)
|
||||
func LoadTokenHashes(hashesDir, hashesCSV string) ([]string, error) {
|
||||
hashes := make([]string, 0)
|
||||
|
||||
dirTokens, err := readTokensFromDir(tokensDir)
|
||||
dirHashes, err := readHashesFromDir(hashesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, dirTokens...)
|
||||
tokens = append(tokens, parseTokensCSV(tokensCSV)...)
|
||||
hashes = append(hashes, dirHashes...)
|
||||
hashes = append(hashes, parseHashesCSV(hashesCSV)...)
|
||||
|
||||
cleaned := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
t := strings.TrimSpace(token)
|
||||
if t != "" {
|
||||
cleaned = append(cleaned, t)
|
||||
cleaned := make([]string, 0, len(hashes))
|
||||
for _, hashValue := range hashes {
|
||||
h := normalizeHash(hashValue)
|
||||
if h != "" {
|
||||
cleaned = append(cleaned, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,45 +116,67 @@ func LoadTokens(tokensDir, tokensCSV string) ([]string, error) {
|
||||
return cleaned, nil
|
||||
}
|
||||
|
||||
func readTokensFromDir(tokensDir string) ([]string, error) {
|
||||
if strings.TrimSpace(tokensDir) == "" {
|
||||
func readHashesFromDir(hashesDir string) ([]string, error) {
|
||||
if strings.TrimSpace(hashesDir) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(tokensDir)
|
||||
entries, err := os.ReadDir(hashesDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w %q: %v", ErrTokenDirUnreadable, tokensDir, err)
|
||||
return nil, fmt.Errorf("%w %q: %v", ErrTokenDirUnreadable, hashesDir, err)
|
||||
}
|
||||
|
||||
tokens := make([]string, 0, len(entries))
|
||||
hashes := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(tokensDir, entry.Name())
|
||||
path := filepath.Join(hashesDir, entry.Name())
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read token file %q: %w", path, err)
|
||||
return nil, fmt.Errorf("read hash file %q: %w", path, err)
|
||||
}
|
||||
tokens = append(tokens, strings.TrimSpace(string(content)))
|
||||
hashes = append(hashes, strings.TrimSpace(string(content)))
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
return hashes, nil
|
||||
}
|
||||
|
||||
func parseTokensCSV(tokensCSV string) []string {
|
||||
if strings.TrimSpace(tokensCSV) == "" {
|
||||
func parseHashesCSV(hashesCSV string) []string {
|
||||
if strings.TrimSpace(hashesCSV) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(tokensCSV, ",")
|
||||
tokens := make([]string, 0, len(parts))
|
||||
parts := strings.Split(hashesCSV, ",")
|
||||
hashes := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
t := strings.TrimSpace(part)
|
||||
if t != "" {
|
||||
tokens = append(tokens, t)
|
||||
h := normalizeHash(part)
|
||||
if h != "" {
|
||||
hashes = append(hashes, h)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
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 {
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package auth
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func hashHex(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return fmt.Sprintf("%x", sum[:])
|
||||
}
|
||||
|
||||
func TestValidator_IsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator, err := NewValidator([]string{"token-one", "token-two", "token-three"})
|
||||
validator, err := NewValidator([]string{hashHex("token-one"), hashHex("token-two"), hashHex("token-three")})
|
||||
if err != nil {
|
||||
t.Fatalf("NewValidator() error = %v", err)
|
||||
}
|
||||
@@ -45,6 +54,14 @@ func TestNewValidator_EmptyTokenSet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidator_InvalidHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := NewValidator([]string{"not-a-hash"}); err == nil {
|
||||
t.Fatal("NewValidator(invalid hash) expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBearerToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user