add HSP IAM audit capabilities (#52)

<!--- Provide a general summary of your changes in the Title above -->

## Description

Add HSDP IAM audit capabilities to oauth2 proxy

## Motivation and Context

HSDP IAM does not provide important audit capabilities out of the box
such as successful login and some key auth errors.

## How Has This Been Tested?

Tested locally with:

```sh
export OAUTH2_PROXY_ENABLE_AUDIT='true'
export OAUTH2_PROXY_AUDIT_URL='url'
export OAUTH2_PROXY_AUDIT_PRODUCT_NAME='Reporting'
export OAUTH2_PROXY_AUDIT_PRODUCT_KEY='product key'
export OAUTH2_PROXY_AUDIT_SHARED_KEY='shared key'
export OAUTH2_PROXY_AUDIT_SECRET_KEY='secret'
```

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->

- [ ] My change requires a change to the documentation or CHANGELOG.
- [ ] I have updated the documentation/CHANGELOG accordingly.
- [ ] I have created a feature (non-master) branch for my PR.
This commit is contained in:
Eduardo Vozniak 2024-07-03 10:11:30 -03:00 committed by GitHub
commit 8f7537a6e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 406 additions and 12 deletions

9
go.mod
View File

@ -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

35
go.sum
View File

@ -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=

View File

@ -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")

View File

@ -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())

181
pkg/audit/audit_client.go Normal file
View File

@ -0,0 +1,181 @@
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 (c *Client) CreateSuccessfulLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string) {
c.createAuditEntry(ss, appURL, tenantID, "0", "Success")
}
func (c *Client) CreateFailedLoginAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, errorDesc string) {
c.createAuditEntry(ss, appURL, tenantID, "1", errorDesc)
}
func (c *Client) createAuditEntry(ss *sessions.SessionState, appURL string, tenantID string, outcomeCode string, outcomeDesc string) {
if !c.enabled {
return
}
auditObject := RootEvent{
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/audit-source-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: c.opts.ProductName,
},
{
URL: "applicationVersion",
ValueString: "1",
},
{
URL: "serverName",
ValueString: "oauth2proxy",
},
{
URL: "componentName",
ValueString: "oauth2proxy",
},
{
URL: "productKey",
ValueString: c.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 = c.send(string(auditMessage))
if err != nil {
logger.Errorf("%s: could not send the audit message to the url '%s': %v", ErrPersitAuditEvent.Error(), c.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 (c *ClientOpts) Validate() error {
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
}

7
pkg/audit/audit_error.go Normal file
View File

@ -0,0 +1,7 @@
package audit
import "errors"
var (
ErrPersitAuditEvent = errors.New("could not persist the audit event")
)

116
pkg/audit/audit_event.go Normal file
View File

@ -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 RootEvent 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"`
}

32
pkg/audit/signature.go Normal file
View File

@ -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)
}

View File

@ -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
}