From ce668ef7f6cf01180bdf61ac0dc5b865b40e54c8 Mon Sep 17 00:00:00 2001 From: Erikson Bahr Date: Mon, 1 Jul 2024 15:08:07 -0300 Subject: [PATCH 1/4] add HSP IAM audit capabilities --- go.mod | 9 +- go.sum | 35 ++++++-- oauthproxy.go | 20 ++++- pkg/apis/options/options.go | 16 ++++ pkg/audit/audit_client.go | 160 ++++++++++++++++++++++++++++++++++++ pkg/audit/audit_error.go | 7 ++ pkg/audit/audit_event.go | 116 ++++++++++++++++++++++++++ pkg/audit/signature.go | 32 ++++++++ 8 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 pkg/audit/audit_client.go create mode 100644 pkg/audit/audit_error.go create mode 100644 pkg/audit/audit_event.go create mode 100644 pkg/audit/signature.go diff --git a/go.mod b/go.mod index 239485fa90..a754d9167b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-redis/redis/v9 v9.0.0-rc.1 + github.com/go-resty/resty/v2 v2.13.1 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.1 github.com/gorilla/mux v1.8.0 @@ -29,9 +30,9 @@ require ( github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.8.1 github.com/vmihailenco/msgpack/v5 v5.3.5 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.23.0 golang.org/x/exp v0.0.0-20221019170559-20944726eadf - golang.org/x/net v0.19.0 + golang.org/x/net v0.25.0 golang.org/x/oauth2 v0.13.0 golang.org/x/sync v0.4.0 google.golang.org/api v0.126.0 @@ -71,8 +72,8 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/grpc v1.60.1 // indirect diff --git a/go.sum b/go.sum index be7406a36e..84468e5d7f 100644 --- a/go.sum +++ b/go.sum @@ -159,6 +159,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-redis/redis/v9 v9.0.0-rc.1 h1:/+bS+yeUnanqAbuD3QwlejzQZ+4eqgfUtFTG4b+QnXs= github.com/go-redis/redis/v9 v9.0.0-rc.1/go.mod h1:8et+z03j0l8N+DvsVnclzjf3Dl/pFHgRk+2Ct1qw66A= +github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g= +github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -458,8 +460,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -499,6 +502,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -551,8 +555,11 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -589,6 +596,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -668,10 +676,17 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -682,11 +697,16 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -744,6 +764,7 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/oauthproxy.go b/oauthproxy.go index b733a780c4..403c7444af 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -24,6 +24,7 @@ import ( sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/audit" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" @@ -104,6 +105,8 @@ type OAuthProxy struct { serveMux *mux.Router redirectValidator redirect.Validator appDirector redirect.AppDirector + + AuditClient *audit.Client } // NewOAuthProxy creates a new instance of OAuthProxy from the options provided @@ -202,6 +205,17 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr Validator: redirectValidator, }) + auditClient, err := audit.NewAuditClient(&audit.ClientOpts{ + URL: opts.AuditURL, + Enabled: opts.EnableAudit, + ProductName: opts.AuditProductName, + ProductKey: opts.AuditProductKey, + SharedKey: opts.AuditSharedKey, + SecretKey: opts.AuditSecretKey}) + if err != nil { + return nil, fmt.Errorf("error setting up server (audit client): %v", err) + } + p := &OAuthProxy{ CookieOptions: &opts.Cookie, Validator: validator, @@ -231,6 +245,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr upstreamProxy: upstreamProxy, redirectValidator: redirectValidator, appDirector: appDirector, + AuditClient: auditClient, } p.buildServeMux(opts.ProxyPrefix) @@ -851,7 +866,9 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { } if !csrf.CheckOAuthState(nonce) { - logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack") + errorMsg := "Invalid authentication via OAuth2: CSRF token mismatch, potential attack" + logger.PrintAuthf(session.Email, req, logger.AuthFailure, errorMsg) + p.AuditClient.CreateFailedLoginAuditEntry(session, appRedirect, req.Header.Get("edisp-org-id"), errorMsg) p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.") return } @@ -880,6 +897,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) return } + p.AuditClient.CreateSuccessfulLoginAuditEntry(session, appRedirect, req.Header.Get("edisp-org-id")) http.Redirect(rw, req, appRedirect, http.StatusFound) } else { logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized") diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index c65f124445..daf1324f5e 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -72,6 +72,14 @@ type Options struct { oidcVerifier internaloidc.IDTokenVerifier jwtBearerVerifiers []internaloidc.IDTokenVerifier realClientIPParser ipapi.RealClientIPParser + + // Philips opts + EnableAudit bool `flag:"enable-audit" cfg:"enable_audit"` + AuditURL string `flag:"audit-url" cfg:"audit_url"` + AuditProductName string `flag:"audit-product-name" cfg:"audit_product_name"` + AuditProductKey string `flag:"audit-product-key" cfg:"audit_product_key"` + AuditSharedKey string `flag:"audit-shared-key" cfg:"audit_shared_key"` + AuditSecretKey string `flag:"audit-secret-key" cfg:"audit_secret_key"` } // Options for Getting internal values @@ -149,6 +157,14 @@ func NewFlagSet() *pflag.FlagSet { flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") + // Philips opts + flagSet.Bool("enable-audit", false, "Persist audit entries in the audit server upon user authentication and relevant errors") + flagSet.String("audit-url", "", "The url where the audit entries will be posted") + flagSet.String("audit-product-name", "", "The name of the product to be used in the audit") + flagSet.String("audit-product-key", "", "The key of the audit") + flagSet.String("audit-shared-key", "", "The shared key of the audit") + flagSet.String("audit-secret-key", "", "The secret key of the audit") + flagSet.AddFlagSet(cookieFlagSet()) flagSet.AddFlagSet(loggingFlagSet()) flagSet.AddFlagSet(templatesFlagSet()) diff --git a/pkg/audit/audit_client.go b/pkg/audit/audit_client.go new file mode 100644 index 0000000000..4bc3a7dc45 --- /dev/null +++ b/pkg/audit/audit_client.go @@ -0,0 +1,160 @@ +package audit + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/go-resty/resty/v2" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" +) + +type ClientOpts struct { + Enabled bool + URL string + ProductKey string + ProductName string + SharedKey string + SecretKey string +} + +// Client interface for communicating with audit system +type Client struct { + enabled bool + apiSignature APISignature + opts *ClientOpts + client *resty.Client +} + +func NewAuditClient(opts *ClientOpts) (*Client, error) { + if opts.Enabled { + log.Print("Audit entries will be created since OAUTH2_PROXY_ENABLE_AUDIT is true") + err := opts.Validate() + if err != nil { + return nil, err + } + } else { + log.Print("Audit entries will NOT be created since OAUTH2_PROXY_ENABLE_AUDIT is false") + } + apiSignature := NewAPISignature(opts.SecretKey, opts.SharedKey) + client := resty.New() + client.SetRetryCount(3). + SetRetryWaitTime(5 * time.Second). + SetRetryMaxWaitTime(20 * time.Second). + SetContentLength(true). + SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) { + return 0, fmt.Errorf("%w: retry quota exceeded", ErrPersitAuditEvent) + }) + return &Client{enabled: opts.Enabled, apiSignature: apiSignature, client: client, opts: opts}, nil +} + +func (a *Client) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantId string) { + a.createAuditEntry(ss, appURL, tenantId, "0", "Success") +} + +func (a *Client) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantId string, errorDesc string) { + a.createAuditEntry(ss, appURL, tenantId, "1", errorDesc) +} + +func (a *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tenantId string, outcomeCode string, outcomeDesc string) { + if !a.enabled { + return + } + auditObject := AuditEvent{ + ResourceType: "AuditEvent", + Event: &Event{ + Type: &Coding{ + System: "http://hl7.org/fhir/ValueSet/audit-event-type", Version: "1", Code: "110114", Display: "User Authentication"}, + Action: "E", + DateTime: time.Now().UTC().Format(time.RFC3339), + Outcome: outcomeCode, + OutcomeDesc: outcomeDesc}, + + Participant: []*Participant{ + {AltID: ss.User, UserID: UserID{Value: ss.Email}, Name: ss.PreferredUsername, Requestor: true}}, + Source: Source{ + Identifier: Identifier{ + Type: &Coding{ + System: "http://hl7.org/fhir/ValueSet/identifier-type", + Code: "4", + Display: "Application Server", + }, + Value: ss.Email, + }, + Type: []*Coding{{System: "http://hl7.org/fhir/security-source-type", Code: "1", Display: "End-user display device, diagnostic device."}}, + Extension: []*Extension{ + { + URL: appURL, + Extension: []*ExtensionContent{ + { + URL: "applicationName", + ValueString: a.opts.ProductName, + }, + { + URL: "applicationVersion", + ValueString: "1", + }, + { + URL: "serverName", + ValueString: "oauth2proxy", + }, + { + URL: "componentName", + ValueString: "oauth2proxy", + }, + { + URL: "productKey", + ValueString: a.opts.ProductKey, + }, + { + URL: "tenant", + ValueString: tenantId, + }, + }, + }, + }, + }, + } + + auditMessage, err := json.Marshal(auditObject) + if err != nil { + logger.Errorf("%s: could not marshal the audit object: %v", ErrPersitAuditEvent.Error(), err) + return + } + err = a.send(string(auditMessage)) + if err != nil { + logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), a.opts.URL, err) + return + } +} + +func (c *Client) send(msg string) error { + signedDate := time.Now().UTC().Format(time.RFC3339) + signature := c.apiSignature.GetSignature(signedDate) + resp, err := c.client.R(). + SetHeader("Content-Type", "application/json"). + SetHeader("api-version", "2"). + SetHeader("HSDP-API-Signature", signature). + SetHeader("SignedDate", signedDate). + SetBody(msg). + Post(c.opts.URL) + if err != nil { + return err + } + if resp.StatusCode() != 201 { + log.Println("Not able to send the audit message ", resp) + return fmt.Errorf("not able to persist audit, audit server returned %v", resp.StatusCode()) + } + return nil +} + +func (a *ClientOpts) Validate() error { + if strings.TrimSpace(a.ProductName) == "" || strings.TrimSpace(a.ProductKey) == "" || strings.TrimSpace(a.SecretKey) == "" || strings.TrimSpace(a.SharedKey) == "" { + return errors.New("the audit is enabled and therefore the audit product name, audit key, audit secret key or audit shared key are required (however found empty)") + } + return nil +} diff --git a/pkg/audit/audit_error.go b/pkg/audit/audit_error.go new file mode 100644 index 0000000000..a6cda1456b --- /dev/null +++ b/pkg/audit/audit_error.go @@ -0,0 +1,7 @@ +package audit + +import "errors" + +var ( + ErrPersitAuditEvent = errors.New("could not persist the audit event") +) diff --git a/pkg/audit/audit_event.go b/pkg/audit/audit_event.go new file mode 100644 index 0000000000..076fd600e0 --- /dev/null +++ b/pkg/audit/audit_event.go @@ -0,0 +1,116 @@ +package audit + +type Coding struct { + System string `json:"system,omitempty"` + Version string `json:"version,omitempty"` + Code string `json:"code,omitempty"` + Display string `json:"display,omitempty"` + UserSelected string `json:"userSelected,omitempty"` +} + +type Identifier struct { + Use string `json:"use,omitempty"` + Type *Coding `json:"type,omitempty"` + Value string `json:"value,omitempty"` + System string `json:"system,omitempty"` +} + +type Reference struct { + Reference string `json:"reference,omitempty"` + Display string `json:"display,omitempty"` +} + +type Detail struct { + Type string `json:"type,omitempty"` + Value string `json:"value,omitempty"` +} + +type Object struct { + Identifier *Identifier `json:"identifier,omitempty"` + Reference *Reference `json:"reference,omitempty"` + Type *Coding `json:"type,omitempty"` + Role *Coding `json:"role,omitempty"` + Lifecycle *Coding `json:"lifecycle,omitempty"` + SecurityLabel []Coding `json:"securityLabel,omitempty"` + Description string `json:"description,omitempty"` + Query interface{} `json:"query,omitempty"` + Name string `json:"name,omitempty"` + Detail []Detail `json:"detail,omitempty"` +} + +type Type struct { + Coding `json:"coding,omitempty"` + Text string `json:"text,omitempty"` +} + +type UserID struct { + Use string `json:"use,omitempty"` + Type *Type `json:"type,omitempty"` + System string `json:"system,omitempty"` + Value string `json:"value,omitempty"` +} + +type Network struct { + Address string `json:"address,omitempty"` + Type string `json:"type,omitempty"` +} + +type Media struct { + *Coding `json:"coding,omitempty"` +} + +type PurposeOfUse struct { + *Coding `json:"coding,omitempty"` +} + +type Participant struct { + Role []Type `json:"role,omitempty"` + Reference *Reference `json:"reference,omitempty"` + UserID UserID `json:"userId,omitempty"` + AltID string `json:"altId,omitempty"` + Name string `json:"name,omitempty"` + Requestor bool `json:"requestor,omitempty"` + Location *Reference `json:"location,omitempty"` + Policy []string `json:"policy,omitempty"` + Media *Media `json:"media,omitempty"` + Network *Network `json:"network,omitempty"` + PurposeOfUse []PurposeOfUse `json:"purposeOfUse,omitempty"` +} + +type ExtensionContent struct { + URL string `json:"url,omitempty"` + ValueString string `json:"valueString,omitempty"` +} +type Extension struct { + URL string `json:"url,omitempty"` + Extension []*ExtensionContent `json:"extension,omitempty"` +} + +type Source struct { + Site string `json:"site,omitempty"` + Identifier Identifier `json:"identifier,omitempty"` + Type []*Coding `json:"type,omitempty"` + Extension []*Extension `json:"extension,omitempty"` +} + +type PurposeOfEvent struct { + Coding `json:"coding,omitempty"` +} + +type Event struct { + Type *Coding `json:"type,omitempty"` + Subtype []*Coding `json:"subtype,omitempty"` + Action string `json:"action,omitempty"` + DateTime string `json:"dateTime,omitempty"` + Outcome string `json:"outcome,omitempty"` + OutcomeDesc string `json:"outcomeDesc,omitempty"` + PurposeOfEvent []*PurposeOfEvent `json:"purposeOfEvent,omitempty"` +} + +type AuditEvent struct { + ResourceType string `json:"resourceType,omitempty"` + Event *Event `json:"event,omitempty"` + Participant []*Participant `json:"participant,omitempty"` + Source Source `json:"source,omitempty"` + Object []*Object `json:"object,omitempty"` +} diff --git a/pkg/audit/signature.go b/pkg/audit/signature.go new file mode 100644 index 0000000000..173ab64c1b --- /dev/null +++ b/pkg/audit/signature.go @@ -0,0 +1,32 @@ +package audit + +import ( + "crypto/hmac" + "crypto/sha256" + b64 "encoding/base64" + "fmt" + "hash" +) + +type APISignature interface { + GetSignature(signedDate string) string +} + +type apiSignature struct { + secretKey string + sharedKey string + hash hash.Hash +} + +func NewAPISignature(secretKey, sharedKey string) APISignature { + secret := fmt.Sprintf("DHPWS%s", secretKey) + return &apiSignature{secretKey: secretKey, sharedKey: sharedKey, hash: hmac.New(sha256.New, []byte(secret))} +} + +func (s *apiSignature) GetSignature(signedDate string) string { + s.hash.Write([]byte(b64.StdEncoding.EncodeToString([]byte(signedDate)))) + b := s.hash.Sum(nil) + sEnc := b64.StdEncoding.EncodeToString(b) + s.hash.Reset() + return fmt.Sprintf("HmacSHA256;Credential:%s;SignedHeaders:SignedDate;Signature:%s", s.sharedKey, sEnc) +} From 8e832221bb3e350c909d5d54c9a0c9b7de7e183b Mon Sep 17 00:00:00 2001 From: Erikson Bahr Date: Mon, 1 Jul 2024 15:22:04 -0300 Subject: [PATCH 2/4] no longer break pipeline if cookie refresher fail --- pkg/middleware/cookie_refresh.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/middleware/cookie_refresh.go b/pkg/middleware/cookie_refresh.go index 06bcfcef34..890594abca 100644 --- a/pkg/middleware/cookie_refresh.go +++ b/pkg/middleware/cookie_refresh.go @@ -41,6 +41,7 @@ func (cr *cookieRefresh) refreshCookie(next http.Handler) http.Handler { cookie, err := req.Cookie(cr.CookieRefreshName) if err != nil { logger.Errorf("SSO Cookie Refresher - Could find '%s' cookie in the request: %v", cr.CookieRefreshName, err) + next.ServeHTTP(rw, req) return } resp := requests.New(cr.CookieRefreshURL). @@ -53,6 +54,7 @@ func (cr *cookieRefresh) refreshCookie(next http.Handler) http.Handler { if resp.StatusCode() != http.StatusNoContent { bodyString := string(resp.Body()) logger.Errorf("SSO Cookie Refresher - Could not refresh the '%s' cookie in the url '%s' due to status and content: %v - %v", cr.CookieRefreshName, cr.CookieRefreshURL, resp.StatusCode(), bodyString) + next.ServeHTTP(rw, req) return } From a00969ab06c041140cb5a5f6b709837e643a61c0 Mon Sep 17 00:00:00 2001 From: Erikson Bahr Date: Mon, 1 Jul 2024 15:26:17 -0300 Subject: [PATCH 3/4] fix lint --- pkg/audit/audit_client.go | 28 ++++++++++++++-------------- pkg/audit/audit_event.go | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/audit/audit_client.go b/pkg/audit/audit_client.go index 4bc3a7dc45..3bee97d462 100644 --- a/pkg/audit/audit_client.go +++ b/pkg/audit/audit_client.go @@ -52,19 +52,19 @@ func NewAuditClient(opts *ClientOpts) (*Client, error) { return &Client{enabled: opts.Enabled, apiSignature: apiSignature, client: client, opts: opts}, nil } -func (a *Client) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantId string) { - a.createAuditEntry(ss, appURL, tenantId, "0", "Success") +func (c *Client) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string) { + c.createAuditEntry(ss, appURL, tenantID, "0", "Success") } -func (a *Client) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantId string, errorDesc string) { - a.createAuditEntry(ss, appURL, tenantId, "1", errorDesc) +func (c *Client) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, errorDesc string) { + c.createAuditEntry(ss, appURL, tenantID, "1", errorDesc) } -func (a *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tenantId string, outcomeCode string, outcomeDesc string) { - if !a.enabled { +func (c *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, outcomeCode string, outcomeDesc string) { + if !c.enabled { return } - auditObject := AuditEvent{ + auditObject := RootEvent{ ResourceType: "AuditEvent", Event: &Event{ Type: &Coding{ @@ -92,7 +92,7 @@ func (a *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tena Extension: []*ExtensionContent{ { URL: "applicationName", - ValueString: a.opts.ProductName, + ValueString: c.opts.ProductName, }, { URL: "applicationVersion", @@ -108,11 +108,11 @@ func (a *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tena }, { URL: "productKey", - ValueString: a.opts.ProductKey, + ValueString: c.opts.ProductKey, }, { URL: "tenant", - ValueString: tenantId, + ValueString: tenantID, }, }, }, @@ -125,9 +125,9 @@ func (a *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tena logger.Errorf("%s: could not marshal the audit object: %v", ErrPersitAuditEvent.Error(), err) return } - err = a.send(string(auditMessage)) + err = c.send(string(auditMessage)) if err != nil { - logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), a.opts.URL, err) + logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), c.opts.URL, err) return } } @@ -152,8 +152,8 @@ func (c *Client) send(msg string) error { return nil } -func (a *ClientOpts) Validate() error { - if strings.TrimSpace(a.ProductName) == "" || strings.TrimSpace(a.ProductKey) == "" || strings.TrimSpace(a.SecretKey) == "" || strings.TrimSpace(a.SharedKey) == "" { +func (c *ClientOpts) Validate() error { + if strings.TrimSpace(c.ProductName) == "" || strings.TrimSpace(c.ProductKey) == "" || strings.TrimSpace(c.SecretKey) == "" || strings.TrimSpace(c.SharedKey) == "" { return errors.New("the audit is enabled and therefore the audit product name, audit key, audit secret key or audit shared key are required (however found empty)") } return nil diff --git a/pkg/audit/audit_event.go b/pkg/audit/audit_event.go index 076fd600e0..c0b9555efa 100644 --- a/pkg/audit/audit_event.go +++ b/pkg/audit/audit_event.go @@ -107,7 +107,7 @@ type Event struct { PurposeOfEvent []*PurposeOfEvent `json:"purposeOfEvent,omitempty"` } -type AuditEvent struct { +type RootEvent struct { ResourceType string `json:"resourceType,omitempty"` Event *Event `json:"event,omitempty"` Participant []*Participant `json:"participant,omitempty"` From ab969ac2bde473835d022641d763b1fd40eb7afd Mon Sep 17 00:00:00 2001 From: Erikson Bahr Date: Wed, 3 Jul 2024 09:34:44 -0300 Subject: [PATCH 4/4] fix valueset and improve error handling --- pkg/audit/audit_client.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/audit/audit_client.go b/pkg/audit/audit_client.go index 3bee97d462..2f60842d63 100644 --- a/pkg/audit/audit_client.go +++ b/pkg/audit/audit_client.go @@ -79,7 +79,7 @@ func (c *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tena Source: Source{ Identifier: Identifier{ Type: &Coding{ - System: "http://hl7.org/fhir/ValueSet/identifier-type", + System: "http://hl7.org/fhir/ValueSet/audit-source-type", Code: "4", Display: "Application Server", }, @@ -153,8 +153,29 @@ func (c *Client) send(msg string) error { } func (c *ClientOpts) Validate() error { - if strings.TrimSpace(c.ProductName) == "" || strings.TrimSpace(c.ProductKey) == "" || strings.TrimSpace(c.SecretKey) == "" || strings.TrimSpace(c.SharedKey) == "" { - return errors.New("the audit is enabled and therefore the audit product name, audit key, audit secret key or audit shared key are required (however found empty)") + err := errors.New("") + if strings.TrimSpace(c.URL) == "" { + err = errors.New("the OAUTH2_PROXY_AUDIT_URL must be set") + } + + if strings.TrimSpace(c.ProductName) == "" { + err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_PRODUCT_NAME must be set", err) + } + + if strings.TrimSpace(c.ProductKey) == "" { + err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_PRODUCT_KEY must be set", err) + } + + if strings.TrimSpace(c.SharedKey) == "" { + err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_SHARED_KEY must be set", err) + } + + if strings.TrimSpace(c.SecretKey) == "" { + err = fmt.Errorf("%w: the OAUTH2_PROXY_AUDIT_SECRET_KEY must be set", err) + } + + if err != nil && err.Error() != "" { + return fmt.Errorf("the OAUTH2_PROXY_ENABLE_AUDIT is set to true however these are missing: %w", err) } return nil }