auth.go

v1.4.2
Doc Versions Source
1
package server
2
3
import (
4
	"crypto/subtle"
5
	"fmt"
6
	"net/http"
7
	"strings"
8
9
	"go.bigb.es/curator/internal/config"
10
)
11
12
// ParseAdminToken resolves an admin token value.
13
// If value starts with "@", the rest is treated as a file path and the token
14
// is read from the first non-empty, non-comment line.
15
func ParseAdminToken(value string) (string, error) {
16
	resolved, err := config.ResolveValue(value)
17
	if err != nil {
18
		return "", err
19
	}
20
	if !strings.HasPrefix(value, "@") {
21
		return value, nil
22
	}
23
24
	for _, line := range strings.Split(resolved, "\n") {
25
		token := strings.TrimSpace(line)
26
		if token != "" && !strings.HasPrefix(token, "#") {
27
			return token, nil
28
		}
29
	}
30
	return "", fmt.Errorf("no token found in file")
31
}
32
33
// ParseAuthTokens parses auth tokens from a value.
34
// If value starts with "@", the rest is treated as a file path
35
// (one token per line, # comments supported).
36
// Otherwise the value is split on commas.
37
func ParseAuthTokens(value string) (map[string]struct{}, error) {
38
	resolved, err := config.ResolveValue(value)
39
	if err != nil {
40
		return nil, err
41
	}
42
43
	if strings.HasPrefix(value, "@") {
44
		return parseTokenLines(resolved), nil
45
	}
46
47
	// Comma-separated tokens.
48
	tokens := make(map[string]struct{})
49
	for _, raw := range strings.Split(resolved, ",") {
50
		token := strings.TrimSpace(raw)
51
		if token != "" {
52
			tokens[token] = struct{}{}
53
		}
54
	}
55
	return tokens, nil
56
}
57
58
func parseTokenLines(data string) map[string]struct{} {
59
	tokens := make(map[string]struct{})
60
	for _, line := range strings.Split(data, "\n") {
61
		token := strings.TrimSpace(line)
62
		if token != "" && !strings.HasPrefix(token, "#") {
63
			tokens[token] = struct{}{}
64
		}
65
	}
66
	return tokens
67
}
68
69
func (s *Server) isAuthenticated(r *http.Request) bool {
70
	// Check Authorization: Bearer header.
71
	if auth := r.Header.Get("Authorization"); auth != "" {
72
		if token, ok := strings.CutPrefix(auth, "Bearer "); ok {
73
			if _, valid := s.AuthTokens[token]; valid {
74
				return true
75
			}
76
		}
77
	}
78
79
	// Check Basic auth.
80
	if _, password, ok := r.BasicAuth(); ok {
81
		if _, valid := s.AuthTokens[password]; valid {
82
			return true
83
		}
84
	}
85
86
	// Check session cookie (set via browser login).
87
	if cookie, err := r.Cookie("curator_auth"); err == nil {
88
		if _, valid := s.AuthTokens[cookie.Value]; valid {
89
			return true
90
		}
91
		if s.Cfg.AdminToken != "" && subtle.ConstantTimeCompare([]byte(cookie.Value), []byte(s.Cfg.AdminToken)) == 1 {
92
			return true
93
		}
94
	}
95
96
	// Check OIDC session cookie.
97
	if s.OIDCProvider != nil {
98
		if cookie, err := r.Cookie("curator_session"); err == nil {
99
			if email := s.OIDCProvider.ValidateSession(cookie.Value); email != "" {
100
				return true
101
			}
102
		}
103
	}
104
105
	return false
106
}
107
108
// CanAccessModule returns true if the request is allowed to access the module.
109
func (s *Server) CanAccessModule(modName string, r *http.Request) bool {
110
	mod, ok := s.Resolver.ResolveModule(modName)
111
	if !ok {
112
		return false
113
	}
114
115
	if !mod.Private {
116
		return true
117
	}
118
119
	return s.isAuthenticated(r)
120
}
121

Source Files