Skip to content

Commit

Permalink
Retrieve client information from spoe message
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mougams committed Mar 14, 2023
1 parent 7e9eb29 commit 1681cce
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 42 deletions.
36 changes: 22 additions & 14 deletions cmd/haproxy-spoe-auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func LogLevelFromLogString(level string) logrus.Level {
func main() {
var configFile string
flag.StringVar(&configFile, "config", "", "The path to the configuration file")
dynamicClientInfo := flag.Bool("dynamic-client-info", false, "Dynamically read client information")
flag.Parse()

if configFile != "" {
Expand Down Expand Up @@ -57,24 +58,31 @@ func main() {
}

if viper.IsSet("oidc") {
// TODO: watch the config file to update the list of clients dynamically
var clientsConfig map[string]auth.OIDCClientConfig
err := viper.UnmarshalKey("oidc.clients", &clientsConfig)
if err != nil {
logrus.Panic(err)
var clientsStore auth.OIDCClientsStore
if !*dynamicClientInfo {
// TODO: watch the config file to update the list of clients dynamically
var clientsConfig map[string]auth.OIDCClientConfig
err := viper.UnmarshalKey("oidc.clients", &clientsConfig)
if err != nil {
logrus.Panic(err)
}
clientsStore = auth.NewStaticOIDCClientStore(clientsConfig)
} else {
clientsStore = auth.NewEmptyStaticOIDCClientStore()
}

oidcAuthenticator := auth.NewOIDCAuthenticator(auth.OIDCAuthenticatorOptions{
OAuth2AuthenticatorOptions: auth.OAuth2AuthenticatorOptions{
RedirectCallbackPath: viper.GetString("oidc.oauth2_callback_path"),
LogoutPath: viper.GetString("oidc.oauth2_logout_path"),
HealthCheckPath: viper.GetString("oidc.oauth2_healthcheck_path"),
CallbackAddr: viper.GetString("oidc.callback_addr"),
CookieName: viper.GetString("oidc.cookie_name"),
CookieSecure: viper.GetBool("oidc.cookie_secure"),
CookieTTL: viper.GetDuration("oidc.cookie_ttl_seconds") * time.Second,
SignatureSecret: viper.GetString("oidc.signature_secret"),
ClientsStore: auth.NewStaticOIDCClientStore(clientsConfig),
RedirectCallbackPath: viper.GetString("oidc.oauth2_callback_path"),
LogoutPath: viper.GetString("oidc.oauth2_logout_path"),
HealthCheckPath: viper.GetString("oidc.oauth2_healthcheck_path"),
CallbackAddr: viper.GetString("oidc.callback_addr"),
CookieName: viper.GetString("oidc.cookie_name"),
CookieSecure: viper.GetBool("oidc.cookie_secure"),
CookieTTL: viper.GetDuration("oidc.cookie_ttl_seconds") * time.Second,
SignatureSecret: viper.GetString("oidc.signature_secret"),
ClientsStore: clientsStore,
ReadClientInfoFromMessages: *dynamicClientInfo,
},
ProviderURL: viper.GetString("oidc.provider_url"),
EncryptionSecret: viper.GetString("oidc.encryption_secret"),
Expand Down
108 changes: 94 additions & 14 deletions internal/auth/authenticator_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type OAuth2AuthenticatorOptions struct {

// The object retrieving the OIDC client configuration from the given domain
ClientsStore OIDCClientsStore

// Indicates whether the client info have to be read from spoe messages
ReadClientInfoFromMessages bool
}

// State the content of the state
Expand All @@ -72,6 +75,16 @@ type OIDCAuthenticator struct {
options OIDCAuthenticatorOptions
}

type OAuthArgs struct {
ssl bool
host string
pathq string
clientid string
clientsecret string
redirecturl string
cookie string
}

// NewOIDCAuthenticator create an instance of an OIDC authenticator
func NewOIDCAuthenticator(options OIDCAuthenticatorOptions) *OIDCAuthenticator {
if len(options.SignatureSecret) < 16 {
Expand Down Expand Up @@ -164,18 +177,20 @@ func (oa *OIDCAuthenticator) checkCookie(cookieValue string, domain string) erro
return err
}

func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error) {
func extractOAuth2Args(msg *spoe.Message, readClientInfoFromMessages bool) (OAuthArgs, error) {
var ssl *bool
var host, pathq *string
var cookie string
var clientid, clientsecret, redirecturl *string

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

if arg.Name == "arg_ssl" {
x, ok := arg.Value.(bool)
if !ok {
return false, "", "", "", fmt.Errorf("SSL is not a bool: %v", arg.Value)
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
fmt.Errorf("SSL is not a bool: %v", arg.Value)
}

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

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

pathq = new(string)
Expand All @@ -214,20 +231,78 @@ func extractOAuth2Args(msg *spoe.Message) (bool, string, string, string, error)
cookie = x
continue
}

if arg.Name == "arg_client_id" {
if !readClientInfoFromMessages {
continue
}
x, ok := arg.Value.(string)
if !ok {
logrus.Debugf("clientid is not defined or not a string: %v", arg.Value)
continue
}

clientid = new(string)
*clientid = x
continue
}

if arg.Name == "arg_client_secret" {
if !readClientInfoFromMessages {
continue
}
x, ok := arg.Value.(string)
if !ok {
logrus.Debugf("clientsecret is not defined or not a string: %v", arg.Value)
continue
}

clientsecret = new(string)
*clientsecret = x
continue
}

if arg.Name == "arg_redirect_url" {
if !readClientInfoFromMessages {
continue
}
x, ok := arg.Value.(string)
if !ok {
logrus.Debugf("redirecturl is not defined or not a string: %v", arg.Value)
continue
}

redirecturl = new(string)
*redirecturl = x
continue
}
}

if ssl == nil {
return false, "", "", "", ErrSSLArgNotFound
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
ErrSSLArgNotFound
}

if host == nil {
return false, "", "", "", ErrHostArgNotFound
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
ErrHostArgNotFound
}

if pathq == nil {
return false, "", "", "", ErrPathqArgNotFound
return OAuthArgs{ssl: false, host: "", pathq: "", cookie: "", clientid: "", clientsecret: "", redirecturl: ""},
ErrPathqArgNotFound
}
return *ssl, *host, *pathq, cookie, nil

if clientid == nil || clientsecret == nil || redirecturl == nil {
temp := ""
clientid = &temp
clientsecret = &temp
redirecturl = &temp
}
return OAuthArgs{ssl: *ssl, host: *host, pathq: *pathq,
cookie: cookie, clientid: *clientid,
clientsecret: *clientsecret, redirecturl: *redirecturl},
nil
}

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

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

domain := extractDomainFromHost(host)
domain := extractDomainFromHost(oauthArgs.host)

if oauthArgs.clientid != "" {
oa.options.ClientsStore.AddClient(domain, oauthArgs.clientid, oauthArgs.clientsecret, oauthArgs.redirecturl)
}

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

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

var state State
state.Timestamp = currentTime
state.PathAndQueryString = pathq
state.SSL = ssl
state.PathAndQueryString = oauthArgs.pathq
state.SSL = oauthArgs.ssl
state.Signature = oa.computeStateSignature(&state)

stateBytes, err := msgpack.Marshal(state)
Expand Down
19 changes: 19 additions & 0 deletions internal/auth/oidc_clients_store.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package auth

import (
"strings"
)

type OIDCClientConfig struct {
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
Expand All @@ -9,6 +13,7 @@ type OIDCClientConfig struct {
type OIDCClientsStore interface {
// Retrieve the client_id and client_secret based on the domain
GetClient(domain string) (*OIDCClientConfig, error)
AddClient(domain string, clientid string, clientsecret string, redirecturl string)
}

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

func NewEmptyStaticOIDCClientStore() *StaticOIDCClientsStore {
return &StaticOIDCClientsStore{clients: map[string]OIDCClientConfig{}}
}

func (ocf *StaticOIDCClientsStore) GetClient(domain string) (*OIDCClientConfig, error) {
if config, ok := ocf.clients[domain]; ok {
return &config, nil
}
return nil, ErrOIDCClientConfigNotFound
}

func (ocf *StaticOIDCClientsStore) AddClient(domain string, clientid string, clientsecret string, redirecturl string) {
if _, ok := ocf.clients[domain]; !ok {
ocf.clients[strings.Clone(domain)] = OIDCClientConfig {
ClientID: strings.Clone(clientid),
ClientSecret: strings.Clone(clientsecret),
RedirectURL: strings.Clone(redirecturl),
}
}
}
12 changes: 4 additions & 8 deletions resources/configuration/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ oidc:

# A mapping of client credentials per protected domain
clients:
app2.example.com:
client_id: app2-client
client_secret: app2-secret
redirect_url: http://app2.example.com:9080/oauth2/callback
app3.example.com:
client_id: app3-client
client_secret: app3-secret
redirect_url: http://app3.example.com:9080/oauth2/callback
dummy.example.com:
client_id: dummy-client
client_secret: dummy-secret
redirect_url: http://dummy.example.com:9080/oauth2/callback
10 changes: 10 additions & 0 deletions resources/haproxy/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ frontend haproxynode
# Domains to protect
acl acl_public hdr_beg(host) -i public.example.com
acl acl_app1 hdr_beg(host) -i app1.example.com

acl acl_app2 hdr_beg(host) -i app2.example.com
http-request set-var(req.oidc_client_id) str(app2-client) if acl_app2
http-request set-var(req.oidc_client_secret) str(app2-secret) if acl_app2
http-request set-var(req.oidc_redirect_url) str(http://app2.example.com:9080/oauth2/callback) if acl_app2

acl acl_app3 hdr_beg(host) -i app3.example.com
http-request set-var(req.oidc_client_id) str(app3-client) if acl_app3
http-request set-var(req.oidc_client_secret) str(app3-secret) if acl_app3
http-request set-var(req.oidc_redirect_url) str(http://app3.example.com:9080/oauth2/callback) if acl_app3

acl oauth2callback path_beg /oauth2/callback
acl oauth2logout path_beg /oauth2/logout

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

# map the spoe response to acl variables
Expand Down
10 changes: 6 additions & 4 deletions resources/haproxy/spoe-auth.conf
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
[spoe-auth]
spoe-agent auth-agents
messages try-auth-ldap
messages try-auth-oidc

option var-prefix auth

timeout hello 2s
timeout idle 2m
timeout processing 1s

groups try-auth-all
use-backend backend_spoe-agent

spoe-group try-auth-all
messages try-auth-ldap
messages try-auth-oidc

spoe-message try-auth-ldap
args authorization=req.hdr(Authorization) authorized_group=str(users)
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 }

spoe-message try-auth-oidc
args arg_ssl=ssl_fc arg_host=req.hdr(Host) arg_pathq=pathq arg_cookie=req.cook(authsession)
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)
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 }
4 changes: 2 additions & 2 deletions resources/scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ if [[ "$DEBUG_ENABLED" -eq "1" ]]
then
echo "Running agent along with debug server"
/scripts/run-with-debug.sh haproxy-spoe-auth cmd/haproxy-spoe-auth/main.go -- \
-config /configuration/config.yml
-config /configuration/config.yml -dynamic-client-info
else
while true
do
echo "Running agent without debug server"
go run cmd/haproxy-spoe-auth/main.go -config /configuration/config.yml
go run cmd/haproxy-spoe-auth/main.go -config /configuration/config.yml -dynamic-client-info
sleep 2
done
fi

0 comments on commit 1681cce

Please sign in to comment.