Skip to content

Commit 1681cce

Browse files
committed
Retrieve client information from spoe message
The client information are statically defined in agent config file. Goal of this patch is to be able to send those information directly via the spoe messages, and therefore be able to configure them directly on HAProxy config file. For the test/docker, since we can run only type of environment, run the one with dynamic client info option enabled.
1 parent 7e9eb29 commit 1681cce

File tree

7 files changed

+157
-42
lines changed

7 files changed

+157
-42
lines changed

cmd/haproxy-spoe-auth/main.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func LogLevelFromLogString(level string) logrus.Level {
2525
func main() {
2626
var configFile string
2727
flag.StringVar(&configFile, "config", "", "The path to the configuration file")
28+
dynamicClientInfo := flag.Bool("dynamic-client-info", false, "Dynamically read client information")
2829
flag.Parse()
2930

3031
if configFile != "" {
@@ -57,24 +58,31 @@ func main() {
5758
}
5859

5960
if viper.IsSet("oidc") {
60-
// TODO: watch the config file to update the list of clients dynamically
61-
var clientsConfig map[string]auth.OIDCClientConfig
62-
err := viper.UnmarshalKey("oidc.clients", &clientsConfig)
63-
if err != nil {
64-
logrus.Panic(err)
61+
var clientsStore auth.OIDCClientsStore
62+
if !*dynamicClientInfo {
63+
// TODO: watch the config file to update the list of clients dynamically
64+
var clientsConfig map[string]auth.OIDCClientConfig
65+
err := viper.UnmarshalKey("oidc.clients", &clientsConfig)
66+
if err != nil {
67+
logrus.Panic(err)
68+
}
69+
clientsStore = auth.NewStaticOIDCClientStore(clientsConfig)
70+
} else {
71+
clientsStore = auth.NewEmptyStaticOIDCClientStore()
6572
}
6673

6774
oidcAuthenticator := auth.NewOIDCAuthenticator(auth.OIDCAuthenticatorOptions{
6875
OAuth2AuthenticatorOptions: auth.OAuth2AuthenticatorOptions{
69-
RedirectCallbackPath: viper.GetString("oidc.oauth2_callback_path"),
70-
LogoutPath: viper.GetString("oidc.oauth2_logout_path"),
71-
HealthCheckPath: viper.GetString("oidc.oauth2_healthcheck_path"),
72-
CallbackAddr: viper.GetString("oidc.callback_addr"),
73-
CookieName: viper.GetString("oidc.cookie_name"),
74-
CookieSecure: viper.GetBool("oidc.cookie_secure"),
75-
CookieTTL: viper.GetDuration("oidc.cookie_ttl_seconds") * time.Second,
76-
SignatureSecret: viper.GetString("oidc.signature_secret"),
77-
ClientsStore: auth.NewStaticOIDCClientStore(clientsConfig),
76+
RedirectCallbackPath: viper.GetString("oidc.oauth2_callback_path"),
77+
LogoutPath: viper.GetString("oidc.oauth2_logout_path"),
78+
HealthCheckPath: viper.GetString("oidc.oauth2_healthcheck_path"),
79+
CallbackAddr: viper.GetString("oidc.callback_addr"),
80+
CookieName: viper.GetString("oidc.cookie_name"),
81+
CookieSecure: viper.GetBool("oidc.cookie_secure"),
82+
CookieTTL: viper.GetDuration("oidc.cookie_ttl_seconds") * time.Second,
83+
SignatureSecret: viper.GetString("oidc.signature_secret"),
84+
ClientsStore: clientsStore,
85+
ReadClientInfoFromMessages: *dynamicClientInfo,
7886
},
7987
ProviderURL: viper.GetString("oidc.provider_url"),
8088
EncryptionSecret: viper.GetString("oidc.encryption_secret"),

internal/auth/authenticator_oidc.go

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ type OAuth2AuthenticatorOptions struct {
5252

5353
// The object retrieving the OIDC client configuration from the given domain
5454
ClientsStore OIDCClientsStore
55+
56+
// Indicates whether the client info have to be read from spoe messages
57+
ReadClientInfoFromMessages bool
5558
}
5659

5760
// State the content of the state
@@ -72,6 +75,16 @@ type OIDCAuthenticator struct {
7275
options OIDCAuthenticatorOptions
7376
}
7477

78+
type OAuthArgs struct {
79+
ssl bool
80+
host string
81+
pathq string
82+
clientid string
83+
clientsecret string
84+
redirecturl string
85+
cookie string
86+
}
87+
7588
// NewOIDCAuthenticator create an instance of an OIDC authenticator
7689
func NewOIDCAuthenticator(options OIDCAuthenticatorOptions) *OIDCAuthenticator {
7790
if len(options.SignatureSecret) < 16 {
@@ -164,18 +177,20 @@ func (oa *OIDCAuthenticator) checkCookie(cookieValue string, domain string) erro
164177
return err
165178
}
166179

167-
func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error) {
180+
func extractOAuth2Args(msg *spoe.Message, readClientInfoFromMessages bool) (OAuthArgs, error) {
168181
var ssl *bool
169182
var host, pathq *string
170183
var cookie string
184+
var clientid, clientsecret, redirecturl *string
171185

172186
for msg.Args.Next() {
173187
arg := msg.Args.Arg
174188

175189
if arg.Name == "arg_ssl" {
176190
x, ok := arg.Value.(bool)
177191
if !ok {
178-
return false, "", "", "", fmt.Errorf("SSL is not a bool: %v", arg.Value)
192+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
193+
fmt.Errorf("SSL is not a bool: %v", arg.Value)
179194
}
180195

181196
ssl = new(bool)
@@ -186,7 +201,8 @@ func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error)
186201
if arg.Name == "arg_host" {
187202
x, ok := arg.Value.(string)
188203
if !ok {
189-
return false, "", "", "", fmt.Errorf("host is not a string: %v", arg.Value)
204+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
205+
fmt.Errorf("host is not a string: %v", arg.Value)
190206
}
191207

192208
host = new(string)
@@ -197,7 +213,8 @@ func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error)
197213
if arg.Name == "arg_pathq" {
198214
x, ok := arg.Value.(string)
199215
if !ok {
200-
return false, "", "", "", fmt.Errorf("pathq is not a string: %v", arg.Value)
216+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
217+
fmt.Errorf("pathq is not a string: %v", arg.Value)
201218
}
202219

203220
pathq = new(string)
@@ -214,20 +231,78 @@ func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error)
214231
cookie = x
215232
continue
216233
}
234+
235+
if arg.Name == "arg_client_id" {
236+
if !readClientInfoFromMessages {
237+
continue
238+
}
239+
x, ok := arg.Value.(string)
240+
if !ok {
241+
logrus.Debugf("clientid is not defined or not a string: %v", arg.Value)
242+
continue
243+
}
244+
245+
clientid = new(string)
246+
*clientid = x
247+
continue
248+
}
249+
250+
if arg.Name == "arg_client_secret" {
251+
if !readClientInfoFromMessages {
252+
continue
253+
}
254+
x, ok := arg.Value.(string)
255+
if !ok {
256+
logrus.Debugf("clientsecret is not defined or not a string: %v", arg.Value)
257+
continue
258+
}
259+
260+
clientsecret = new(string)
261+
*clientsecret = x
262+
continue
263+
}
264+
265+
if arg.Name == "arg_redirect_url" {
266+
if !readClientInfoFromMessages {
267+
continue
268+
}
269+
x, ok := arg.Value.(string)
270+
if !ok {
271+
logrus.Debugf("redirecturl is not defined or not a string: %v", arg.Value)
272+
continue
273+
}
274+
275+
redirecturl = new(string)
276+
*redirecturl = x
277+
continue
278+
}
217279
}
218280

219281
if ssl == nil {
220-
return false, "", "", "", ErrSSLArgNotFound
282+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
283+
ErrSSLArgNotFound
221284
}
222285

223286
if host == nil {
224-
return false, "", "", "", ErrHostArgNotFound
287+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
288+
ErrHostArgNotFound
225289
}
226290

227291
if pathq == nil {
228-
return false, "", "", "", ErrPathqArgNotFound
292+
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
293+
ErrPathqArgNotFound
229294
}
230-
return *ssl, *host, *pathq, cookie, nil
295+
296+
if clientid == nil || clientsecret == nil || redirecturl == nil {
297+
temp := ""
298+
clientid = &temp
299+
clientsecret = &temp
300+
redirecturl = &temp
301+
}
302+
return OAuthArgs{ssl: *ssl, host: *host, pathq: *pathq,
303+
cookie: cookie, clientid: *clientid,
304+
clientsecret: *clientsecret, redirecturl: *redirecturl},
305+
nil
231306
}
232307

233308
func (oa *OIDCAuthenticator) computeStateSignature(state *State) string {
@@ -252,12 +327,17 @@ func extractDomainFromHost(host string) string {
252327

253328
// Authenticate treat an authentication request coming from HAProxy
254329
func (oa *OIDCAuthenticator) Authenticate(msg *spoe.Message) (bool, []spoe.Action, error) {
255-
ssl, host, pathq, cookieValue, err := extractOAuth2Args(msg)
330+
// ssl, host, pathq, clientid, clientsecret, redirecturl, cookieValue, err := extractOAuth2Args(msg, oa.options.ReadClientInfoFromMessages)
331+
oauthArgs, err := extractOAuth2Args(msg, oa.options.ReadClientInfoFromMessages)
256332
if err != nil {
257333
return false, nil, fmt.Errorf("unable to extract origin URL: %v", err)
258334
}
259335

260-
domain := extractDomainFromHost(host)
336+
domain := extractDomainFromHost(oauthArgs.host)
337+
338+
if oauthArgs.clientid != "" {
339+
oa.options.ClientsStore.AddClient(domain, oauthArgs.clientid, oauthArgs.clientsecret, oauthArgs.redirecturl)
340+
}
261341

262342
_, err = oa.options.ClientsStore.GetClient(domain)
263343
if err == ErrOIDCClientConfigNotFound {
@@ -267,8 +347,8 @@ func (oa *OIDCAuthenticator) Authenticate(msg *spoe.Message) (bool, []spoe.Actio
267347
}
268348

269349
// Verify the cookie to make sure the user is authenticated
270-
if cookieValue != "" {
271-
err := oa.checkCookie(cookieValue, extractDomainFromHost(host))
350+
if oauthArgs.cookie != "" {
351+
err := oa.checkCookie(oauthArgs.cookie, extractDomainFromHost(oauthArgs.host))
272352
if err != nil {
273353
return false, nil, err
274354
} else {
@@ -280,8 +360,8 @@ func (oa *OIDCAuthenticator) Authenticate(msg *spoe.Message) (bool, []spoe.Actio
280360

281361
var state State
282362
state.Timestamp = currentTime
283-
state.PathAndQueryString = pathq
284-
state.SSL = ssl
363+
state.PathAndQueryString = oauthArgs.pathq
364+
state.SSL = oauthArgs.ssl
285365
state.Signature = oa.computeStateSignature(&state)
286366

287367
stateBytes, err := msgpack.Marshal(state)

internal/auth/oidc_clients_store.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package auth
22

3+
import (
4+
"strings"
5+
)
6+
37
type OIDCClientConfig struct {
48
ClientID string `mapstructure:"client_id"`
59
ClientSecret string `mapstructure:"client_secret"`
@@ -9,6 +13,7 @@ type OIDCClientConfig struct {
913
type OIDCClientsStore interface {
1014
// Retrieve the client_id and client_secret based on the domain
1115
GetClient(domain string) (*OIDCClientConfig, error)
16+
AddClient(domain string, clientid string, clientsecret string, redirecturl string)
1217
}
1318

1419
type StaticOIDCClientsStore struct {
@@ -19,9 +24,23 @@ func NewStaticOIDCClientStore(config map[string]OIDCClientConfig) *StaticOIDCCli
1924
return &StaticOIDCClientsStore{clients: config}
2025
}
2126

27+
func NewEmptyStaticOIDCClientStore() *StaticOIDCClientsStore {
28+
return &StaticOIDCClientsStore{clients: map[string]OIDCClientConfig{}}
29+
}
30+
2231
func (ocf *StaticOIDCClientsStore) GetClient(domain string) (*OIDCClientConfig, error) {
2332
if config, ok := ocf.clients[domain]; ok {
2433
return &config, nil
2534
}
2635
return nil, ErrOIDCClientConfigNotFound
2736
}
37+
38+
func (ocf *StaticOIDCClientsStore) AddClient(domain string, clientid string, clientsecret string, redirecturl string) {
39+
if _, ok := ocf.clients[domain]; !ok {
40+
ocf.clients[strings.Clone(domain)] = OIDCClientConfig {
41+
ClientID: strings.Clone(clientid),
42+
ClientSecret: strings.Clone(clientsecret),
43+
RedirectURL: strings.Clone(redirecturl),
44+
}
45+
}
46+
}

resources/configuration/config.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ oidc:
4444

4545
# A mapping of client credentials per protected domain
4646
clients:
47-
app2.example.com:
48-
client_id: app2-client
49-
client_secret: app2-secret
50-
redirect_url: http://app2.example.com:9080/oauth2/callback
51-
app3.example.com:
52-
client_id: app3-client
53-
client_secret: app3-secret
54-
redirect_url: http://app3.example.com:9080/oauth2/callback
47+
dummy.example.com:
48+
client_id: dummy-client
49+
client_secret: dummy-secret
50+
redirect_url: http://dummy.example.com:9080/oauth2/callback

resources/haproxy/haproxy.cfg

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,23 @@ frontend haproxynode
2222
# Domains to protect
2323
acl acl_public hdr_beg(host) -i public.example.com
2424
acl acl_app1 hdr_beg(host) -i app1.example.com
25+
2526
acl acl_app2 hdr_beg(host) -i app2.example.com
27+
http-request set-var(req.oidc_client_id) str(app2-client) if acl_app2
28+
http-request set-var(req.oidc_client_secret) str(app2-secret) if acl_app2
29+
http-request set-var(req.oidc_redirect_url) str(http://app2.example.com:9080/oauth2/callback) if acl_app2
30+
2631
acl acl_app3 hdr_beg(host) -i app3.example.com
32+
http-request set-var(req.oidc_client_id) str(app3-client) if acl_app3
33+
http-request set-var(req.oidc_client_secret) str(app3-secret) if acl_app3
34+
http-request set-var(req.oidc_redirect_url) str(http://app3.example.com:9080/oauth2/callback) if acl_app3
35+
2736
acl oauth2callback path_beg /oauth2/callback
2837
acl oauth2logout path_beg /oauth2/logout
2938

3039
acl dex_domain hdr_beg(host) -i dex.example.com
3140
# define the spoe agent
41+
http-request send-spoe-group spoe-auth try-auth-all
3242
filter spoe engine spoe-auth config /usr/local/etc/haproxy/spoe-auth.conf
3343

3444
# map the spoe response to acl variables

resources/haproxy/spoe-auth.conf

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
[spoe-auth]
22
spoe-agent auth-agents
3-
messages try-auth-ldap
4-
messages try-auth-oidc
5-
63
option var-prefix auth
74

85
timeout hello 2s
96
timeout idle 2m
107
timeout processing 1s
118

9+
groups try-auth-all
1210
use-backend backend_spoe-agent
1311

12+
spoe-group try-auth-all
13+
messages try-auth-ldap
14+
messages try-auth-oidc
15+
1416
spoe-message try-auth-ldap
1517
args authorization=req.hdr(Authorization) authorized_group=str(users)
1618
event on-frontend-http-request if { hdr_beg(host) -i app1.example.com } || { hdr_beg(host) -i app2.example.com } || { hdr_beg(host) -i app3.example.com }
1719

1820
spoe-message try-auth-oidc
19-
args arg_ssl=ssl_fc arg_host=req.hdr(Host) arg_pathq=pathq arg_cookie=req.cook(authsession)
21+
args arg_ssl=ssl_fc arg_host=req.hdr(Host) arg_pathq=pathq arg_cookie=req.cook(authsession) arg_client_id=var(req.oidc_client_id) arg_client_secret=var(req.oidc_client_secret) arg_redirect_url=var(req.oidc_redirect_url)
2022
event on-frontend-http-request if { hdr_beg(host) -i app1.example.com } || { hdr_beg(host) -i app2.example.com } || { hdr_beg(host) -i app3.example.com }

resources/scripts/run.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ if [[ "$DEBUG_ENABLED" -eq "1" ]]
44
then
55
echo "Running agent along with debug server"
66
/scripts/run-with-debug.sh haproxy-spoe-auth cmd/haproxy-spoe-auth/main.go -- \
7-
-config /configuration/config.yml
7+
-config /configuration/config.yml -dynamic-client-info
88
else
99
while true
1010
do
1111
echo "Running agent without debug server"
12-
go run cmd/haproxy-spoe-auth/main.go -config /configuration/config.yml
12+
go run cmd/haproxy-spoe-auth/main.go -config /configuration/config.yml -dynamic-client-info
1313
sleep 2
1414
done
1515
fi

0 commit comments

Comments
 (0)