From 4e6416fa799b274036fc743728daf7c1527d2c09 Mon Sep 17 00:00:00 2001 From: reugn Date: Wed, 7 Feb 2024 17:11:35 +0200 Subject: [PATCH 01/14] add golangci-lint workflow --- .github/workflows/golangci-lint.yml | 18 ++++++++++++++++++ .golangci.yml | 27 +++++++++++++++++++++++++++ auth/env.go | 2 +- auth/jwt_validator.go | 3 +-- auth/keys.go | 12 +++++------- repository/repository.go | 7 ++----- server.go | 6 +++--- 7 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..2209ddf --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,18 @@ +name: golangci-lint + +on: + push: + branches: + - master + pull_request: + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3329eb9 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,27 @@ +linters: + disable-all: true + enable: + - dupl + - errcheck + - errorlint + - exportloopref + - funlen + - gci + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - prealloc + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused diff --git a/auth/env.go b/auth/env.go index fc62afe..ff53f79 100644 --- a/auth/env.go +++ b/auth/env.go @@ -12,7 +12,7 @@ type environmentVariables struct { publicKeyPath string } -var env environmentVariables = environmentVariables{time.Hour, "", ""} +var env = environmentVariables{time.Hour, "", ""} func init() { // read environment variables diff --git a/auth/jwt_validator.go b/auth/jwt_validator.go index a24affd..dd454cd 100644 --- a/auth/jwt_validator.go +++ b/auth/jwt_validator.go @@ -2,7 +2,6 @@ package auth import ( "encoding/json" - "errors" "log" "time" @@ -41,7 +40,7 @@ func (val *JWTValidator) validateClaims(token *jwt.Token) (*Claims, error) { // validate expiration if claims.ExpiresAt < time.Now().Unix() { - return nil, errors.New("Expired JWT") + return nil, jwt.ErrTokenExpired } return claims, nil diff --git a/auth/keys.go b/auth/keys.go index 55e3d92..46e2db8 100644 --- a/auth/keys.go +++ b/auth/keys.go @@ -3,7 +3,7 @@ package auth import ( "crypto/rsa" "errors" - "io/ioutil" + "os" "github.com/golang-jwt/jwt/v4" ) @@ -51,28 +51,26 @@ func NewKeysFromPem(privatePem []byte, publicPem []byte) (*Keys, error) { func parsePrivateKey(privateKeyPath *string, pem []byte) (*rsa.PrivateKey, error) { if privateKeyPath != nil { - pem, err := ioutil.ReadFile(*privateKeyPath) + pem, err := os.ReadFile(*privateKeyPath) if err != nil { return nil, err } return jwt.ParseRSAPrivateKeyFromPEM(pem) } else if pem != nil { return jwt.ParseRSAPrivateKeyFromPEM(pem) - } else { - return nil, errors.New("parsePrivateKey nil parameters") } + return nil, errors.New("parsePrivateKey nil parameters") } func parsePublicKey(publicKeyPath *string, pem []byte) (*rsa.PublicKey, error) { if publicKeyPath != nil { - pem, err := ioutil.ReadFile(*publicKeyPath) + pem, err := os.ReadFile(*publicKeyPath) if err != nil { return nil, err } return jwt.ParseRSAPublicKeyFromPEM(pem) } else if pem != nil { return jwt.ParseRSAPublicKeyFromPEM(pem) - } else { - return nil, errors.New("parsePublicKey nil parameters") } + return nil, errors.New("parsePublicKey nil parameters") } diff --git a/repository/repository.go b/repository/repository.go index 76e75ed..ba68be3 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -41,6 +41,7 @@ func isAuthorizedRequest(scopes []map[string]string, request RequestDetails) boo return false } +//nolint:unused func hashAndSalt(pwd string) ([]byte, error) { bytePwd := []byte(pwd) @@ -58,9 +59,5 @@ func pwdMatch(hashed string, plain string) bool { plainBytes := []byte(plain) err := bcrypt.CompareHashAndPassword(hashedBytes, plainBytes) - if err != nil { - return false - } - - return true + return err == nil } diff --git a/server.go b/server.go index b4e95b1..4e16cb3 100644 --- a/server.go +++ b/server.go @@ -98,15 +98,15 @@ func rootActionHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "") } -func healthActionHandler(w http.ResponseWriter, r *http.Request) { +func healthActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "Ok") } -func readyActionHandler(w http.ResponseWriter, r *http.Request) { +func readyActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "Ok") } -func versionActionHandler(w http.ResponseWriter, r *http.Request) { +func versionActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, authServerVersion) } From 1e8264325d5b2ad968942daab762e4c41034ac42 Mon Sep 17 00:00:00 2001 From: reugn Date: Thu, 8 Feb 2024 09:19:28 +0200 Subject: [PATCH 02/14] update dependencies --- auth/jwt.go | 4 +- auth/jwt_generator.go | 6 +-- auth/jwt_validator.go | 4 +- auth/keys.go | 2 +- go.mod | 25 ++++++++----- go.sum | 83 +++++++++++++++++++++++------------------ main.go | 2 +- proxy/parser.go | 2 +- repository/aerospike.go | 2 +- 9 files changed, 72 insertions(+), 58 deletions(-) diff --git a/auth/jwt.go b/auth/jwt.go index 3aa99a2..8e0d52d 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -3,7 +3,7 @@ package auth import ( "encoding/json" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/repository" ) @@ -28,7 +28,7 @@ func (t TokenType) ToString() string { // Claims is the custom JWT claims container. type Claims struct { - jwt.StandardClaims + jwt.RegisteredClaims Username string `json:"user"` Role repository.UserRole `json:"role"` } diff --git a/auth/jwt_generator.go b/auth/jwt_generator.go index 45c32d1..6e5ac3e 100644 --- a/auth/jwt_generator.go +++ b/auth/jwt_generator.go @@ -3,7 +3,7 @@ package auth import ( "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/repository" ) @@ -29,9 +29,9 @@ func (gen *JWTGenerator) Generate(username string, role repository.UserRole) (*A // set standard claims now := time.Now() - claims.IssuedAt = now.Unix() + claims.IssuedAt = jwt.NewNumericDate(now) if env.expireAfter > 0 { - claims.ExpiresAt = now.Add(env.expireAfter).Unix() + claims.ExpiresAt = jwt.NewNumericDate(now.Add(env.expireAfter)) } token.Claims = &claims diff --git a/auth/jwt_validator.go b/auth/jwt_validator.go index dd454cd..3b00008 100644 --- a/auth/jwt_validator.go +++ b/auth/jwt_validator.go @@ -5,7 +5,7 @@ import ( "log" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/repository" ) @@ -39,7 +39,7 @@ func (val *JWTValidator) validateClaims(token *jwt.Token) (*Claims, error) { } // validate expiration - if claims.ExpiresAt < time.Now().Unix() { + if claims.ExpiresAt.Before(time.Now()) { return nil, jwt.ErrTokenExpired } diff --git a/auth/keys.go b/auth/keys.go index 46e2db8..b588a08 100644 --- a/auth/keys.go +++ b/auth/keys.go @@ -5,7 +5,7 @@ import ( "errors" "os" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" ) // Keys represents a container for the private and public keys. diff --git a/go.mod b/go.mod index 4fc7438..b07737b 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,19 @@ module github.com/reugn/auth-server go 1.21 require ( - github.com/aerospike/aerospike-client-go/v6 v6.13.0 - github.com/golang-jwt/jwt/v4 v4.5.0 - github.com/hashicorp/vault/api v1.9.2 - golang.org/x/crypto v0.12.0 - golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 + github.com/aerospike/aerospike-client-go/v7 v7.1.0 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/hashicorp/vault/api v1.11.0 + golang.org/x/crypto v0.18.0 + golang.org/x/time v0.5.0 ) require ( github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/fatih/color v1.13.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-test/deep v1.0.7 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -29,8 +30,12 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/text v0.12.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 67656df..cdc4150 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,34 @@ -github.com/aerospike/aerospike-client-go/v6 v6.13.0 h1:9V5qKtdF2t9hDUKRKU8POUMKtOyw6pkfhHlVI6L32cU= -github.com/aerospike/aerospike-client-go/v6 v6.13.0/go.mod h1:2Syy0n4FKdgJxn0ZCfLfggVdaTXgMaGW6EOlPV6MGG4= +github.com/aerospike/aerospike-client-go/v7 v7.1.0 h1:yvCTKdbpqZxHvv7sWsFHV1j49jZcC8yXRooWsDFqKtA= +github.com/aerospike/aerospike-client-go/v7 v7.1.0/go.mod h1:AkHiKvCbqa1c16gCNGju3c5X/yzwLVvblNczqjxNwNk= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -53,8 +54,8 @@ github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0S github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/hcl v1.0.1-vault-3 h1:V95v5KSTu6DB5huDSKiq4uAfILEuNigK/+qPET6H/Mg= github.com/hashicorp/hcl v1.0.1-vault-3/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= -github.com/hashicorp/vault/api v1.9.2 h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as= -github.com/hashicorp/vault/api v1.9.2/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= +github.com/hashicorp/vault/api v1.11.0 h1:AChWByeHf4/P9sX3Y1B7vFsQhZO2BgQiCMQ2SA1P1UY= +github.com/hashicorp/vault/api v1.11.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= @@ -70,10 +71,10 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= -github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= -github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -83,37 +84,45 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index d95022e..9c5fcf7 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - "github.com/golang-jwt/jwt/v4" + "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/auth" "github.com/reugn/auth-server/proxy" "github.com/reugn/auth-server/repository" diff --git a/proxy/parser.go b/proxy/parser.go index e22c3ef..8d287b7 100644 --- a/proxy/parser.go +++ b/proxy/parser.go @@ -6,7 +6,7 @@ import ( "github.com/reugn/auth-server/repository" ) -// RequestParser is the interface to a custom request parser. +// RequestParser represents a request parser. type RequestParser interface { // ParseAuthorizationToken parses and returns an Authorization token from the original request. diff --git a/repository/aerospike.go b/repository/aerospike.go index 6394c84..b37c3f7 100644 --- a/repository/aerospike.go +++ b/repository/aerospike.go @@ -6,7 +6,7 @@ import ( "os" "strconv" - as "github.com/aerospike/aerospike-client-go/v6" + as "github.com/aerospike/aerospike-client-go/v7" ) type aerospikeEnv struct { From ccffe30ff2ea832b3cdc0192b8cf45ce01012ad5 Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 9 Feb 2024 10:25:34 +0200 Subject: [PATCH 03/14] refactor and enhance service functionality --- .golangci.yml | 7 +++ Dockerfile | 13 +++-- auth/env.go | 40 -------------- auth/jwt_generator.go | 28 +++++++--- auth/jwt_test.go | 96 +++++++++++++++++++++++++++++++++ auth/jwt_validator.go | 25 +++++---- auth/keys.go | 21 ++++++-- config/local_config.yml | 14 +++++ go.mod | 2 + go.sum | 7 +++ main.go | 30 ++++++----- proxy/simple_parser.go | 4 +- proxy/traefik_parser.go | 4 +- repository/aerospike.go | 97 +++++++++++++++------------------- repository/aerospike_test.go | 35 ++++++++++++ repository/local.go | 83 ++++++++++++++++------------- repository/repository.go | 10 ++-- repository/vault.go | 80 ++++++++++++++-------------- repository/vault_test.go | 27 ++++++++++ server.go | 6 +-- util/env/env.go | 40 ++++++++++++++ utils/hash.go => util/utils.go | 9 +++- {utils => util}/utils_test.go | 6 +-- utils/utils.go | 8 --- 24 files changed, 458 insertions(+), 234 deletions(-) delete mode 100644 auth/env.go create mode 100644 auth/jwt_test.go create mode 100644 config/local_config.yml create mode 100644 repository/aerospike_test.go create mode 100644 repository/vault_test.go create mode 100644 util/env/env.go rename utils/hash.go => util/utils.go (67%) rename {utils => util}/utils_test.go (72%) delete mode 100644 utils/utils.go diff --git a/.golangci.yml b/.golangci.yml index 3329eb9..c115364 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,3 +25,10 @@ linters: - unconvert - unparam - unused + +issues: + exclude-rules: + - path: _test\.go + linters: + - unparam + - funlen diff --git a/Dockerfile b/Dockerfile index 4310f84..dccb45d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ -FROM golang:alpine AS build +FROM golang:alpine3.19 AS build RUN apk --no-cache add gcc g++ make git WORKDIR /go/src/app COPY . . RUN go get ./... RUN GOOS=linux go build -ldflags="-s -w" -o ./bin/auth -FROM alpine:3.14 -WORKDIR /go/bin -COPY --from=build /go/src/app/bin /go/bin +FROM alpine:3.19.1 +WORKDIR /app +COPY --from=build /go/src/app/bin /app +COPY --from=build /go/src/app/config/local_config.yml /app/ COPY ./secrets ./secrets +ENV AUTH_SERVER_LOCAL_CONFIG_PATH=local_config.yml + EXPOSE 8081 -ENTRYPOINT ["/go/bin/auth"] \ No newline at end of file +ENTRYPOINT ["/app/auth"] diff --git a/auth/env.go b/auth/env.go deleted file mode 100644 index ff53f79..0000000 --- a/auth/env.go +++ /dev/null @@ -1,40 +0,0 @@ -package auth - -import ( - "os" - "strconv" - "time" -) - -type environmentVariables struct { - expireAfter time.Duration - privateKeyPath string - publicKeyPath string -} - -var env = environmentVariables{time.Hour, "", ""} - -func init() { - // read environment variables - tokenExpirationMilis, ok := os.LookupEnv("AUTH_SERVER_ACCESS_TOKEN_EXPIRATION_MILLIS") - if ok { - expireAfter, err := strconv.Atoi(tokenExpirationMilis) - if err == nil { - env.expireAfter = time.Duration(expireAfter) * time.Millisecond - } - } - - privateKeyPath, ok := os.LookupEnv("AUTH_SERVER_PRIVATE_KEY_PATH") - if ok { - env.privateKeyPath = privateKeyPath - } else { - env.privateKeyPath = "secrets/privkey.pem" - } - - publicKeyPath, ok := os.LookupEnv("AUTH_SERVER_PUBLIC_KEY_PATH") - if ok { - env.publicKeyPath = publicKeyPath - } else { - env.publicKeyPath = "secrets/cert.pem" - } -} diff --git a/auth/jwt_generator.go b/auth/jwt_generator.go index 6e5ac3e..92cbc9f 100644 --- a/auth/jwt_generator.go +++ b/auth/jwt_generator.go @@ -5,22 +5,34 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/util/env" +) + +const ( + envTokenExpireAfterMillis = "AUTH_SERVER_ACCESS_TOKEN_EXPIRATION_MILLIS" ) // JWTGenerator generates an AccessToken. type JWTGenerator struct { - Keys *Keys - SigningMethod jwt.SigningMethod + keys *Keys + signingMethod jwt.SigningMethod + tokenExpireAfter time.Duration } // NewJWTGenerator returns a new instance of JWTGenerator. func NewJWTGenerator(keys *Keys, signingMethod jwt.SigningMethod) *JWTGenerator { - return &JWTGenerator{keys, signingMethod} + tokenExpireAfter := time.Hour // default 1 hour + env.ReadTime(&tokenExpireAfter, envTokenExpireAfterMillis, time.Millisecond) + return &JWTGenerator{ + keys: keys, + signingMethod: signingMethod, + tokenExpireAfter: tokenExpireAfter, + } } // Generate generates an AccessToken using the username and role claims. func (gen *JWTGenerator) Generate(username string, role repository.UserRole) (*AccessToken, error) { - token := jwt.New(gen.SigningMethod) + token := jwt.New(gen.signingMethod) claims := Claims{} // set custom claims @@ -30,12 +42,12 @@ func (gen *JWTGenerator) Generate(username string, role repository.UserRole) (*A // set standard claims now := time.Now() claims.IssuedAt = jwt.NewNumericDate(now) - if env.expireAfter > 0 { - claims.ExpiresAt = jwt.NewNumericDate(now.Add(env.expireAfter)) + if gen.tokenExpireAfter > 0 { + claims.ExpiresAt = jwt.NewNumericDate(now.Add(gen.tokenExpireAfter)) } token.Claims = &claims - signed, err := token.SignedString(gen.Keys.PrivateKey) + signed, err := token.SignedString(gen.keys.privateKey) if err != nil { return nil, err } @@ -44,7 +56,7 @@ func (gen *JWTGenerator) Generate(username string, role repository.UserRole) (*A accessToken := &AccessToken{ signed, BearerToken.ToString(), - env.expireAfter.Milliseconds(), + gen.tokenExpireAfter.Milliseconds(), } return accessToken, nil diff --git a/auth/jwt_test.go b/auth/jwt_test.go new file mode 100644 index 0000000..0cb11b7 --- /dev/null +++ b/auth/jwt_test.go @@ -0,0 +1,96 @@ +package auth + +import ( + "os" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/reugn/auth-server/repository" +) + +const up = "../" + +func TestJWT_Authorize(t *testing.T) { + os.Setenv(repository.EnvLocalConfigPath, up+repository.DefaultLocalConfigPath) + repo, err := repository.NewLocal() + if err != nil { + t.Fatal(err) + } + os.Setenv(envPrivateKeyPath, up+defaultPrivateKeyPath) + os.Setenv(envPublicKeyPath, up+defaultPublicKeyPath) + keys, err := NewKeys() + if err != nil { + t.Fatal(err) + } + tokenGenerator := NewJWTGenerator(keys, jwt.SigningMethodRS256) + tokenValidator := NewJWTValidator(keys, repo) + + tests := []struct { + name string + username string + password string + request repository.RequestDetails + authorized bool + }{ + { + "configured-uri", + "admin", + "1234", + repository.RequestDetails{ + Method: "GET", + URI: "/health", + }, + true, + }, + { + "unknown-uri", + "admin", + "1234", + repository.RequestDetails{ + Method: "GET", + URI: "/health2", + }, + false, + }, + { + "invalid-user", + "admin2", + "1234", + repository.RequestDetails{ + Method: "GET", + URI: "/health", + }, + false, + }, + { + "invalid-password", + "admin", + "1111", + repository.RequestDetails{ + Method: "GET", + URI: "/health", + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userDetails := repo.AuthenticateBasic(tt.username, tt.password) + if userDetails == nil { + if tt.authorized { + t.Fatal("authentication failed") + } else { + return + } + } + token, err := tokenGenerator.Generate(tt.username, userDetails.UserRole) + if err != nil { + t.Fatal(err) + } + authorized := tokenValidator.Authorize(token.Token, &tt.request) + if authorized != tt.authorized { + t.Fatal("authorization result mismatch") + } + }) + } +} diff --git a/auth/jwt_validator.go b/auth/jwt_validator.go index 3b00008..ad9ee41 100644 --- a/auth/jwt_validator.go +++ b/auth/jwt_validator.go @@ -11,28 +11,31 @@ import ( // JWTValidator validates and authorizes an AccessToken. type JWTValidator struct { - keys *Keys - repo repository.Repository + keys *Keys + backend repository.Repository } // NewJWTValidator returns a new instance of JWTValidator. -func NewJWTValidator(key *Keys, repo repository.Repository) *JWTValidator { - return &JWTValidator{key, repo} +func NewJWTValidator(keys *Keys, backend repository.Repository) *JWTValidator { + return &JWTValidator{ + keys: keys, + backend: backend, + } } // validate validates the AccessToken. -func (val *JWTValidator) validate(jtwToken string) (*Claims, error) { +func (v *JWTValidator) validate(jtwToken string) (*Claims, error) { token, err := jwt.Parse(jtwToken, func(token *jwt.Token) (interface{}, error) { - return val.keys.PublicKey, nil + return v.keys.publicKey, nil }) if err != nil { return nil, err } - return val.validateClaims(token) + return v.validateClaims(token) } -func (val *JWTValidator) validateClaims(token *jwt.Token) (*Claims, error) { +func (v *JWTValidator) validateClaims(token *jwt.Token) (*Claims, error) { claims, err := getClaims(token) if err != nil { return nil, err @@ -63,12 +66,12 @@ func getClaims(token *jwt.Token) (*Claims, error) { } // Authorize validates the token and authorizes the actual request. -func (val *JWTValidator) Authorize(token string, request *repository.RequestDetails) bool { - claims, err := val.validate(token) +func (v *JWTValidator) Authorize(token string, request *repository.RequestDetails) bool { + claims, err := v.validate(token) if err != nil { log.Println(err.Error()) return false } - return val.repo.AuthorizeRequest(claims.Role, *request) + return v.backend.AuthorizeRequest(claims.Role, *request) } diff --git a/auth/keys.go b/auth/keys.go index b588a08..2144c4b 100644 --- a/auth/keys.go +++ b/auth/keys.go @@ -6,17 +6,32 @@ import ( "os" "github.com/golang-jwt/jwt/v5" + "github.com/reugn/auth-server/util/env" +) + +const ( + envPrivateKeyPath = "AUTH_SERVER_PRIVATE_KEY_PATH" + envPublicKeyPath = "AUTH_SERVER_PUBLIC_KEY_PATH" + + defaultPrivateKeyPath = "secrets/privkey.pem" + defaultPublicKeyPath = "secrets/cert.pem" ) // Keys represents a container for the private and public keys. type Keys struct { - PrivateKey *rsa.PrivateKey - PublicKey *rsa.PublicKey + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey } // NewKeys returns a new instance of Keys. func NewKeys() (*Keys, error) { - return NewKeysFromFile(env.privateKeyPath, env.publicKeyPath) + privateKeyPath := defaultPrivateKeyPath + env.ReadString(&privateKeyPath, envPrivateKeyPath) + + publicKeyPath := defaultPublicKeyPath + env.ReadString(&publicKeyPath, envPublicKeyPath) + + return NewKeysFromFile(privateKeyPath, publicKeyPath) } // NewKeysFromFile creates and returns a new instance of Keys from the files. diff --git a/config/local_config.yml b/config/local_config.yml new file mode 100644 index 0000000..11f1101 --- /dev/null +++ b/config/local_config.yml @@ -0,0 +1,14 @@ +--- +users: + admin: + password: 1234 + role: admin + +roles: + admin: + - method: "GET" + uri: "/dashboard" + - method: "POST" + uri: "/auth" + - method: "GET" + uri: "/health" diff --git a/go.mod b/go.mod index b07737b..626b577 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/hashicorp/vault/api v1.11.0 golang.org/x/crypto v0.18.0 golang.org/x/time v0.5.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -25,6 +26,7 @@ require ( github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.1-vault-3 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index cdc4150..54d3dac 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -56,6 +57,10 @@ github.com/hashicorp/hcl v1.0.1-vault-3 h1:V95v5KSTu6DB5huDSKiq4uAfILEuNigK/+qPE github.com/hashicorp/hcl v1.0.1-vault-3/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.11.0 h1:AChWByeHf4/P9sX3Y1B7vFsQhZO2BgQiCMQ2SA1P1UY= github.com/hashicorp/vault/api v1.11.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= @@ -124,6 +129,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 9c5fcf7..a2156f2 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,14 @@ package main import ( "flag" + "fmt" + "strings" "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/auth" "github.com/reugn/auth-server/proxy" "github.com/reugn/auth-server/repository" - "github.com/reugn/auth-server/utils" + "github.com/reugn/auth-server/util" ) const authServerVersion = "0.3.1" @@ -25,7 +27,7 @@ func main() { // load ssl keys keys, err := auth.NewKeys() - utils.Check(err) + util.Check(err) // start http server server := NewHTTPServer(*serverHostParam, *serverPortParam, keys) @@ -34,7 +36,7 @@ func main() { func parseAlgo() *jwt.SigningMethodRSA { var signingMethodRSA *jwt.SigningMethodRSA - switch *algoParam { + switch strings.ToUpper(*algoParam) { case "RS256": signingMethodRSA = jwt.SigningMethodRS256 case "RS384": @@ -42,35 +44,39 @@ func parseAlgo() *jwt.SigningMethodRSA { case "RS512": signingMethodRSA = jwt.SigningMethodRS512 default: - panic("Invalid signing method") + panic(fmt.Sprintf("Unsupported signing method: %s", *algoParam)) } return signingMethodRSA } func parseProxy() proxy.RequestParser { var parser proxy.RequestParser - switch *proxyParam { + switch strings.ToLower(*proxyParam) { case "simple": parser = proxy.NewSimpleParser() case "traefik": parser = proxy.NewTraefikParser() default: - panic("Invalid proxy provider") + panic(fmt.Sprintf("Unsupported proxy provider: %s", *proxyParam)) } return parser } -func parseRepo() repository.Repository { +func parseRepository() repository.Repository { var repo repository.Repository var err error - switch *repoParam { + switch strings.ToLower(*repoParam) { case "local": - repo = repository.NewLocalRepo() + repo, err = repository.NewLocal() + util.Check(err) case "aerospike": - repo, err = repository.NewAerospikeRepositoryFromEnv() - utils.Check(err) + repo, err = repository.NewAerospike() + util.Check(err) + case "vault": + repo, err = repository.NewVault() + util.Check(err) default: - panic("Invalid repository provider") + panic(fmt.Sprintf("Unsupported storage provider: %s", *repoParam)) } return repo } diff --git a/proxy/simple_parser.go b/proxy/simple_parser.go index 6a80562..da20f07 100644 --- a/proxy/simple_parser.go +++ b/proxy/simple_parser.go @@ -10,7 +10,9 @@ import ( // SimpleParser implements the RequestParser interface. type SimpleParser struct{} -// NewSimpleParser returns a new instance of SimpleParser. +var _ RequestParser = (*SimpleParser)(nil) + +// NewSimpleParser returns a new SimpleParser. func NewSimpleParser() *SimpleParser { return &SimpleParser{} } diff --git a/proxy/traefik_parser.go b/proxy/traefik_parser.go index 87e0c95..4c92900 100644 --- a/proxy/traefik_parser.go +++ b/proxy/traefik_parser.go @@ -10,7 +10,9 @@ import ( // TraefikParser implements the RequestParser interface. type TraefikParser struct{} -// NewTraefikParser returns a new instance of TraefikParser. +var _ RequestParser = (*TraefikParser)(nil) + +// NewTraefikParser returns a new TraefikParser. func NewTraefikParser() *TraefikParser { return &TraefikParser{} } diff --git a/repository/aerospike.go b/repository/aerospike.go index b37c3f7..4761a29 100644 --- a/repository/aerospike.go +++ b/repository/aerospike.go @@ -1,15 +1,24 @@ package repository import ( - "fmt" "log" - "os" - "strconv" as "github.com/aerospike/aerospike-client-go/v7" + "github.com/reugn/auth-server/util/env" ) -type aerospikeEnv struct { +// Environment variables to configure AerospikeRepository. +const ( + envAerospikeHost = "AUTH_SERVER_AEROSPIKE_HOST" + envAerospikePort = "AUTH_SERVER_AEROSPIKE_PORT" + envAerospikeNamespace = "AUTH_SERVER_AEROSPIKE_NAMESPACE" + envAerospikeSet = "AUTH_SERVER_AEROSPIKE_SETNAME" + envAerospikeBasicKey = "AUTH_SERVER_AEROSPIKE_BASIC_KEY" + envAerospikeAuthKey = "AUTH_SERVER_AEROSPIKE_AUTHORIZATION_KEY" +) + +// aerospikeConfig contains AerospikeRepository configuration properties. +type aerospikeConfig struct { hostname string port int namespase string @@ -18,17 +27,20 @@ type aerospikeEnv struct { authorizationKey string } -// AerospikeRepository implements the Repository interface backed by Aerospike Database. +// AerospikeRepository implements the Repository interface using Aerospike Database +// as the storage backend. type AerospikeRepository struct { client *as.Client - env aerospikeEnv + config aerospikeConfig baseKey *as.Key authKey *as.Key } -func getAerospikeEnv() aerospikeEnv { +var _ Repository = (*AerospikeRepository)(nil) + +func getAerospikeConfig() aerospikeConfig { // set defaults - env := aerospikeEnv{ + config := aerospikeConfig{ hostname: "localhost", port: 3000, namespase: "test", @@ -37,65 +49,43 @@ func getAerospikeEnv() aerospikeEnv { authorizationKey: "authorization", } - hostname, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_HOST") - if ok { - env.hostname = hostname - } - port, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_PORT") - if ok { - iport, err := strconv.Atoi(port) - if err == nil { - env.port = iport - } - } - namespace, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_NAMESPACE") - if ok { - env.namespase = namespace - } - setName, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_SETNAME") - if ok { - env.setName = setName - } - basicKey, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_BASIC_KEY") - if ok { - env.basicAuthKey = basicKey - } - authKey, ok := os.LookupEnv("AUTH_SERVER_AEROSPIKE_AUTHORIZATION_KEY") - if ok { - env.authorizationKey = authKey - } + // read configuration from environment variables + env.ReadString(&config.hostname, envAerospikeHost) + env.ReadInt(&config.port, envAerospikePort) + env.ReadString(&config.namespase, envAerospikeNamespace) + env.ReadString(&config.setName, envAerospikeSet) + env.ReadString(&config.basicAuthKey, envAerospikeBasicKey) + env.ReadString(&config.authorizationKey, envAerospikeAuthKey) - return env + return config } -// NewAerospikeRepositoryFromEnv returns a new instance of AerospikeRepository using env configuration. -func NewAerospikeRepositoryFromEnv() (*AerospikeRepository, error) { - env := getAerospikeEnv() - client, err := as.NewClient(env.hostname, env.port) +// NewAerospike returns a new AerospikeRepository using environment variables for configuration. +func NewAerospike() (*AerospikeRepository, error) { + config := getAerospikeConfig() // read configuration + client, err := as.NewClient(config.hostname, config.port) if err != nil { return nil, err } - - baseKey, err := as.NewKey(env.namespase, env.setName, env.basicAuthKey) + baseKey, err := as.NewKey(config.namespase, config.setName, config.basicAuthKey) if err != nil { return nil, err } - - authKey, err := as.NewKey(env.namespase, env.setName, env.authorizationKey) + authKey, err := as.NewKey(config.namespase, config.setName, config.authorizationKey) if err != nil { return nil, err } return &AerospikeRepository{ client: client, - env: env, + config: config, baseKey: baseKey, authKey: authKey, }, nil } // AuthenticateBasic validates the basic username and password before issuing a JWT. -// Uses the bcrypt password-hashing function to validate the password. +// It uses the bcrypt password-hashing function to validate the password. func (aero *AerospikeRepository) AuthenticateBasic(username string, password string) *UserDetails { record, err := aero.client.Get(nil, aero.baseKey, username) if err != nil { @@ -103,13 +93,10 @@ func (aero *AerospikeRepository) AuthenticateBasic(username string, password str return nil } - // Bin(user1: {username: user1, password: sha256, role: 1}) + // Bin(user1: {username: user1, password: sha256, role: admin}) userBin := record.Bins[username].(map[string]interface{}) - if hashed, ok := userBin["password"].(string); ok { - if !pwdMatch(hashed, password) { - return nil - } - } else { + hashed, ok := userBin["password"].(string) + if !ok || !pwdMatch(hashed, password) { return nil } @@ -121,13 +108,13 @@ func (aero *AerospikeRepository) AuthenticateBasic(username string, password str // AuthorizeRequest checks if the role has permissions to access the endpoint. func (aero *AerospikeRepository) AuthorizeRequest(userRole UserRole, request RequestDetails) bool { - record, err := aero.client.Get(nil, aero.authKey, fmt.Sprint(userRole)) + record, err := aero.client.Get(nil, aero.authKey, string(userRole)) if err != nil { log.Println(err.Error()) return false } - // Bin(1: [{method: GET, uri: /health}]) - scopes := record.Bins[fmt.Sprint(userRole)].([]map[string]string) + // Bin(admin: [{method: GET, uri: /health}]) + scopes := record.Bins[string(userRole)].([]map[string]string) return isAuthorizedRequest(scopes, request) } diff --git a/repository/aerospike_test.go b/repository/aerospike_test.go new file mode 100644 index 0000000..ffc98fd --- /dev/null +++ b/repository/aerospike_test.go @@ -0,0 +1,35 @@ +package repository + +import ( + "os" + "testing" +) + +func Test_getAerospikeConfig(t *testing.T) { + os.Setenv(envAerospikeHost, "127.0.0.1") + os.Setenv(envAerospikePort, "3300") + os.Setenv(envAerospikeNamespace, "test1") + os.Setenv(envAerospikeSet, "set1") + os.Setenv(envAerospikeBasicKey, "basic1") + os.Setenv(envAerospikeAuthKey, "authorization1") + + config := getAerospikeConfig() + if config.hostname != "127.0.0.1" { + t.Fail() + } + if config.port != 3300 { + t.Fail() + } + if config.namespase != "test1" { + t.Fail() + } + if config.setName != "set1" { + t.Fail() + } + if config.basicAuthKey != "basic1" { + t.Fail() + } + if config.authorizationKey != "authorization1" { + t.Fail() + } +} diff --git a/repository/local.go b/repository/local.go index 158f2d5..90e18a3 100644 --- a/repository/local.go +++ b/repository/local.go @@ -1,49 +1,58 @@ package repository -// User model for the internal Local repository use. -type User struct { - password string - role UserRole +import ( + "os" + + "github.com/reugn/auth-server/util/env" + "gopkg.in/yaml.v3" +) + +const ( + EnvLocalConfigPath = "AUTH_SERVER_LOCAL_CONFIG_PATH" + DefaultLocalConfigPath = "config/local_config.yml" +) + +// AuthDetails contains authentication details for the user. +type AuthDetails struct { + Password string `yaml:"password"` + Role UserRole `yaml:"role"` } -// Local implements the Repository interface backed by in-memory permission details. -// Use it for test purposes only. +// Local implements the Repository interface by loading authentication details from +// a local configuration file. type Local struct { - Users map[string]User - Permissions map[UserRole][]RequestDetails + Users map[string]AuthDetails `yaml:"users"` + Roles map[UserRole][]RequestDetails `yaml:"roles"` } -// NewLocalRepo returns a new instance of the Local repo. -func NewLocalRepo() *Local { - users := make(map[string]User) - users["admin"] = User{"1234", 1} +var _ Repository = (*Local)(nil) + +// NewLocal returns a new Local repository using an environment variable to +// read a custom path to the configuration file. +func NewLocal() (*Local, error) { + configPath := DefaultLocalConfigPath + env.ReadString(&configPath, EnvLocalConfigPath) - perms := make(map[UserRole][]RequestDetails) - requestDetails := []RequestDetails{ - { - "GET", - "/dashboard", - }, - { - "POST", - "/auth", - }, - { - "GET", - "/health", - }, + data, err := os.ReadFile(configPath) + if err != nil { + return nil, err } - perms[1] = requestDetails - return &Local{users, perms} + + localRepository := &Local{} + if err = yaml.Unmarshal(data, localRepository); err != nil { + return nil, err + } + + return localRepository, nil } // AuthenticateBasic validates the basic username and password before issuing a JWT. func (local *Local) AuthenticateBasic(username string, password string) *UserDetails { - if user, ok := local.Users[username]; ok { - if user.password == password { + if authDetails, ok := local.Users[username]; ok { + if authDetails.Password == password { return &UserDetails{ - username, - user.role, + UserName: username, + UserRole: authDetails.Role, } } } @@ -51,18 +60,18 @@ func (local *Local) AuthenticateBasic(username string, password string) *UserDet } // AuthorizeRequest checks if the role has permissions to access the endpoint. -func (local *Local) AuthorizeRequest(userRole UserRole, request RequestDetails) bool { - if perms, ok := local.Permissions[userRole]; ok { - if containsRequestDetails(perms, request) { +func (local *Local) AuthorizeRequest(userRole UserRole, requestDetails RequestDetails) bool { + if permissions, ok := local.Roles[userRole]; ok { + if containsRequestDetails(permissions, requestDetails) { return true } } return false } -func containsRequestDetails(details []RequestDetails, rd RequestDetails) bool { +func containsRequestDetails(details []RequestDetails, requestDetails RequestDetails) bool { for _, detail := range details { - if detail == rd { + if detail == requestDetails { return true } } diff --git a/repository/repository.go b/repository/repository.go index ba68be3..e15c358 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -7,7 +7,7 @@ import ( ) // UserRole represents a user role. -type UserRole int +type UserRole string // UserDetails represents user details. type UserDetails struct { @@ -17,11 +17,11 @@ type UserDetails struct { // RequestDetails represents request details. type RequestDetails struct { - Method string - URI string + Method string `yaml:"method"` + URI string `yaml:"uri"` } -// Repository is the interface to a custom authentication/authorization backend facade. +// Repository represents an authentication/authorization backend facade. type Repository interface { // AuthenticateBasic validates the basic username and password before issuing a JWT. @@ -45,7 +45,7 @@ func isAuthorizedRequest(scopes []map[string]string, request RequestDetails) boo func hashAndSalt(pwd string) ([]byte, error) { bytePwd := []byte(pwd) - // Use bcrypt.GenerateFromPassword to hash and salt the password. + // use bcrypt.GenerateFromPassword to hash and salt the password hash, err := bcrypt.GenerateFromPassword(bytePwd, bcrypt.MinCost) if err != nil { return nil, err diff --git a/repository/vault.go b/repository/vault.go index 5c2a578..f56cc22 100644 --- a/repository/vault.go +++ b/repository/vault.go @@ -1,86 +1,84 @@ package repository import ( + "fmt" "log" - "os" - "strconv" "github.com/hashicorp/vault/api" + "github.com/reugn/auth-server/util/env" ) -type vaultEnv struct { +// Environment variables to configure VaultRepository. +const ( + envVaultAddr = "AUTH_SERVER_VAULT_ADDR" + envVaultToken = "AUTH_SERVER_VAULT_TOKEN" + envVaultBasicKey = "AUTH_SERVER_VAULT_BASIC_KEY" + envVaultAuthKey = "AUTH_SERVER_VAULT_AUTHORIZATION_KEY" +) + +// vaultConfig contains VaultRepository configuration properties. +type vaultConfig struct { vaultAddr string vaultToken string basicAuthKeyPrefix string authorizationKeyPrefix string } -// VaultRepository implements the Repository interface backed by HashiCorp Vault. +// VaultRepository implements the Repository interface using HashiCorp Vault +// as the storage backend. type VaultRepository struct { client *api.Client - env vaultEnv + config vaultConfig } -func getVaultEnv() vaultEnv { +var _ Repository = (*VaultRepository)(nil) + +func getVaultConfig() vaultConfig { // set defaults - env := vaultEnv{ + config := vaultConfig{ vaultAddr: "localhost:8200", basicAuthKeyPrefix: "secret/basic", authorizationKeyPrefix: "secret/authorization", } - vaultAddr, ok := os.LookupEnv("AUTH_SERVER_VAULT_ADDR") - if ok { - env.vaultAddr = vaultAddr - } - vaultToken, ok := os.LookupEnv("AUTH_SERVER_VAULT_TOKEN") - if ok { - env.vaultToken = vaultToken - } - basicKey, ok := os.LookupEnv("AUTH_SERVER_VAULT_BASIC_KEY") - if ok { - env.basicAuthKeyPrefix = basicKey - } - authKey, ok := os.LookupEnv("AUTH_SERVER_VAULT_AUTHORIZATION_KEY") - if ok { - env.authorizationKeyPrefix = authKey - } + // read configuration from environment variables + env.ReadString(&config.vaultAddr, envVaultAddr) + env.ReadString(&config.vaultToken, envVaultToken) + env.ReadString(&config.basicAuthKeyPrefix, envVaultBasicKey) + env.ReadString(&config.authorizationKeyPrefix, envVaultAuthKey) - return env + return config } -// NewVaultRepositoryFromEnv returns a new instance of VaultRepository using env configuration. -func NewVaultRepositoryFromEnv() (*VaultRepository, error) { - env := getVaultEnv() - config := &api.Config{ - Address: env.vaultAddr, +// NewVault returns a new VaultRepository using environment variables for configuration. +func NewVault() (*VaultRepository, error) { + config := getVaultConfig() // read configuration + apiConfig := &api.Config{ + Address: config.vaultAddr, } - client, err := api.NewClient(config) + client, err := api.NewClient(apiConfig) if err != nil { return nil, err } - client.SetToken(env.vaultToken) + client.SetToken(config.vaultToken) return &VaultRepository{ client: client, - env: env, + config: config, }, nil } // AuthenticateBasic validates the basic username and password before issuing a JWT. -// Uses the bcrypt password-hashing function to validate the password. +// It uses the bcrypt password-hashing function to validate the password. func (vr *VaultRepository) AuthenticateBasic(username string, password string) *UserDetails { - secret, err := vr.client.Logical().Read(vr.env.basicAuthKeyPrefix + "/" + username) + secret, err := vr.client.Logical().Read(vr.config.basicAuthKeyPrefix + "/" + username) if err != nil { log.Println(err.Error()) return nil } - if hashed, ok := secret.Data["password"].(string); ok { - if !pwdMatch(hashed, password) { - return nil - } - } else { + hashed, ok := secret.Data["password"].(string) + if !ok || !pwdMatch(hashed, password) { return nil } @@ -92,7 +90,7 @@ func (vr *VaultRepository) AuthenticateBasic(username string, password string) * // AuthorizeRequest checks if the role has permissions to access the endpoint. func (vr *VaultRepository) AuthorizeRequest(userRole UserRole, request RequestDetails) bool { - secret, err := vr.client.Logical().Read(vr.env.authorizationKeyPrefix + "/" + strconv.Itoa(int(userRole))) + secret, err := vr.client.Logical().Read(fmt.Sprintf("%s/%s", vr.config.authorizationKeyPrefix, userRole)) if err != nil { log.Println(err.Error()) return false @@ -100,7 +98,7 @@ func (vr *VaultRepository) AuthorizeRequest(userRole UserRole, request RequestDe scopes, ok := secret.Data["scopes"].([]map[string]string) if !ok { - log.Printf("VaultRepository: error on reading scopes for %d", userRole) + log.Printf("VaultRepository: error on reading scopes for: %s", userRole) return false } diff --git a/repository/vault_test.go b/repository/vault_test.go new file mode 100644 index 0000000..3cca4c8 --- /dev/null +++ b/repository/vault_test.go @@ -0,0 +1,27 @@ +package repository + +import ( + "os" + "testing" +) + +func Test_getVaultConfig(t *testing.T) { + os.Setenv(envVaultAddr, "127.0.0.1:8200") + os.Setenv(envVaultToken, "token1") + os.Setenv(envVaultBasicKey, "secret/basic1") + os.Setenv(envVaultAuthKey, "secret/authorization1") + + config := getVaultConfig() + if config.vaultAddr != "127.0.0.1:8200" { + t.Fail() + } + if config.vaultToken != "token1" { + t.Fail() + } + if config.basicAuthKeyPrefix != "secret/basic1" { + t.Fail() + } + if config.authorizationKeyPrefix != "secret/authorization1" { + t.Fail() + } +} diff --git a/server.go b/server.go index 4e16cb3..0f278c3 100644 --- a/server.go +++ b/server.go @@ -9,7 +9,7 @@ import ( "github.com/reugn/auth-server/auth" "github.com/reugn/auth-server/proxy" "github.com/reugn/auth-server/repository" - "github.com/reugn/auth-server/utils" + "github.com/reugn/auth-server/util" ) var rateLimiter = NewIPRateLimiter(1, 10) @@ -30,7 +30,7 @@ type HTTPServer struct { // NewHTTPServer returns a new instance of HTTPServer. func NewHTTPServer(host string, port int, keys *auth.Keys) *HTTPServer { addr := host + ":" + strconv.Itoa(port) - repository := parseRepo() + repository := parseRepository() generator := auth.NewJWTGenerator(keys, parseAlgo()) validator := auth.NewJWTValidator(keys, repository) @@ -88,7 +88,7 @@ func (ws *HTTPServer) start() { mux.HandleFunc("/auth", ws.authActionHandler) err := http.ListenAndServe(ws.addr, rateLimiterMiddleware(mux)) - utils.Check(err) + util.Check(err) } func rootActionHandler(w http.ResponseWriter, r *http.Request) { diff --git a/util/env/env.go b/util/env/env.go new file mode 100644 index 0000000..d2be45c --- /dev/null +++ b/util/env/env.go @@ -0,0 +1,40 @@ +package env + +import ( + "os" + "strconv" + "time" +) + +// ReadString retrieves the string value of the environment variable named +// by the key. +func ReadString(value *string, key string) { + envValue, ok := os.LookupEnv(key) + if ok { + *value = envValue + } +} + +// ReadInt retrieves the integer value of the environment variable named +// by the key. +func ReadInt(value *int, key string) { + envValue, ok := os.LookupEnv(key) + if ok { + intValue, err := strconv.Atoi(envValue) + if err == nil { + *value = intValue + } + } +} + +// ReadTime retrieves the time value of the environment variable named +// by the key. +func ReadTime(value *time.Duration, key string, timeUnit time.Duration) { + envValue, ok := os.LookupEnv(key) + if ok { + intValue, err := strconv.Atoi(envValue) + if err == nil { + *value = time.Duration(intValue) * timeUnit + } + } +} diff --git a/utils/hash.go b/util/utils.go similarity index 67% rename from utils/hash.go rename to util/utils.go index 07d649d..d4473d4 100644 --- a/utils/hash.go +++ b/util/utils.go @@ -1,4 +1,4 @@ -package utils +package util import ( "crypto/sha256" @@ -10,3 +10,10 @@ func Sha256(str string) string { sha256pwd := sha256.Sum256([]byte(str)) return fmt.Sprintf("%x", sha256pwd) } + +// Check panics if the error is not nil. +func Check(e error) { + if e != nil { + panic(e) + } +} diff --git a/utils/utils_test.go b/util/utils_test.go similarity index 72% rename from utils/utils_test.go rename to util/utils_test.go index 45d7b66..90ac318 100644 --- a/utils/utils_test.go +++ b/util/utils_test.go @@ -1,14 +1,14 @@ -package utils_test +package util_test import ( "reflect" "testing" - "github.com/reugn/auth-server/utils" + "github.com/reugn/auth-server/util" ) func TestSha256(t *testing.T) { - assertEqual(t, "03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", utils.Sha256("1234")) + assertEqual(t, "03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", util.Sha256("1234")) } func assertEqual(t *testing.T, a interface{}, b interface{}) { diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index 4066057..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,8 +0,0 @@ -package utils - -// Check panics if the error is not nil. -func Check(e error) { - if e != nil { - panic(e) - } -} From 297edbc0c2035fc08e8cac88297e17cf03c05a59 Mon Sep 17 00:00:00 2001 From: reugn Date: Thu, 22 Feb 2024 09:37:42 +0200 Subject: [PATCH 04/14] reorganize package architecture --- Dockerfile | 14 +- README.md | 2 +- cmd/auth/main.go | 73 +++++++++ ...config.yml => local_repository_config.yml} | 0 config/service_config.yml | 14 ++ go.mod | 15 +- go.sum | 44 +++-- {auth => internal/auth}/jwt.go | 2 +- {auth => internal/auth}/jwt_generator.go | 4 +- {auth => internal/auth}/jwt_test.go | 10 +- {auth => internal/auth}/jwt_validator.go | 4 +- {auth => internal/auth}/keys.go | 6 +- internal/config/http_server.go | 68 ++++++++ internal/config/logger.go | 99 ++++++++++++ internal/config/service.go | 114 +++++++++++++ .../http/rate_limiter.go | 67 +++++++- internal/http/server.go | 152 ++++++++++++++++++ {proxy => internal/proxy}/parser.go | 2 +- {proxy => internal/proxy}/simple_parser.go | 2 +- {proxy => internal/proxy}/traefik_parser.go | 2 +- .../repository}/aerospike.go | 2 +- .../repository}/aerospike_test.go | 0 {repository => internal/repository}/local.go | 4 +- .../repository}/repository.go | 3 +- {repository => internal/repository}/vault.go | 2 +- .../repository}/vault_test.go | 0 {util => internal/util}/env/env.go | 0 {util => internal/util}/utils.go | 0 {util => internal/util}/utils_test.go | 2 +- main.go | 82 ---------- server.go | 142 ---------------- 31 files changed, 659 insertions(+), 272 deletions(-) create mode 100644 cmd/auth/main.go rename config/{local_config.yml => local_repository_config.yml} (100%) create mode 100644 config/service_config.yml rename {auth => internal/auth}/jwt.go (95%) rename {auth => internal/auth}/jwt_generator.go (93%) rename {auth => internal/auth}/jwt_test.go (86%) rename {auth => internal/auth}/jwt_validator.go (92%) rename {auth => internal/auth}/keys.go (93%) create mode 100644 internal/config/http_server.go create mode 100644 internal/config/logger.go create mode 100644 internal/config/service.go rename rate_limiter.go => internal/http/rate_limiter.go (51%) create mode 100644 internal/http/server.go rename {proxy => internal/proxy}/parser.go (88%) rename {proxy => internal/proxy}/simple_parser.go (94%) rename {proxy => internal/proxy}/traefik_parser.go (95%) rename {repository => internal/repository}/aerospike.go (98%) rename {repository => internal/repository}/aerospike_test.go (100%) rename {repository => internal/repository}/local.go (94%) rename {repository => internal/repository}/repository.go (95%) rename {repository => internal/repository}/vault.go (98%) rename {repository => internal/repository}/vault_test.go (100%) rename {util => internal/util}/env/env.go (100%) rename {util => internal/util}/utils.go (100%) rename {util => internal/util}/utils_test.go (87%) delete mode 100644 main.go delete mode 100644 server.go diff --git a/Dockerfile b/Dockerfile index dccb45d..f004ec9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,18 @@ RUN apk --no-cache add gcc g++ make git WORKDIR /go/src/app COPY . . RUN go get ./... +WORKDIR /go/src/app/cmd/auth RUN GOOS=linux go build -ldflags="-s -w" -o ./bin/auth FROM alpine:3.19.1 WORKDIR /app -COPY --from=build /go/src/app/bin /app -COPY --from=build /go/src/app/config/local_config.yml /app/ +COPY --from=build /go/src/app/cmd/auth/bin /app +COPY --from=build /go/src/app/config /app/ COPY ./secrets ./secrets -ENV AUTH_SERVER_LOCAL_CONFIG_PATH=local_config.yml -EXPOSE 8081 -ENTRYPOINT ["/app/auth"] +ENV AUTH_SERVER_LOCAL_CONFIG_PATH=local_repository_config.yml +ENV AUTH_SERVER_PRIVATE_KEY_PATH=secrets/privkey.pem +ENV AUTH_SERVER_PUBLIC_KEY_PATH=secrets/cert.pem + +EXPOSE 8080 +ENTRYPOINT ["/app/auth", "-c", "service_config.yml"] diff --git a/README.md b/README.md index fffe092..dbd9d55 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/reugn/auth-server)](https://goreportcard.com/report/github.com/reugn/auth-server) This project provides tools to set up a custom authentication and authorization server. -`auth-server` can act as a proxy middleware or be configured in a stand-alone mode. It doesn't require any third-party software integration. Use one of the [available repositories](./repository) to configure backend storage, or implement one of your own. +`auth-server` can act as a proxy middleware or be configured in a stand-alone mode. It doesn't require any third-party software integration. Use one of the [available repositories](internal/repository) to configure backend storage, or implement one of your own. **Note:** This project has not yet passed security testing. Make sure you know what you are doing when setting up your own OAuth2 provider. diff --git a/cmd/auth/main.go b/cmd/auth/main.go new file mode 100644 index 0000000..54cb0dd --- /dev/null +++ b/cmd/auth/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/reugn/auth-server/internal/auth" + "github.com/reugn/auth-server/internal/config" + "github.com/reugn/auth-server/internal/http" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +const ( + version = "0.4.0" +) + +func run() int { + rootCmd := &cobra.Command{ + Short: "Authentication and authorization service", + Version: version, + } + + var configFilePath string + rootCmd.Flags().StringVarP(&configFilePath, "config", "c", "config.yaml", "configuration file path") + + rootCmd.RunE = func(_ *cobra.Command, _ []string) error { + // load ssl keys + keys, err := auth.NewKeys() + if err != nil { + return err + } + // read configuration file + config, err := readConfiguration(configFilePath) + if err != nil { + return err + } + // set default logger + slogHandler, err := config.Logger.SlogHandler() + if err != nil { + return err + } + slog.SetDefault(slog.New(slogHandler)) + // start http server + server, err := http.NewServer(version, keys, config) + if err != nil { + return err + } + slog.Info("Starting service", "config", config) + return server.Start() + } + + err := rootCmd.Execute() + if err != nil { + return 1 + } + return 0 +} + +func readConfiguration(path string) (*config.Service, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + config := config.NewServiceDefault() + err = yaml.Unmarshal(data, config) + return config, err +} + +func main() { + // start the service + os.Exit(run()) +} diff --git a/config/local_config.yml b/config/local_repository_config.yml similarity index 100% rename from config/local_config.yml rename to config/local_repository_config.yml diff --git a/config/service_config.yml b/config/service_config.yml new file mode 100644 index 0000000..82065e5 --- /dev/null +++ b/config/service_config.yml @@ -0,0 +1,14 @@ +--- +signing-method: RS256 +proxy: simple +repository: local +http: + host: 0.0.0.0 + port: 8080 + rate: + tps: 1024 + size: 1024 + white-list: [] +logger: + level: INFO + format: PLAIN diff --git a/go.mod b/go.mod index 626b577..892e8e7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/aerospike/aerospike-client-go/v7 v7.1.0 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/hashicorp/vault/api v1.11.0 + github.com/spf13/cobra v1.8.0 golang.org/x/crypto v0.18.0 golang.org/x/time v0.5.0 gopkg.in/yaml.v3 v3.0.1 @@ -13,12 +14,14 @@ require ( require ( github.com/cenkalti/backoff/v3 v3.0.0 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.14.1 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-test/deep v1.0.7 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.6.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect @@ -26,12 +29,15 @@ require ( github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.1-vault-3 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.11 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect @@ -40,4 +46,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 54d3dac..4640057 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,16 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= @@ -37,8 +40,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= @@ -57,18 +60,23 @@ github.com/hashicorp/hcl v1.0.1-vault-3 h1:V95v5KSTu6DB5huDSKiq4uAfILEuNigK/+qPE github.com/hashicorp/hcl v1.0.1-vault-3/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/vault/api v1.11.0 h1:AChWByeHf4/P9sX3Y1B7vFsQhZO2BgQiCMQ2SA1P1UY= github.com/hashicorp/vault/api v1.11.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -80,16 +88,26 @@ github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4 github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= @@ -110,6 +128,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -129,8 +149,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/auth/jwt.go b/internal/auth/jwt.go similarity index 95% rename from auth/jwt.go rename to internal/auth/jwt.go index 8e0d52d..44e9dd2 100644 --- a/auth/jwt.go +++ b/internal/auth/jwt.go @@ -4,7 +4,7 @@ import ( "encoding/json" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) // TokenType represents a token type. diff --git a/auth/jwt_generator.go b/internal/auth/jwt_generator.go similarity index 93% rename from auth/jwt_generator.go rename to internal/auth/jwt_generator.go index 92cbc9f..ca57a82 100644 --- a/auth/jwt_generator.go +++ b/internal/auth/jwt_generator.go @@ -4,8 +4,8 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/repository" - "github.com/reugn/auth-server/util/env" + "github.com/reugn/auth-server/internal/repository" + "github.com/reugn/auth-server/internal/util/env" ) const ( diff --git a/auth/jwt_test.go b/internal/auth/jwt_test.go similarity index 86% rename from auth/jwt_test.go rename to internal/auth/jwt_test.go index 0cb11b7..9996975 100644 --- a/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -5,19 +5,17 @@ import ( "testing" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) -const up = "../" - func TestJWT_Authorize(t *testing.T) { - os.Setenv(repository.EnvLocalConfigPath, up+repository.DefaultLocalConfigPath) + os.Setenv(repository.EnvLocalConfigPath, repository.DefaultLocalConfigPath) repo, err := repository.NewLocal() if err != nil { t.Fatal(err) } - os.Setenv(envPrivateKeyPath, up+defaultPrivateKeyPath) - os.Setenv(envPublicKeyPath, up+defaultPublicKeyPath) + os.Setenv(envPrivateKeyPath, defaultPrivateKeyPath) + os.Setenv(envPublicKeyPath, defaultPublicKeyPath) keys, err := NewKeys() if err != nil { t.Fatal(err) diff --git a/auth/jwt_validator.go b/internal/auth/jwt_validator.go similarity index 92% rename from auth/jwt_validator.go rename to internal/auth/jwt_validator.go index ad9ee41..f070915 100644 --- a/auth/jwt_validator.go +++ b/internal/auth/jwt_validator.go @@ -6,7 +6,7 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) // JWTValidator validates and authorizes an AccessToken. @@ -25,7 +25,7 @@ func NewJWTValidator(keys *Keys, backend repository.Repository) *JWTValidator { // validate validates the AccessToken. func (v *JWTValidator) validate(jtwToken string) (*Claims, error) { - token, err := jwt.Parse(jtwToken, func(token *jwt.Token) (interface{}, error) { + token, err := jwt.Parse(jtwToken, func(_ *jwt.Token) (interface{}, error) { return v.keys.publicKey, nil }) if err != nil { diff --git a/auth/keys.go b/internal/auth/keys.go similarity index 93% rename from auth/keys.go rename to internal/auth/keys.go index 2144c4b..cf90eb6 100644 --- a/auth/keys.go +++ b/internal/auth/keys.go @@ -6,15 +6,15 @@ import ( "os" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/util/env" + "github.com/reugn/auth-server/internal/util/env" ) const ( envPrivateKeyPath = "AUTH_SERVER_PRIVATE_KEY_PATH" envPublicKeyPath = "AUTH_SERVER_PUBLIC_KEY_PATH" - defaultPrivateKeyPath = "secrets/privkey.pem" - defaultPublicKeyPath = "secrets/cert.pem" + defaultPrivateKeyPath = "../../secrets/privkey.pem" + defaultPublicKeyPath = "../../secrets/cert.pem" ) // Keys represents a container for the private and public keys. diff --git a/internal/config/http_server.go b/internal/config/http_server.go new file mode 100644 index 0000000..d82eb13 --- /dev/null +++ b/internal/config/http_server.go @@ -0,0 +1,68 @@ +package config + +import ( + "errors" + "fmt" +) + +// HTTP contains HTTP server configuration properties. +type HTTP struct { + // The address to listen on. + Host string `yaml:"host,omitempty" json:"host,omitempty"` + // The port to listen on. + Port int `yaml:"port,omitempty" json:"port,omitempty"` + // Rate limiter configuration. + Rate RateLimiter `yaml:"rate,omitempty" json:"rate,omitempty"` +} + +// RateLimiter contains rate limiter configuration properties. +type RateLimiter struct { + // Rate limiter tokens per second threshold. + Tps int `yaml:"tps,omitempty" json:"tps,omitempty"` + // Rate limiter token bucket size (bursts threshold). + Size int `yaml:"size,omitempty" json:"size,omitempty"` + // A list of IP addresses to exclude from rate limiting. + WhiteList []string `yaml:"white-list,omitempty" json:"white-list,omitempty"` +} + +func (c *RateLimiter) validate() error { + if c == nil { + return errors.New("rate limiter config is nil") + } + if c.Tps < 1 { + return fmt.Errorf("invalid rate tps: %d", c.Tps) + } + if c.Size < 1 { + return fmt.Errorf("invalid rate size: %d", c.Size) + } + return nil +} + +// NewHTTPDefault returns a new HTTP config with default values. +func NewHTTPDefault() *HTTP { + return &HTTP{ + Host: "0.0.0.0", + Port: 8080, + Rate: RateLimiter{ + Tps: 1024, + Size: 1024, + }, + } +} + +// validate validates the HTTP configuration. +func (c *HTTP) validate() error { + if c == nil { + return errors.New("http config is nil") + } + if c.Host == "" { + return errors.New("host is not specified") + } + if c.Port < 1 { + return fmt.Errorf("invalid port: %d", c.Port) + } + if err := c.Rate.validate(); err != nil { + return err + } + return nil +} diff --git a/internal/config/logger.go b/internal/config/logger.go new file mode 100644 index 0000000..2b6cfc9 --- /dev/null +++ b/internal/config/logger.go @@ -0,0 +1,99 @@ +package config + +import ( + "errors" + "fmt" + "log/slog" + "os" + "slices" + "strings" +) + +const ( + logLevelDebug = "DEBUG" + logLevelInfo = "INFO" + logLevelWarn = "WARN" + logLevelWarning = "WARNING" + logLevelError = "ERROR" + + logFormatPlain = "PLAIN" + logFormatJSON = "JSON" +) + +// Logger contains the service logger configuration properties. +type Logger struct { + // Level is the log level (DEBUG, INFO, WARN, WARNING, ERROR). + Level string `yaml:"level,omitempty" json:"level,omitempty"` + // Format is the log format (PLAIN, JSON). + Format string `yaml:"format,omitempty" json:"format,omitempty"` +} + +// NewLoggerDefault returns a new Logger with default values. +func NewLoggerDefault() *Logger { + return &Logger{ + Level: logLevelInfo, + Format: logFormatPlain, + } +} + +var ( + validLoggerLevels = []string{logLevelDebug, logLevelInfo, logLevelWarn, + logLevelWarning, logLevelError} + supportedLoggerFormats = []string{logFormatPlain, logFormatJSON} +) + +func (l *Logger) SlogHandler() (slog.Handler, error) { + if err := l.validate(); err != nil { + return nil, err + } + logLevel, err := l.logLevel() + if err != nil { + return nil, err + } + addSource := true + writer := os.Stdout + switch strings.ToUpper(l.Format) { + case logFormatPlain: + return slog.NewTextHandler(writer, &slog.HandlerOptions{ + Level: logLevel, + AddSource: addSource, + }), nil + case logFormatJSON: + return slog.NewJSONHandler(writer, &slog.HandlerOptions{ + Level: logLevel, + AddSource: addSource, + }), nil + default: + return nil, fmt.Errorf("unsupported log format: %s", l.Format) + } +} + +// validate validates the logger configuration properties. +func (l *Logger) validate() error { + if l == nil { + return errors.New("logger config is nil") + } + if !slices.Contains(validLoggerLevels, strings.ToUpper(l.Level)) { + return fmt.Errorf("unsupported log level: %s", l.Level) + } + if !slices.Contains(supportedLoggerFormats, strings.ToUpper(l.Format)) { + return fmt.Errorf("unsupported log format: %s", l.Format) + } + return nil +} + +// logLevel returns the log level. +func (l *Logger) logLevel() (slog.Level, error) { + switch strings.ToUpper(l.Level) { + case logLevelDebug: + return slog.LevelDebug, nil + case logLevelInfo: + return slog.LevelInfo, nil + case logLevelWarn, logLevelWarning: + return slog.LevelWarn, nil + case logLevelError: + return slog.LevelError, nil + default: + return 0, fmt.Errorf("invalid log level: %s", l.Level) + } +} diff --git a/internal/config/service.go b/internal/config/service.go new file mode 100644 index 0000000..64de13c --- /dev/null +++ b/internal/config/service.go @@ -0,0 +1,114 @@ +package config + +import ( + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + + "github.com/golang-jwt/jwt/v5" + "github.com/reugn/auth-server/internal/proxy" + "github.com/reugn/auth-server/internal/repository" +) + +const ( + signingMethodRS256 = "RS256" + signingMethodRS384 = "RS384" + signingMethodRS512 = "RS512" +) + +var validSigningMethods = []string{signingMethodRS256, signingMethodRS384, signingMethodRS512} + +// Service contains the entire service configuration. +type Service struct { + SigningMethod string `yaml:"signing-method,omitempty" json:"signing-method,omitempty"` + ProxyProvider string `yaml:"proxy,omitempty" json:"proxy,omitempty"` + RepositoryProvider string `yaml:"repository,omitempty" json:"repository,omitempty"` + HTTP *HTTP `yaml:"http,omitempty" json:"http,omitempty"` + Logger *Logger `yaml:"logger,omitempty" json:"logger,omitempty"` +} + +// NewServiceDefault returns a new Service config with default values. +func NewServiceDefault() *Service { + return &Service{ + SigningMethod: signingMethodRS256, + ProxyProvider: "simple", + RepositoryProvider: "local", + HTTP: NewHTTPDefault(), + Logger: NewLoggerDefault(), + } +} + +func (c *Service) SigningMethodRSA() (*jwt.SigningMethodRSA, error) { + var signingMethodRSA *jwt.SigningMethodRSA + switch strings.ToUpper(c.SigningMethod) { + case signingMethodRS256: + signingMethodRSA = jwt.SigningMethodRS256 + case signingMethodRS384: + signingMethodRSA = jwt.SigningMethodRS384 + case signingMethodRS512: + signingMethodRSA = jwt.SigningMethodRS512 + default: + return nil, fmt.Errorf("unsupported signing method: %s", c.SigningMethod) + } + return signingMethodRSA, nil +} + +func (c *Service) RequestParser() (proxy.RequestParser, error) { + var parser proxy.RequestParser + switch strings.ToLower(c.ProxyProvider) { + case "simple": + parser = proxy.NewSimpleParser() + case "traefik": + parser = proxy.NewTraefikParser() + default: + return nil, fmt.Errorf("unsupported proxy provider: %s", c.ProxyProvider) + } + return parser, nil +} + +func (c *Service) Repository() (repository.Repository, error) { + switch strings.ToLower(c.RepositoryProvider) { + case "local": + return repository.NewLocal() + case "aerospike": + return repository.NewAerospike() + case "vault": + return repository.NewVault() + default: + return nil, fmt.Errorf("unsupported storage provider: %s", c.RepositoryProvider) + } +} + +// Validate validates the service configuration. +func (c *Service) Validate() error { + if c == nil { + return errors.New("service config is nil") + } + if !slices.Contains(validSigningMethods, strings.ToUpper(c.SigningMethod)) { + return fmt.Errorf("invalid signing method: %s", c.SigningMethod) + } + if c.ProxyProvider == "" { + return errors.New("proxy provider is not specified") + } + if c.RepositoryProvider == "" { + return errors.New("repository provider is not specified") + } + if err := c.HTTP.validate(); err != nil { + return err + } + if err := c.Logger.validate(); err != nil { + return err + } + return nil +} + +// String returns string representation of the service configuration. +func (c *Service) String() string { + data, err := json.Marshal(c) + if err != nil { + return err.Error() + } + return string(data) +} diff --git a/rate_limiter.go b/internal/http/rate_limiter.go similarity index 51% rename from rate_limiter.go rename to internal/http/rate_limiter.go index b43f816..f9af737 100644 --- a/rate_limiter.go +++ b/internal/http/rate_limiter.go @@ -1,11 +1,75 @@ -package main +package http import ( + "log/slog" + "net/netip" + "strings" "sync" "golang.org/x/time/rate" ) +// IPWhiteList contains white list information for rate limiting. +type IPWhiteList struct { + addresses map[string]*netip.Addr + networks []*netip.Prefix + allowAny bool +} + +// NewIPWhiteList builds a new IPWhiteList from the list of IPs. +func NewIPWhiteList(ipList []string) (*IPWhiteList, error) { + addresses := make(map[string]*netip.Addr) + networks := make([]*netip.Prefix, 0) + var allowAny bool + for _, ip := range ipList { + ip := strings.TrimSpace(ip) + if ip == "" { + continue + } + if strings.HasPrefix(ip, "0.0.0.0") { + allowAny = true + } + network, err := netip.ParsePrefix(ip) + if err != nil { + ipAddr, err := netip.ParseAddr(ip) + if err != nil { + return nil, err + } + addresses[ip] = &ipAddr + } else { + networks = append(networks, &network) + } + } + return &IPWhiteList{ + addresses: addresses, + networks: networks, + allowAny: allowAny, + }, nil +} + +func (wl *IPWhiteList) isAllowed(ip string) bool { + if wl.allowAny { + return true + } + ipAddr, err := netip.ParseAddr(ip) + if err != nil { + slog.Warn("Invalid client ip", "ip", ip, "err", err) + return false + } + _, ok := wl.addresses[ip] + if ok { + return true + } + + for _, network := range wl.networks { + if network.Contains(ipAddr) { + return true + } + } + + return false +} + // IPAddress represents an IP address string. type IPAddress string @@ -35,7 +99,6 @@ func (ipLimiter *IPRateLimiter) AddLimiter(ipAddr string) *rate.Limiter { defer ipLimiter.Unlock() limiter := rate.NewLimiter(ipLimiter.tokensPerSecond, ipLimiter.tokenBucketSize) - ipLimiter.limiters[IPAddress(ipAddr)] = limiter return limiter diff --git a/internal/http/server.go b/internal/http/server.go new file mode 100644 index 0000000..5a5299d --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,152 @@ +package http + +import ( + "fmt" + "net" + "net/http" + + "github.com/reugn/auth-server/internal/auth" + "github.com/reugn/auth-server/internal/config" + "github.com/reugn/auth-server/internal/proxy" + "github.com/reugn/auth-server/internal/repository" + "golang.org/x/time/rate" +) + +// Server is the authentication HTTP server wrapper. +type Server struct { + address string + version string + parser proxy.RequestParser + repository repository.Repository + rateLimiter *IPRateLimiter + ipWhiteList *IPWhiteList + jwtGenerator *auth.JWTGenerator + jwtValidator *auth.JWTValidator +} + +// NewServer returns a new instance of Server. +func NewServer(version string, keys *auth.Keys, config *config.Service) (*Server, error) { + address := fmt.Sprintf("%s:%d", config.HTTP.Host, config.HTTP.Port) + repository, err := config.Repository() + if err != nil { + return nil, err + } + signingMethod, err := config.SigningMethodRSA() + if err != nil { + return nil, err + } + generator := auth.NewJWTGenerator(keys, signingMethod) + validator := auth.NewJWTValidator(keys, repository) + + requestParser, err := config.RequestParser() + if err != nil { + return nil, err + } + ipWhiteList, err := NewIPWhiteList(config.HTTP.Rate.WhiteList) + if err != nil { + return nil, err + } + return &Server{ + address: address, + version: version, + parser: requestParser, + repository: repository, + rateLimiter: NewIPRateLimiter(rate.Limit(config.HTTP.Rate.Tps), config.HTTP.Rate.Size), + ipWhiteList: ipWhiteList, + jwtGenerator: generator, + jwtValidator: validator, + }, nil +} + +func (ws *Server) rateLimiterMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), + http.StatusInternalServerError) + return + } + if !ws.ipWhiteList.isAllowed(ip) { + limiter := ws.rateLimiter.GetLimiter(ip) + if !limiter.Allow() { + http.Error(w, http.StatusText(http.StatusTooManyRequests), + http.StatusTooManyRequests) + return + } + } + next.ServeHTTP(w, r) + }) +} + +func (ws *Server) Start() error { + mux := http.NewServeMux() + + // root route + mux.HandleFunc("/", rootActionHandler) + + // health route + mux.HandleFunc("/health", healthActionHandler) + + // readiness route + mux.HandleFunc("/ready", readyActionHandler) + + // version route + mux.HandleFunc("/version", ws.versionActionHandler) + + // token issuing route + // uses basic authentication + mux.HandleFunc("/token", ws.tokenActionHandler) + + // authorization route + // validates bearer JWT + mux.HandleFunc("/auth", ws.authActionHandler) + + return http.ListenAndServe(ws.address, ws.rateLimiterMiddleware(mux)) +} + +func rootActionHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + w.WriteHeader(http.StatusNotFound) + } + fmt.Fprintf(w, "") +} + +func healthActionHandler(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintf(w, "Ok") +} + +func readyActionHandler(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintf(w, "Ok") +} + +func (ws *Server) versionActionHandler(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, ws.version) +} + +func (ws *Server) tokenActionHandler(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + userDetails := ws.repository.AuthenticateBasic(user, pass) + if userDetails == nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + accessToken, err := ws.jwtGenerator.Generate(userDetails.UserName, userDetails.UserRole) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "%s", accessToken.Marshal()) +} + +func (ws *Server) authActionHandler(w http.ResponseWriter, r *http.Request) { + requestDetails := ws.parser.ParseRequestDetails(r) + authToken := ws.parser.ParseAuthorizationToken(r) + + if !ws.jwtValidator.Authorize(authToken, requestDetails) { + w.WriteHeader(http.StatusUnauthorized) + } +} diff --git a/proxy/parser.go b/internal/proxy/parser.go similarity index 88% rename from proxy/parser.go rename to internal/proxy/parser.go index 8d287b7..ffdc2b0 100644 --- a/proxy/parser.go +++ b/internal/proxy/parser.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) // RequestParser represents a request parser. diff --git a/proxy/simple_parser.go b/internal/proxy/simple_parser.go similarity index 94% rename from proxy/simple_parser.go rename to internal/proxy/simple_parser.go index da20f07..71f2aef 100644 --- a/proxy/simple_parser.go +++ b/internal/proxy/simple_parser.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) // SimpleParser implements the RequestParser interface. diff --git a/proxy/traefik_parser.go b/internal/proxy/traefik_parser.go similarity index 95% rename from proxy/traefik_parser.go rename to internal/proxy/traefik_parser.go index 4c92900..27f3065 100644 --- a/proxy/traefik_parser.go +++ b/internal/proxy/traefik_parser.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - "github.com/reugn/auth-server/repository" + "github.com/reugn/auth-server/internal/repository" ) // TraefikParser implements the RequestParser interface. diff --git a/repository/aerospike.go b/internal/repository/aerospike.go similarity index 98% rename from repository/aerospike.go rename to internal/repository/aerospike.go index 4761a29..e09d75c 100644 --- a/repository/aerospike.go +++ b/internal/repository/aerospike.go @@ -4,7 +4,7 @@ import ( "log" as "github.com/aerospike/aerospike-client-go/v7" - "github.com/reugn/auth-server/util/env" + "github.com/reugn/auth-server/internal/util/env" ) // Environment variables to configure AerospikeRepository. diff --git a/repository/aerospike_test.go b/internal/repository/aerospike_test.go similarity index 100% rename from repository/aerospike_test.go rename to internal/repository/aerospike_test.go diff --git a/repository/local.go b/internal/repository/local.go similarity index 94% rename from repository/local.go rename to internal/repository/local.go index 90e18a3..3e0308e 100644 --- a/repository/local.go +++ b/internal/repository/local.go @@ -3,13 +3,13 @@ package repository import ( "os" - "github.com/reugn/auth-server/util/env" + "github.com/reugn/auth-server/internal/util/env" "gopkg.in/yaml.v3" ) const ( EnvLocalConfigPath = "AUTH_SERVER_LOCAL_CONFIG_PATH" - DefaultLocalConfigPath = "config/local_config.yml" + DefaultLocalConfigPath = "../../config/local_repository_config.yml" ) // AuthDetails contains authentication details for the user. diff --git a/repository/repository.go b/internal/repository/repository.go similarity index 95% rename from repository/repository.go rename to internal/repository/repository.go index e15c358..379314c 100644 --- a/repository/repository.go +++ b/internal/repository/repository.go @@ -41,8 +41,7 @@ func isAuthorizedRequest(scopes []map[string]string, request RequestDetails) boo return false } -//nolint:unused -func hashAndSalt(pwd string) ([]byte, error) { +func HashAndSalt(pwd string) ([]byte, error) { bytePwd := []byte(pwd) // use bcrypt.GenerateFromPassword to hash and salt the password diff --git a/repository/vault.go b/internal/repository/vault.go similarity index 98% rename from repository/vault.go rename to internal/repository/vault.go index f56cc22..2bc4d1d 100644 --- a/repository/vault.go +++ b/internal/repository/vault.go @@ -5,7 +5,7 @@ import ( "log" "github.com/hashicorp/vault/api" - "github.com/reugn/auth-server/util/env" + "github.com/reugn/auth-server/internal/util/env" ) // Environment variables to configure VaultRepository. diff --git a/repository/vault_test.go b/internal/repository/vault_test.go similarity index 100% rename from repository/vault_test.go rename to internal/repository/vault_test.go diff --git a/util/env/env.go b/internal/util/env/env.go similarity index 100% rename from util/env/env.go rename to internal/util/env/env.go diff --git a/util/utils.go b/internal/util/utils.go similarity index 100% rename from util/utils.go rename to internal/util/utils.go diff --git a/util/utils_test.go b/internal/util/utils_test.go similarity index 87% rename from util/utils_test.go rename to internal/util/utils_test.go index 90ac318..3fce03b 100644 --- a/util/utils_test.go +++ b/internal/util/utils_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/reugn/auth-server/util" + "github.com/reugn/auth-server/internal/util" ) func TestSha256(t *testing.T) { diff --git a/main.go b/main.go deleted file mode 100644 index a2156f2..0000000 --- a/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "strings" - - "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/auth" - "github.com/reugn/auth-server/proxy" - "github.com/reugn/auth-server/repository" - "github.com/reugn/auth-server/util" -) - -const authServerVersion = "0.3.1" - -var ( - serverHostParam = flag.String("host", "0.0.0.0", "Server host") - serverPortParam = flag.Int("port", 8081, "Server port") - algoParam = flag.String("algo", "RS256", "JWT signing algorithm") - proxyParam = flag.String("proxy", "simple", "Proxy provider") - repoParam = flag.String("repo", "local", "Repository provider") -) - -func main() { - flag.Parse() - - // load ssl keys - keys, err := auth.NewKeys() - util.Check(err) - - // start http server - server := NewHTTPServer(*serverHostParam, *serverPortParam, keys) - server.start() -} - -func parseAlgo() *jwt.SigningMethodRSA { - var signingMethodRSA *jwt.SigningMethodRSA - switch strings.ToUpper(*algoParam) { - case "RS256": - signingMethodRSA = jwt.SigningMethodRS256 - case "RS384": - signingMethodRSA = jwt.SigningMethodRS384 - case "RS512": - signingMethodRSA = jwt.SigningMethodRS512 - default: - panic(fmt.Sprintf("Unsupported signing method: %s", *algoParam)) - } - return signingMethodRSA -} - -func parseProxy() proxy.RequestParser { - var parser proxy.RequestParser - switch strings.ToLower(*proxyParam) { - case "simple": - parser = proxy.NewSimpleParser() - case "traefik": - parser = proxy.NewTraefikParser() - default: - panic(fmt.Sprintf("Unsupported proxy provider: %s", *proxyParam)) - } - return parser -} - -func parseRepository() repository.Repository { - var repo repository.Repository - var err error - switch strings.ToLower(*repoParam) { - case "local": - repo, err = repository.NewLocal() - util.Check(err) - case "aerospike": - repo, err = repository.NewAerospike() - util.Check(err) - case "vault": - repo, err = repository.NewVault() - util.Check(err) - default: - panic(fmt.Sprintf("Unsupported storage provider: %s", *repoParam)) - } - return repo -} diff --git a/server.go b/server.go deleted file mode 100644 index 0f278c3..0000000 --- a/server.go +++ /dev/null @@ -1,142 +0,0 @@ -package main - -import ( - "fmt" - "net" - "net/http" - "strconv" - - "github.com/reugn/auth-server/auth" - "github.com/reugn/auth-server/proxy" - "github.com/reugn/auth-server/repository" - "github.com/reugn/auth-server/util" -) - -var rateLimiter = NewIPRateLimiter(1, 10) - -var ipsWhiteList = map[string]struct{}{ - "127.0.0.1": {}, -} - -// HTTPServer is the authentication HTTP server wrapper. -type HTTPServer struct { - addr string - parser proxy.RequestParser - repo repository.Repository - jwtGenerator *auth.JWTGenerator - jwtValidtor *auth.JWTValidator -} - -// NewHTTPServer returns a new instance of HTTPServer. -func NewHTTPServer(host string, port int, keys *auth.Keys) *HTTPServer { - addr := host + ":" + strconv.Itoa(port) - repository := parseRepository() - generator := auth.NewJWTGenerator(keys, parseAlgo()) - validator := auth.NewJWTValidator(keys, repository) - - return &HTTPServer{ - addr: addr, - parser: parseProxy(), - repo: repository, - jwtGenerator: generator, - jwtValidtor: validator, - } -} - -func rateLimiterMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - _, ok := ipsWhiteList[ip] - if !ok { - limiter := rateLimiter.GetLimiter(ip) - if !limiter.Allow() { - http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) - return - } - } - - next.ServeHTTP(w, r) - }) -} - -func (ws *HTTPServer) start() { - mux := http.NewServeMux() - - // root route - mux.HandleFunc("/", rootActionHandler) - - // health route - mux.HandleFunc("/health", healthActionHandler) - - // readiness route - mux.HandleFunc("/ready", readyActionHandler) - - // version route - mux.HandleFunc("/version", versionActionHandler) - - // token issuing route - // uses basic authentication - mux.HandleFunc("/token", ws.tokenActionHandler) - - // authorization route - // validates bearer JWT - mux.HandleFunc("/auth", ws.authActionHandler) - - err := http.ListenAndServe(ws.addr, rateLimiterMiddleware(mux)) - util.Check(err) -} - -func rootActionHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - w.WriteHeader(http.StatusNotFound) - } - fmt.Fprintf(w, "") -} - -func healthActionHandler(w http.ResponseWriter, _ *http.Request) { - fmt.Fprintf(w, "Ok") -} - -func readyActionHandler(w http.ResponseWriter, _ *http.Request) { - fmt.Fprintf(w, "Ok") -} - -func versionActionHandler(w http.ResponseWriter, _ *http.Request) { - fmt.Fprint(w, authServerVersion) -} - -func (ws *HTTPServer) tokenActionHandler(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok { - w.WriteHeader(http.StatusBadRequest) - return - } - - userDetails := ws.repo.AuthenticateBasic(user, pass) - if userDetails == nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - token, err := ws.jwtGenerator.Generate(userDetails.UserName, userDetails.UserRole) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - fmt.Fprintf(w, "%s", token.Marshal()) -} - -func (ws *HTTPServer) authActionHandler(w http.ResponseWriter, r *http.Request) { - requestDetails := ws.parser.ParseRequestDetails(r) - auth := ws.parser.ParseAuthorizationToken(r) - - if !ws.jwtValidtor.Authorize(auth, requestDetails) { - w.WriteHeader(http.StatusUnauthorized) - } -} From 926e32cd9b4c65b4650d98b0d993af13a46310db Mon Sep 17 00:00:00 2001 From: reugn Date: Thu, 22 Feb 2024 16:19:30 +0200 Subject: [PATCH 05/14] configure secret keys in the configuration file --- Dockerfile | 3 --- cmd/auth/main.go | 8 ++++---- config/service_config.yml | 3 +++ internal/auth/jwt_test.go | 9 ++++++--- internal/auth/keys.go | 20 +++---------------- internal/config/http_server.go | 5 +++-- internal/config/secret.go | 35 ++++++++++++++++++++++++++++++++++ internal/config/service.go | 17 ++++++++++++++++- 8 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 internal/config/secret.go diff --git a/Dockerfile b/Dockerfile index f004ec9..882be87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,10 +11,7 @@ WORKDIR /app COPY --from=build /go/src/app/cmd/auth/bin /app COPY --from=build /go/src/app/config /app/ COPY ./secrets ./secrets - ENV AUTH_SERVER_LOCAL_CONFIG_PATH=local_repository_config.yml -ENV AUTH_SERVER_PRIVATE_KEY_PATH=secrets/privkey.pem -ENV AUTH_SERVER_PUBLIC_KEY_PATH=secrets/cert.pem EXPOSE 8080 ENTRYPOINT ["/app/auth", "-c", "service_config.yml"] diff --git a/cmd/auth/main.go b/cmd/auth/main.go index 54cb0dd..7177748 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -25,13 +25,13 @@ func run() int { rootCmd.Flags().StringVarP(&configFilePath, "config", "c", "config.yaml", "configuration file path") rootCmd.RunE = func(_ *cobra.Command, _ []string) error { - // load ssl keys - keys, err := auth.NewKeys() + // read configuration file + config, err := readConfiguration(configFilePath) if err != nil { return err } - // read configuration file - config, err := readConfiguration(configFilePath) + // load ssl keys + keys, err := auth.NewKeys(config.Secret) if err != nil { return err } diff --git a/config/service_config.yml b/config/service_config.yml index 82065e5..5737d51 100644 --- a/config/service_config.yml +++ b/config/service_config.yml @@ -9,6 +9,9 @@ http: tps: 1024 size: 1024 white-list: [] +secret: + private-path: secrets/privkey.pem + public-path: secrets/cert.pem logger: level: INFO format: PLAIN diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go index 9996975..64ff5f3 100644 --- a/internal/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/golang-jwt/jwt/v5" + "github.com/reugn/auth-server/internal/config" "github.com/reugn/auth-server/internal/repository" ) @@ -14,9 +15,11 @@ func TestJWT_Authorize(t *testing.T) { if err != nil { t.Fatal(err) } - os.Setenv(envPrivateKeyPath, defaultPrivateKeyPath) - os.Setenv(envPublicKeyPath, defaultPublicKeyPath) - keys, err := NewKeys() + secretConfig := &config.Secret{ + Private: "../../secrets/privkey.pem", + Public: "../../secrets/cert.pem", + } + keys, err := NewKeys(secretConfig) if err != nil { t.Fatal(err) } diff --git a/internal/auth/keys.go b/internal/auth/keys.go index cf90eb6..50f6eb7 100644 --- a/internal/auth/keys.go +++ b/internal/auth/keys.go @@ -6,15 +6,7 @@ import ( "os" "github.com/golang-jwt/jwt/v5" - "github.com/reugn/auth-server/internal/util/env" -) - -const ( - envPrivateKeyPath = "AUTH_SERVER_PRIVATE_KEY_PATH" - envPublicKeyPath = "AUTH_SERVER_PUBLIC_KEY_PATH" - - defaultPrivateKeyPath = "../../secrets/privkey.pem" - defaultPublicKeyPath = "../../secrets/cert.pem" + "github.com/reugn/auth-server/internal/config" ) // Keys represents a container for the private and public keys. @@ -24,14 +16,8 @@ type Keys struct { } // NewKeys returns a new instance of Keys. -func NewKeys() (*Keys, error) { - privateKeyPath := defaultPrivateKeyPath - env.ReadString(&privateKeyPath, envPrivateKeyPath) - - publicKeyPath := defaultPublicKeyPath - env.ReadString(&publicKeyPath, envPublicKeyPath) - - return NewKeysFromFile(privateKeyPath, publicKeyPath) +func NewKeys(config *config.Secret) (*Keys, error) { + return NewKeysFromFile(config.Private, config.Public) } // NewKeysFromFile creates and returns a new instance of Keys from the files. diff --git a/internal/config/http_server.go b/internal/config/http_server.go index d82eb13..18445d5 100644 --- a/internal/config/http_server.go +++ b/internal/config/http_server.go @@ -44,8 +44,9 @@ func NewHTTPDefault() *HTTP { Host: "0.0.0.0", Port: 8080, Rate: RateLimiter{ - Tps: 1024, - Size: 1024, + Tps: 1024, + Size: 1024, + WhiteList: []string{}, }, } } diff --git a/internal/config/secret.go b/internal/config/secret.go new file mode 100644 index 0000000..cd2aa85 --- /dev/null +++ b/internal/config/secret.go @@ -0,0 +1,35 @@ +package config + +import ( + "errors" +) + +// Secret holds the configuration for secret keys. +type Secret struct { + // Private denotes the path to the private key. + Private string `yaml:"private-path,omitempty" json:"private-path,omitempty"` + // Public denotes the path to the public key. + Public string `yaml:"public-path,omitempty" json:"public-path,omitempty"` +} + +// NewSecretDefault returns a new Secret with default values. +func NewSecretDefault() *Secret { + return &Secret{ + Private: "secrets/privkey.pem", + Public: "secrets/cert.pem", + } +} + +// validate validates the Secret configuration properties. +func (s *Secret) validate() error { + if s == nil { + return errors.New("secret config is nil") + } + if s.Private == "" { + return errors.New("private key path is not specified") + } + if s.Public == "" { + return errors.New("public key path is not specified") + } + return nil +} diff --git a/internal/config/service.go b/internal/config/service.go index 64de13c..6f76c4a 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -10,6 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/internal/proxy" "github.com/reugn/auth-server/internal/repository" + "gopkg.in/yaml.v3" ) const ( @@ -26,6 +27,7 @@ type Service struct { ProxyProvider string `yaml:"proxy,omitempty" json:"proxy,omitempty"` RepositoryProvider string `yaml:"repository,omitempty" json:"repository,omitempty"` HTTP *HTTP `yaml:"http,omitempty" json:"http,omitempty"` + Secret *Secret `yaml:"secret,omitempty" json:"secret,omitempty"` Logger *Logger `yaml:"logger,omitempty" json:"logger,omitempty"` } @@ -36,6 +38,7 @@ func NewServiceDefault() *Service { ProxyProvider: "simple", RepositoryProvider: "local", HTTP: NewHTTPDefault(), + Secret: NewSecretDefault(), Logger: NewLoggerDefault(), } } @@ -98,13 +101,16 @@ func (c *Service) Validate() error { if err := c.HTTP.validate(); err != nil { return err } + if err := c.Secret.validate(); err != nil { + return err + } if err := c.Logger.validate(); err != nil { return err } return nil } -// String returns string representation of the service configuration. +// String returns a string representation of the service configuration in JSON format. func (c *Service) String() string { data, err := json.Marshal(c) if err != nil { @@ -112,3 +118,12 @@ func (c *Service) String() string { } return string(data) } + +// StringYaml returns a string representation of the service configuration in YAML format. +func (c *Service) StringYaml() string { + data, err := yaml.Marshal(c) + if err != nil { + return err.Error() + } + return string(data) +} From 1e63f4d6fbc30bc90ab47149a1e330bc3a0e10c1 Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 23 Feb 2024 11:15:41 +0200 Subject: [PATCH 06/14] reorganize utility packages --- internal/util/{utils.go => hash/hash.go} | 9 +-------- internal/util/hash/hash_test.go | 13 +++++++++++++ internal/util/utils_test.go | 18 ------------------ 3 files changed, 14 insertions(+), 26 deletions(-) rename internal/util/{utils.go => hash/hash.go} (67%) create mode 100644 internal/util/hash/hash_test.go delete mode 100644 internal/util/utils_test.go diff --git a/internal/util/utils.go b/internal/util/hash/hash.go similarity index 67% rename from internal/util/utils.go rename to internal/util/hash/hash.go index d4473d4..e057a41 100644 --- a/internal/util/utils.go +++ b/internal/util/hash/hash.go @@ -1,4 +1,4 @@ -package util +package hash import ( "crypto/sha256" @@ -10,10 +10,3 @@ func Sha256(str string) string { sha256pwd := sha256.Sum256([]byte(str)) return fmt.Sprintf("%x", sha256pwd) } - -// Check panics if the error is not nil. -func Check(e error) { - if e != nil { - panic(e) - } -} diff --git a/internal/util/hash/hash_test.go b/internal/util/hash/hash_test.go new file mode 100644 index 0000000..7ffee5d --- /dev/null +++ b/internal/util/hash/hash_test.go @@ -0,0 +1,13 @@ +package hash_test + +import ( + "testing" + + "github.com/reugn/auth-server/internal/util/hash" +) + +func TestSha256(t *testing.T) { + if hash.Sha256("1234") != "03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4" { + t.Fatal("Sha256") + } +} diff --git a/internal/util/utils_test.go b/internal/util/utils_test.go deleted file mode 100644 index 3fce03b..0000000 --- a/internal/util/utils_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package util_test - -import ( - "reflect" - "testing" - - "github.com/reugn/auth-server/internal/util" -) - -func TestSha256(t *testing.T) { - assertEqual(t, "03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4", util.Sha256("1234")) -} - -func assertEqual(t *testing.T, a interface{}, b interface{}) { - if !reflect.DeepEqual(a, b) { - t.Fatalf("%v != %v", a, b) - } -} From d58ff3b6668e44c41a7215875ee04845186849a9 Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 23 Feb 2024 11:23:35 +0200 Subject: [PATCH 07/14] improve service logging --- internal/auth/jwt.go | 2 ++ internal/auth/jwt_validator.go | 7 ++++--- internal/auth/keys.go | 3 ++- internal/http/server.go | 9 ++++----- internal/proxy/simple_parser.go | 2 ++ internal/proxy/traefik_parser.go | 3 ++- internal/repository/aerospike.go | 7 ++++--- internal/repository/repository.go | 12 +++++++++++- internal/repository/vault.go | 15 +++++++++------ 9 files changed, 40 insertions(+), 20 deletions(-) diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 44e9dd2..a396ded 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -2,6 +2,7 @@ package auth import ( "encoding/json" + "log/slog" "github.com/golang-jwt/jwt/v5" "github.com/reugn/auth-server/internal/repository" @@ -44,6 +45,7 @@ type AccessToken struct { func (t *AccessToken) Marshal() string { jsonByteArray, err := json.Marshal(t) if err != nil { + slog.Debug("Failed to marshal token", "err", err) return "" } return string(jsonByteArray) diff --git a/internal/auth/jwt_validator.go b/internal/auth/jwt_validator.go index f070915..7b0fc22 100644 --- a/internal/auth/jwt_validator.go +++ b/internal/auth/jwt_validator.go @@ -2,7 +2,7 @@ package auth import ( "encoding/json" - "log" + "log/slog" "time" "github.com/golang-jwt/jwt/v5" @@ -15,7 +15,7 @@ type JWTValidator struct { backend repository.Repository } -// NewJWTValidator returns a new instance of JWTValidator. +// NewJWTValidator returns a new JWTValidator. func NewJWTValidator(keys *Keys, backend repository.Repository) *JWTValidator { return &JWTValidator{ keys: keys, @@ -43,6 +43,7 @@ func (v *JWTValidator) validateClaims(token *jwt.Token) (*Claims, error) { // validate expiration if claims.ExpiresAt.Before(time.Now()) { + slog.Debug("Token expired") return nil, jwt.ErrTokenExpired } @@ -69,7 +70,7 @@ func getClaims(token *jwt.Token) (*Claims, error) { func (v *JWTValidator) Authorize(token string, request *repository.RequestDetails) bool { claims, err := v.validate(token) if err != nil { - log.Println(err.Error()) + slog.Debug("Failed to authorize token", "err", err) return false } diff --git a/internal/auth/keys.go b/internal/auth/keys.go index 50f6eb7..0da5cbe 100644 --- a/internal/auth/keys.go +++ b/internal/auth/keys.go @@ -20,7 +20,8 @@ func NewKeys(config *config.Secret) (*Keys, error) { return NewKeysFromFile(config.Private, config.Public) } -// NewKeysFromFile creates and returns a new instance of Keys from the files. +// NewKeysFromFile creates and returns a new instance of Keys from the files +// containing the secrets information. func NewKeysFromFile(privateKeyPath string, publicKeyPath string) (*Keys, error) { priv, err := parsePrivateKey(&privateKeyPath, nil) if err != nil { diff --git a/internal/http/server.go b/internal/http/server.go index 5a5299d..e2bb30e 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -12,7 +12,7 @@ import ( "golang.org/x/time/rate" ) -// Server is the authentication HTTP server wrapper. +// Server represents the entry point to interact with the service via HTTP requests. type Server struct { address string version string @@ -78,6 +78,7 @@ func (ws *Server) rateLimiterMiddleware(next http.Handler) http.Handler { }) } +// Start initiates the HTTP server. func (ws *Server) Start() error { mux := http.NewServeMux() @@ -93,12 +94,10 @@ func (ws *Server) Start() error { // version route mux.HandleFunc("/version", ws.versionActionHandler) - // token issuing route - // uses basic authentication + // token issuing route, requires basic authentication mux.HandleFunc("/token", ws.tokenActionHandler) - // authorization route - // validates bearer JWT + // authorization route, requires a JSON Web Token mux.HandleFunc("/auth", ws.authActionHandler) return http.ListenAndServe(ws.address, ws.rateLimiterMiddleware(mux)) diff --git a/internal/proxy/simple_parser.go b/internal/proxy/simple_parser.go index 71f2aef..ce4a70c 100644 --- a/internal/proxy/simple_parser.go +++ b/internal/proxy/simple_parser.go @@ -1,6 +1,7 @@ package proxy import ( + "log/slog" "net/http" "strings" @@ -27,6 +28,7 @@ func (sp *SimpleParser) ParseAuthorizationToken(r *http.Request) string { if len(splitToken) == 2 { return strings.TrimSpace(splitToken[1]) } + slog.Debug("Invalid Authorization header", "header", authHeader) return "" } diff --git a/internal/proxy/traefik_parser.go b/internal/proxy/traefik_parser.go index 27f3065..945ed5f 100644 --- a/internal/proxy/traefik_parser.go +++ b/internal/proxy/traefik_parser.go @@ -1,6 +1,7 @@ package proxy import ( + "log/slog" "net/http" "strings" @@ -23,11 +24,11 @@ func (tp *TraefikParser) ParseAuthorizationToken(r *http.Request) string { if authHeader == "" { return authHeader } - splitToken := strings.Split(authHeader, "Bearer") if len(splitToken) == 2 { return strings.TrimSpace(splitToken[1]) } + slog.Debug("Invalid Authorization header", "header", authHeader) return "" } diff --git a/internal/repository/aerospike.go b/internal/repository/aerospike.go index e09d75c..b3a69fd 100644 --- a/internal/repository/aerospike.go +++ b/internal/repository/aerospike.go @@ -1,7 +1,7 @@ package repository import ( - "log" + "log/slog" as "github.com/aerospike/aerospike-client-go/v7" "github.com/reugn/auth-server/internal/util/env" @@ -89,7 +89,7 @@ func NewAerospike() (*AerospikeRepository, error) { func (aero *AerospikeRepository) AuthenticateBasic(username string, password string) *UserDetails { record, err := aero.client.Get(nil, aero.baseKey, username) if err != nil { - log.Println(err.Error()) + slog.Error("Failed to fetch record", "key", aero.baseKey, "err", err) return nil } @@ -97,6 +97,7 @@ func (aero *AerospikeRepository) AuthenticateBasic(username string, password str userBin := record.Bins[username].(map[string]interface{}) hashed, ok := userBin["password"].(string) if !ok || !pwdMatch(hashed, password) { + slog.Debug("Failed to authenticate", "user", username) return nil } @@ -110,7 +111,7 @@ func (aero *AerospikeRepository) AuthenticateBasic(username string, password str func (aero *AerospikeRepository) AuthorizeRequest(userRole UserRole, request RequestDetails) bool { record, err := aero.client.Get(nil, aero.authKey, string(userRole)) if err != nil { - log.Println(err.Error()) + slog.Error("Failed to fetch record", "key", aero.authKey, "err", err) return false } // Bin(admin: [{method: GET, uri: /health}]) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 379314c..8995cc4 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -1,6 +1,8 @@ package repository import ( + "fmt" + "log/slog" "strings" "golang.org/x/crypto/bcrypt" @@ -21,7 +23,13 @@ type RequestDetails struct { URI string `yaml:"uri"` } -// Repository represents an authentication/authorization backend facade. +// String implements the fmt.Stringer interface. +func (r RequestDetails) String() string { + return fmt.Sprintf("%s %s", r.Method, r.URI) +} + +// A Repository acts as a gateway to the authentication and authorization +// operations, facilitating secure access to resources. type Repository interface { // AuthenticateBasic validates the basic username and password before issuing a JWT. @@ -35,9 +43,11 @@ func isAuthorizedRequest(scopes []map[string]string, request RequestDetails) boo for _, scope := range scopes { if (scope["method"] == "*" || scope["method"] == request.Method) && (scope["uri"] == "*" || strings.HasPrefix(request.URI, scope["uri"])) { + slog.Debug("Request authorized", "request", request) return true } } + slog.Debug("Authorization failed for the request", "request", request) return false } diff --git a/internal/repository/vault.go b/internal/repository/vault.go index 2bc4d1d..a403e00 100644 --- a/internal/repository/vault.go +++ b/internal/repository/vault.go @@ -2,7 +2,7 @@ package repository import ( "fmt" - "log" + "log/slog" "github.com/hashicorp/vault/api" "github.com/reugn/auth-server/internal/util/env" @@ -71,14 +71,16 @@ func NewVault() (*VaultRepository, error) { // AuthenticateBasic validates the basic username and password before issuing a JWT. // It uses the bcrypt password-hashing function to validate the password. func (vr *VaultRepository) AuthenticateBasic(username string, password string) *UserDetails { - secret, err := vr.client.Logical().Read(vr.config.basicAuthKeyPrefix + "/" + username) + path := fmt.Sprintf("%s/%s", vr.config.basicAuthKeyPrefix, username) + secret, err := vr.client.Logical().Read(path) if err != nil { - log.Println(err.Error()) + slog.Error("Failed to read path", "path", path, "err", err) return nil } hashed, ok := secret.Data["password"].(string) if !ok || !pwdMatch(hashed, password) { + slog.Debug("Failed to authenticate", "user", username) return nil } @@ -90,15 +92,16 @@ func (vr *VaultRepository) AuthenticateBasic(username string, password string) * // AuthorizeRequest checks if the role has permissions to access the endpoint. func (vr *VaultRepository) AuthorizeRequest(userRole UserRole, request RequestDetails) bool { - secret, err := vr.client.Logical().Read(fmt.Sprintf("%s/%s", vr.config.authorizationKeyPrefix, userRole)) + path := fmt.Sprintf("%s/%s", vr.config.authorizationKeyPrefix, userRole) + secret, err := vr.client.Logical().Read(path) if err != nil { - log.Println(err.Error()) + slog.Error("Failed to read path", "path", path, "err", err) return false } scopes, ok := secret.Data["scopes"].([]map[string]string) if !ok { - log.Printf("VaultRepository: error on reading scopes for: %s", userRole) + slog.Error("Error reading scopes", "role", userRole) return false } From 834bb0850b52c3f9002f475acee2a5c1824c2694 Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 23 Feb 2024 17:41:33 +0200 Subject: [PATCH 08/14] revamp the example of using the service as a Traefik middleware --- Dockerfile | 3 +- config/service_config.yml | 4 +-- ...docker-compose.yaml => docker-compose.yml} | 19 +++++------ examples/traefik/dynamic-config.yml | 33 +++++++++++++++++++ examples/traefik/dynamic_conf.toml | 24 -------------- examples/traefik/traefik.toml | 22 ------------- examples/traefik/traefik.yml | 21 ++++++++++++ 7 files changed, 66 insertions(+), 60 deletions(-) rename examples/traefik/{docker-compose.yaml => docker-compose.yml} (66%) create mode 100644 examples/traefik/dynamic-config.yml delete mode 100644 examples/traefik/dynamic_conf.toml delete mode 100644 examples/traefik/traefik.toml create mode 100644 examples/traefik/traefik.yml diff --git a/Dockerfile b/Dockerfile index 882be87..bf8e023 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.2 FROM golang:alpine3.19 AS build RUN apk --no-cache add gcc g++ make git WORKDIR /go/src/app @@ -13,5 +14,5 @@ COPY --from=build /go/src/app/config /app/ COPY ./secrets ./secrets ENV AUTH_SERVER_LOCAL_CONFIG_PATH=local_repository_config.yml -EXPOSE 8080 +EXPOSE 8081 ENTRYPOINT ["/app/auth", "-c", "service_config.yml"] diff --git a/config/service_config.yml b/config/service_config.yml index 5737d51..6853daf 100644 --- a/config/service_config.yml +++ b/config/service_config.yml @@ -1,10 +1,10 @@ --- signing-method: RS256 -proxy: simple +proxy: traefik repository: local http: host: 0.0.0.0 - port: 8080 + port: 8081 rate: tps: 1024 size: 1024 diff --git a/examples/traefik/docker-compose.yaml b/examples/traefik/docker-compose.yml similarity index 66% rename from examples/traefik/docker-compose.yaml rename to examples/traefik/docker-compose.yml index fbf823c..5782835 100644 --- a/examples/traefik/docker-compose.yaml +++ b/examples/traefik/docker-compose.yml @@ -1,20 +1,24 @@ version: '3.4' +networks: + proxy: + services: reverse-proxy: restart: always - image: traefik:v2.2 + image: traefik:v2.11 container_name: traefik ports: - 443:443 - - 8080:8080 - 8082:8082 volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./traefik.toml:/etc/traefik/traefik.toml - - ./dynamic_conf.toml:/etc/traefik/dynamic_conf.toml + - ./traefik.yml:/etc/traefik/traefik.yml + - ./dynamic-config.yml:/etc/traefik/dynamic-config.yml labels: + - "traefik.http.routers.site.entryPoints=http,websecure" - "traefik.enable=true" + - "traefik.port=8082" networks: - proxy @@ -27,12 +31,5 @@ services: build: dockerfile: Dockerfile context: ../../. - command: "-proxy=traefik -repo=local" - labels: - - "traefik.enable=true" networks: - proxy - -networks: - proxy: - external: true diff --git a/examples/traefik/dynamic-config.yml b/examples/traefik/dynamic-config.yml new file mode 100644 index 0000000..6731ca7 --- /dev/null +++ b/examples/traefik/dynamic-config.yml @@ -0,0 +1,33 @@ +--- +http: + services: + auth-server: + loadBalancer: + servers: + - url: http://auth-server:8081/ + + middlewares: + test-auth: + forwardAuth: + address: http://auth-server:8081/auth + authResponseHeaders: + - "X-Auth-User" + - "X-Secret" + trustForwardHeader: true + + routers: + token-router: + rule: "Path(`/token`)" + service: auth-server + entrypoints: + - http + priority: 2 + + auth-router: + rule: "HostRegexp(`{host:.*}`)" + middlewares: + - test-auth + service: auth-server + entrypoints: + - http + priority: 1 diff --git a/examples/traefik/dynamic_conf.toml b/examples/traefik/dynamic_conf.toml deleted file mode 100644 index 0307a96..0000000 --- a/examples/traefik/dynamic_conf.toml +++ /dev/null @@ -1,24 +0,0 @@ -[http] - [http.services] - [http.services.auth-server] - [[http.services.auth-server.loadBalancer.servers]] - url = "http://auth-server:8081/" - - [http.middlewares] - [http.middlewares.test-auth.forwardAuth] - address = "http://auth-server:8081/auth" - trustForwardHeader = true - - [http.routers] - [http.routers.token-router] - rule = "Path(`/token`)" - service = "auth-server" - entrypoints = ["http"] - priority = 2 - - [http.routers.auth-router] - rule = "HostRegexp(`{host:.*}`)" - middlewares = ["test-auth"] - service = "auth-server" - entrypoints = ["http"] - priority = 1 diff --git a/examples/traefik/traefik.toml b/examples/traefik/traefik.toml deleted file mode 100644 index d3a777f..0000000 --- a/examples/traefik/traefik.toml +++ /dev/null @@ -1,22 +0,0 @@ -[global] - checkNewVersion = true - sendAnonymousUsage = true - -[entryPoints] - [entryPoints.websecure] - address = ":443" - [entryPoints.http] - address = ":8082" - -# Enable API and dashboard -[api] - dashboard = true - insecure = true - -[providers] - [providers.file] - filename = "/etc/traefik/dynamic_conf.toml" - watch = true - - [providers.docker] - network = "proxy" diff --git a/examples/traefik/traefik.yml b/examples/traefik/traefik.yml new file mode 100644 index 0000000..a82186d --- /dev/null +++ b/examples/traefik/traefik.yml @@ -0,0 +1,21 @@ +--- +global: + checkNewVersion: true + sendAnonymousUsage: true + +entryPoints: + websecure: + address: ":443" + http: + address: ":8082" + +api: + dashboard: true + insecure: true + +providers: + file: + filename: /etc/traefik/dynamic-config.yml + watch: true + docker: + network: proxy From d50b0584cfe6c7ef101e6b2b83b14a404dbd9254 Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 23 Feb 2024 18:02:26 +0200 Subject: [PATCH 09/14] introduce an error return value to the Marshal method of AccessToken --- internal/auth/jwt.go | 10 +++++----- internal/auth/jwt_generator.go | 6 +++--- internal/http/server.go | 10 +++++++++- internal/repository/local.go | 3 +++ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index a396ded..07b92de 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -22,8 +22,8 @@ const ( BasicToken ) -// ToString converts the TokenType to a string. -func (t TokenType) ToString() string { +// String returns the string representation of the TokenType. +func (t TokenType) String() string { return [...]string{"Bearer", "Basic"}[t] } @@ -42,11 +42,11 @@ type AccessToken struct { } // Marshal marshals the AccessToken to a JSON string. -func (t *AccessToken) Marshal() string { +func (t *AccessToken) Marshal() (string, error) { jsonByteArray, err := json.Marshal(t) if err != nil { slog.Debug("Failed to marshal token", "err", err) - return "" + return "", err } - return string(jsonByteArray) + return string(jsonByteArray), nil } diff --git a/internal/auth/jwt_generator.go b/internal/auth/jwt_generator.go index ca57a82..e958dc2 100644 --- a/internal/auth/jwt_generator.go +++ b/internal/auth/jwt_generator.go @@ -54,9 +54,9 @@ func (gen *JWTGenerator) Generate(username string, role repository.UserRole) (*A // create an access token accessToken := &AccessToken{ - signed, - BearerToken.ToString(), - gen.tokenExpireAfter.Milliseconds(), + Token: signed, + Type: BearerToken.String(), + Expires: gen.tokenExpireAfter.Milliseconds(), } return accessToken, nil diff --git a/internal/http/server.go b/internal/http/server.go index e2bb30e..4bfffcb 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -2,6 +2,7 @@ package http import ( "fmt" + "log/slog" "net" "net/http" @@ -123,6 +124,7 @@ func (ws *Server) versionActionHandler(w http.ResponseWriter, _ *http.Request) { } func (ws *Server) tokenActionHandler(w http.ResponseWriter, r *http.Request) { + slog.Debug("Token generation request") user, pass, ok := r.BasicAuth() if !ok { w.WriteHeader(http.StatusBadRequest) @@ -138,10 +140,16 @@ func (ws *Server) tokenActionHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } - fmt.Fprintf(w, "%s", accessToken.Marshal()) + marshalled, err := accessToken.Marshal() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "%s", marshalled) } func (ws *Server) authActionHandler(w http.ResponseWriter, r *http.Request) { + slog.Debug("Token authorization request") requestDetails := ws.parser.ParseRequestDetails(r) authToken := ws.parser.ParseAuthorizationToken(r) diff --git a/internal/repository/local.go b/internal/repository/local.go index 3e0308e..f91b2bf 100644 --- a/internal/repository/local.go +++ b/internal/repository/local.go @@ -1,6 +1,7 @@ package repository import ( + "log/slog" "os" "github.com/reugn/auth-server/internal/util/env" @@ -63,9 +64,11 @@ func (local *Local) AuthenticateBasic(username string, password string) *UserDet func (local *Local) AuthorizeRequest(userRole UserRole, requestDetails RequestDetails) bool { if permissions, ok := local.Roles[userRole]; ok { if containsRequestDetails(permissions, requestDetails) { + slog.Debug("Request authorized", "request", requestDetails) return true } } + slog.Debug("Authorization failed for the request", "request", requestDetails) return false } From 14d6c5432d7d0aebf1aac2d97e07f8dd1d91d28a Mon Sep 17 00:00:00 2001 From: reugn Date: Fri, 23 Feb 2024 19:09:42 +0200 Subject: [PATCH 10/14] adapt ignore files to accommodate project modifications --- .dockerignore | 3 ++- .gitignore | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 0de603a..ec37e86 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ .github .cache -examples/ \ No newline at end of file +examples/ +docs/ diff --git a/.gitignore b/.gitignore index 1cda3d0..688378f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ /vendor /secrets/cert.pem /secrets/privkey.pem -auth-server +/cmd/auth/auth From 1b7182226bbc8bbcf072a111564771ba16fc1aff Mon Sep 17 00:00:00 2001 From: reugn Date: Sat, 24 Feb 2024 11:41:28 +0200 Subject: [PATCH 11/14] update versions in CI workflows --- .github/workflows/build.yml | 6 +++--- .github/workflows/docker.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6683a3b..4cc55df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,12 +16,12 @@ jobs: go-version: [1.21.x] steps: - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test - run: go test ./... \ No newline at end of file + run: go test ./... diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8634fb2..c9361c5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Log in to the Container registry uses: docker/login-action@v2 From 92e2b2214f81414326d6b4fec1dd893f0a294279 Mon Sep 17 00:00:00 2001 From: reugn Date: Sat, 24 Feb 2024 11:48:27 +0200 Subject: [PATCH 12/14] refine project documentation --- README.md | 41 +++++++++++------- .../images}/architecture_diagram_1.png | Bin docs/repository_configuration.md | 26 +++++++++++ 3 files changed, 52 insertions(+), 15 deletions(-) rename {images => docs/images}/architecture_diagram_1.png (100%) create mode 100644 docs/repository_configuration.md diff --git a/README.md b/README.md index dbd9d55..6660bd2 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,29 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/reugn/auth-server)](https://pkg.go.dev/github.com/reugn/auth-server) [![Go Report Card](https://goreportcard.com/badge/github.com/reugn/auth-server)](https://goreportcard.com/report/github.com/reugn/auth-server) -This project provides tools to set up a custom authentication and authorization server. -`auth-server` can act as a proxy middleware or be configured in a stand-alone mode. It doesn't require any third-party software integration. Use one of the [available repositories](internal/repository) to configure backend storage, or implement one of your own. +This project offers a toolkit for building and configuring a tailored authentication and authorization service. -**Note:** This project has not yet passed security testing. Make sure you know what you are doing when setting up your own OAuth2 provider. +`auth-server` can act as a proxy middleware or be configured in a stand-alone mode. It doesn't require any third-party software integration. +Leverage existing backend [storage repositories](internal/repository) for storing security policies or develop a custom one to suit your specific requirements. +For information on how to configure repositories using environment variables, refer to the [repository configuration](docs/repository_configuration.md) page. + +> [!NOTE] +> This project's security has not been thoroughly evaluated. Proceed with caution when setting up your own auth provider. ## Introduction * **Authentication** is used by a server when the server needs to know exactly who is accessing their information or site. * **Authorization** is a process by which a server determines if the client has permission to use a resource or access a file. -Creating an authentication and authorization strategy is always a complex process. A number of quick questions immediately arise: +The inherent complexity of crafting an authentication and authorization strategy raises a barrage of immediate questions: -* Should we set up separate services for authentication and authorization -* How do we handle access token creation and who is responsible for this -* Should we alter our REST service to support authorization flow +* Would it be beneficial to utilize separate services for authentication and authorization purposes? +* What is the process for creating access tokens, and who is tasked with this responsibility? +* Is it necessary to adapt our REST service to support an authorization flow? -The `auth-server` project tries to accumulate all of those capabilities and act as a transparent authentication and authorization proxy middleware. +The `auth-server` project aims to address these concerns by serving as a transparent authentication and authorization proxy middleware. ## Architecture -![architecture_diagram](./images/architecture_diagram_1.png) +![architecture_diagram](docs/images/architecture_diagram_1.png) 1. The user requests an access token (JWT), using a basic authentication header: ``` @@ -45,20 +49,27 @@ The `auth-server` project tries to accumulate all of those capabilities and act ## Installation and Prerequisites * `auth-server` is written in Golang. -To install the latest stable version of Go, visit https://golang.org/dl/ +To install the latest stable version of Go, visit the [releases page](https://golang.org/dl/). + +* Read the following [instructions](./secrets/README.md) to generate keys required to sign the token. Specify the location of the generated certificates in the service configuration file. An example of the configuration file can be found [here](config/service_config.yml). + +* The following example shows how to run the service using a configuration file: + ``` + ./auth -c service_config.yml + ``` * To run the project using Docker, visit their [page](https://www.docker.com/get-started) to get started. Docker images are available under the [GitHub Packages](/~https://github.com/reugn/auth-server/packages). * Install `docker-compose` to get started with the examples. -* Read the following [instructions](./secrets/README.md) to generate keys. - ## Examples -Examples are available under the examples folder. +Examples are available under the [examples](examples) folder. To run `auth-server` as a [Traefik](https://docs.traefik.io/) middleware: -* `cd examples/traefik` -* `docker-compose up -d` +``` +cd examples/traefik +docker-compose up -d +``` ## License Licensed under the Apache 2.0 License. diff --git a/images/architecture_diagram_1.png b/docs/images/architecture_diagram_1.png similarity index 100% rename from images/architecture_diagram_1.png rename to docs/images/architecture_diagram_1.png diff --git a/docs/repository_configuration.md b/docs/repository_configuration.md new file mode 100644 index 0000000..5266c03 --- /dev/null +++ b/docs/repository_configuration.md @@ -0,0 +1,26 @@ +## Repository configuration +Repositories can be configured using environment variables. +Find below the lists of available configuration properties per provider. + +### Vault +| Environment variable | Default value | Description +| --- | --- | --- +| AUTH_SERVER_VAULT_ADDR | localhost:8200 | The address of the Vault server +| AUTH_SERVER_VAULT_TOKEN | | Vault token +| AUTH_SERVER_VAULT_BASIC_KEY | secret/basic | Basic authentication secret key prefix +| AUTH_SERVER_VAULT_AUTHORIZATION_KEY | secret/authorization | Authorization secret key prefix + +### Aerospike +| Environment variable | Default value | Description +| --- | --- | --- +| AUTH_SERVER_AEROSPIKE_HOST | localhost | The Aerospike cluster seed host +| AUTH_SERVER_AEROSPIKE_PORT | 3000 | The Aerospike cluster seed port +| AUTH_SERVER_AEROSPIKE_NAMESPACE | test | The name of the namespace containing auth details +| AUTH_SERVER_AEROSPIKE_SETNAME | auth | The name of the set containing auth details +| AUTH_SERVER_AEROSPIKE_BASIC_KEY | basic | The key of the record containing the basic authentication details +| AUTH_SERVER_AEROSPIKE_AUTHORIZATION_KEY | authorization | The key of the record containing the authorization details + +### Local +| Environment variable | Default value | Description +| --- | --- | --- +| AUTH_SERVER_LOCAL_CONFIG_PATH | config/local_repository_config.yml | The path to the file with the local repository configuration From 6ccec022c04ffea2bef16bf19a6843d0d372e6c5 Mon Sep 17 00:00:00 2001 From: reugn Date: Sat, 24 Feb 2024 12:00:31 +0200 Subject: [PATCH 13/14] skip JWT tests when keys are not available --- internal/auth/jwt_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go index 64ff5f3..70f4cca 100644 --- a/internal/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -21,7 +21,7 @@ func TestJWT_Authorize(t *testing.T) { } keys, err := NewKeys(secretConfig) if err != nil { - t.Fatal(err) + t.Skip("keys are not available") } tokenGenerator := NewJWTGenerator(keys, jwt.SigningMethodRS256) tokenValidator := NewJWTValidator(keys, repo) From c058c60e1cc7e6a191df39b30573709c933f56b2 Mon Sep 17 00:00:00 2001 From: reugn Date: Sat, 24 Feb 2024 12:42:06 +0200 Subject: [PATCH 14/14] add missing configuration to the golangci-lint workflow --- .github/workflows/golangci-lint.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2209ddf..6ed9d42 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,6 +6,9 @@ on: - master pull_request: +permissions: + contents: read + jobs: golangci: name: lint @@ -13,6 +16,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 + with: + version: v1.56