Skip to content

Commit c825a31

Browse files
authored
feat: Implement cross domain validation (#32)
In environments where services run under multiple subdomains, it can be desirable to not re-challenge a user if they already passed a challenge on a different subdomain. Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
1 parent 7c8acb5 commit c825a31

5 files changed

Lines changed: 60 additions & 19 deletions

File tree

berghain.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ type LevelConfig struct {
1616
}
1717

1818
type Berghain struct {
19-
Levels []*LevelConfig
19+
Levels []*LevelConfig
20+
TrustedDomains []string
2021

2122
secret []byte
2223
hmac sync.Pool

cmd/spop/config.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ func (s *Secret) UnmarshalYAML(b []byte) error {
3333
return nil
3434
}
3535

36-
type FrontendConfig []LevelConfig
36+
type FrontendConfig struct {
37+
Levels []LevelConfig `yaml:"levels"`
38+
TrustedDomains []string `yaml:"trusted_domains"`
39+
}
3740

3841
func (fc FrontendConfig) AsBerghain(s []byte) *berghain.Berghain {
3942
b := berghain.NewBerghain(s)
40-
for _, c := range fc {
43+
44+
for _, c := range fc.Levels {
4145
b.Levels = append(b.Levels, c.AsLevelConfig())
4246
}
47+
48+
b.TrustedDomains = fc.TrustedDomains
49+
4350
return b
4451
}
4552

cmd/spop/config.yaml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
secret: JMal0XJRROOMsMdPqggG2tR56CTkpgN3r47GgUN/WSQ=
22

33
default:
4-
- duration: 24h
5-
type: none
6-
- duration: 30m
7-
type: pow
4+
levels:
5+
- duration: 24h
6+
type: none
7+
- duration: 30m
8+
type: pow
89

910
frontend:
1011
my_fancy_frontend:
11-
- duration: 30s
12-
type: none
13-
countdown: 9 # default is 3 seconds, maximum is 9
14-
- duration: 20s
15-
type: pow
16-
- duration: 10s
17-
type: pow
18-
countdown: 0
12+
# normally, the exact host is stored and examined during cookie validation
13+
# to allow a domain including all of its subdomains to share a validated session, list it here
14+
trusted_domains:
15+
- foo.example.com
16+
levels:
17+
- duration: 30s
18+
type: none
19+
countdown: 9 # default is 3 seconds, maximum is 9
20+
- duration: 20s
21+
type: pow
22+
- duration: 10s
23+
type: pow
24+
countdown: 0

cmd/spop/frontend.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log/slog"
77
"net/http"
88
"net/netip"
9+
"strings"
910
"sync"
1011

1112
"github.com/dropmorepackets/haproxy-go/pkg/buffer"
@@ -55,6 +56,17 @@ func releaseHostBuf(b *buffer.SliceBuffer) {
5556
hostBufPool.Put(b)
5657
}
5758

59+
func getTrustedDomain(host []byte, td []string) []byte {
60+
h := string(host)
61+
for _, d := range td {
62+
if strings.HasSuffix(h, d) {
63+
return []byte(d)
64+
}
65+
}
66+
67+
return nil
68+
}
69+
5870
func (f *frontend) HandleSPOEValidate(ctx context.Context, w *encoding.ActionWriter, m *encoding.Message) {
5971
k := encoding.AcquireKVEntry()
6072
defer encoding.ReleaseKVEntry(k)
@@ -93,9 +105,15 @@ func (f *frontend) HandleSPOEValidate(ctx context.Context, w *encoding.ActionWri
93105
return
94106
}
95107

108+
td := getTrustedDomain(host, f.bh.TrustedDomains)
109+
if td != nil {
110+
host = td
111+
}
112+
96113
hostBuf := acquireHostBuf()
97114
defer releaseHostBuf(hostBuf)
98-
copy(hostBuf.WriteNBytes(len(k.ValueBytes())), k.ValueBytes())
115+
116+
copy(hostBuf.WriteNBytes(len(host)), host)
99117
ri.Host = hostBuf.ReadBytes()
100118

101119
if err := readExpectedKVEntry(ctx, m, k, "cookie"); err != nil {
@@ -141,13 +159,22 @@ func (f *frontend) HandleSPOEChallenge(ctx context.Context, w *encoding.ActionWr
141159
if err := readExpectedKVEntry(ctx, m, k, "host"); err != nil {
142160
return
143161
}
144-
if len(k.ValueBytes()) > hostBufferLength {
162+
host := k.ValueBytes()
163+
if len(host) > hostBufferLength {
145164
slog.ErrorContext(ctx, "host length too big")
146165
}
147166

167+
td := getTrustedDomain(host, f.bh.TrustedDomains)
168+
if td != nil {
169+
host = td
170+
}
171+
172+
_ = w.SetString(encoding.VarScopeTransaction, "domain", string(host))
173+
148174
hostBuf := acquireHostBuf()
149175
defer releaseHostBuf(hostBuf)
150-
copy(hostBuf.WriteNBytes(len(k.ValueBytes())), k.ValueBytes())
176+
177+
copy(hostBuf.WriteNBytes(len(host)), host)
151178
ri.Host = hostBuf.ReadBytes()
152179

153180
req := berghain.AcquireValidatorRequest()

examples/haproxy/haproxy.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ backend berghain_http
5858

5959
acl has_token var(txn.berghain.token) -m found
6060

61-
http-after-response add-header set-cookie "berghain=%[var(txn.berghain.token)]; path=/;" if has_token
61+
http-after-response add-header set-cookie "berghain=%[var(txn.berghain.token)]; domain=%[var(txn.berghain.domain)]; path=/;" if has_token
6262
http-request return status 200 content-type "application/json" lf-string "%[var(txn.berghain.response)]" if is_challenge_path
6363

6464
http-request return status 404

0 commit comments

Comments
 (0)