Add initial implementation of gitea-mcp-forward-auth microservice
Build and Test / verify (push) Failing after 22s

- Create build and release workflows for CI/CD
- Implement Dockerfile for multi-stage builds
- Add core authentication logic with token validation
- Include HTTP handler for authorization checks
- Set up Kubernetes deployment and service manifests
- Update README with usage instructions and configuration details
This commit is contained in:
2026-07-11 21:58:58 +02:00
parent 75acb3c376
commit ea4aac6641
11 changed files with 603 additions and 1 deletions
+162
View File
@@ -0,0 +1,162 @@
package auth
import (
"crypto/sha256"
"crypto/subtle"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const EnvTokens = "AUTH_PROXY_TOKENS"
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")
)
type Validator struct {
tokenDigests [][sha256.Size]byte
}
func NewValidator(tokens []string) (*Validator, error) {
if len(tokens) == 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 == "" {
continue
}
digest := sha256.Sum256([]byte(t))
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 LoadTokens(tokensDir, tokensCSV string) ([]string, error) {
tokens := make([]string, 0)
dirTokens, err := readTokensFromDir(tokensDir)
if err != nil {
return nil, err
}
tokens = append(tokens, dirTokens...)
tokens = append(tokens, parseTokensCSV(tokensCSV)...)
cleaned := make([]string, 0, len(tokens))
for _, token := range tokens {
t := strings.TrimSpace(token)
if t != "" {
cleaned = append(cleaned, t)
}
}
if len(cleaned) == 0 {
return nil, ErrEmptyTokenSet
}
return cleaned, nil
}
func readTokensFromDir(tokensDir string) ([]string, error) {
if strings.TrimSpace(tokensDir) == "" {
return nil, nil
}
entries, err := os.ReadDir(tokensDir)
if err != nil {
return nil, fmt.Errorf("%w %q: %v", ErrTokenDirUnreadable, tokensDir, err)
}
tokens := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
path := filepath.Join(tokensDir, entry.Name())
content, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read token file %q: %w", path, err)
}
tokens = append(tokens, strings.TrimSpace(string(content)))
}
return tokens, nil
}
func parseTokensCSV(tokensCSV string) []string {
if strings.TrimSpace(tokensCSV) == "" {
return nil
}
parts := strings.Split(tokensCSV, ",")
tokens := make([]string, 0, len(parts))
for _, part := range parts {
t := strings.TrimSpace(part)
if t != "" {
tokens = append(tokens, t)
}
}
return tokens
}
func TokenFingerprint(token string) string {
token = strings.TrimSpace(token)
if token == "" {
return ""
}
sum := sha256.Sum256([]byte(token))
return fmt.Sprintf("sha256:%x", sum[:6])
}