From c22cb74864e05f2a13c0c5fcab3d28677813e8c9 Mon Sep 17 00:00:00 2001 From: Sebastian Tobie Date: Sat, 9 Jan 2021 21:39:05 +0100 Subject: [PATCH] first version of my httpserver --- auth/auth.go | 59 +++++++++++++ auth/enums.go | 7 ++ go.mod | 13 +++ go.sum | 91 ++++++++++++++++++++ http.go | 103 +++++++++++++++++++++++ middleware/middleware.go | 41 +++++++++ modules/saml/account.go | 104 +++++++++++++++++++++++ modules/saml/funcs.go | 98 ++++++++++++++++++++++ modules/saml/saml.go | 176 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 692 insertions(+) create mode 100644 auth/auth.go create mode 100644 auth/enums.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 http.go create mode 100644 middleware/middleware.go create mode 100644 modules/saml/account.go create mode 100644 modules/saml/funcs.go create mode 100644 modules/saml/saml.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..322b845 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,59 @@ +package auth + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// AuthenticationHandler is an interface that is used to give the account of the request. +// it is set into the context using gin.Context.Set. +// The context must be reused for the redirect. +// Account NEVER returns nil. +// Not loggedin Users have return Anonymous() = true and get() = nil and List() = []string{} +type AuthenticationHandler interface { + Account(*gin.Context) Account +} + +// Account is an interface that gives the application access to infos about the user. +type Account interface { + Get(string) interface{} + List() []string + Anonymous() bool + Redirect(c *gin.Context) +} + +// AnonAccountHandler is an simple struct that fullfills the AuthenticationHandler Interface +type AnonAccountHandler struct{} + +// Account is an simple method that returns an Account that is always anonymous. +func (*AnonAccountHandler) Account(*gin.Context) Account { + return &AnonAccount{} +} + +// AnonAccount is an simple Account-interface implementation. +// It is always Anonymous +type AnonAccount struct{} + +// Get returns only AccountAnon = true +func (*AnonAccount) Get(key string) (in interface{}) { + if key == AccountAnon { + return true + } + return +} + +// List return only AccountAnon as the only Listitem +func (*AnonAccount) List() []string { + return []string{AccountAnon} +} + +// Anonymous is always true +func (*AnonAccount) Anonymous() bool { + return true +} + +// Redirect should point to an login, but since its not possible for this handler it sends an 401 Page +func (*AnonAccount) Redirect(c *gin.Context) { + c.AbortWithStatus(http.StatusForbidden) +} diff --git a/auth/enums.go b/auth/enums.go new file mode 100644 index 0000000..a58a36e --- /dev/null +++ b/auth/enums.go @@ -0,0 +1,7 @@ +package auth + +const ( + AccountID string = "jti" + AccountAnon string = "anon" + AccountUser string = "uid" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7e2dd5b --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module go.sebtobie.de/httpserver + +go 1.15 + +require ( + github.com/crewjam/saml v0.4.5 + github.com/gin-gonic/gin v1.6.3 + github.com/google/uuid v1.1.4 + github.com/pelletier/go-toml v1.8.1 + github.com/phuslu/log v1.0.58 + github.com/pkg/errors v0.9.1 // indirect + gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e975c0e --- /dev/null +++ b/go.sum @@ -0,0 +1,91 @@ +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da h1:WXnT88cFG2davqSFqvaFfzkSMC0lqh/8/rKZ+z7tYvI= +github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da/go.mod h1:+rmNIXRvYMqLQeR4DHyTvs6y0MEMymTz4vyFpFkKTPs= +github.com/crewjam/saml v0.4.5 h1:H9u+6CZAESUKHxMyxUbVn0IawYvKZn4nt3d4ccV4O/M= +github.com/crewjam/saml v0.4.5/go.mod h1:qCJQpUtZte9R1ZjUBcW8qtCNlinbO363ooNl02S68bk= +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/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0= +github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.2.1 h1:S/EaQvW6FpWMYAvYvY+OBDvpaM+izu0oiwo5y0MH7U0= +github.com/jonboulle/clockwork v0.2.1/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e h1:qqXczln0qwkVGcpQ+sQuPOVntt2FytYarXXxYSNJkgw= +github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/phuslu/log v1.0.58 h1:4jZhb7HxL/ki1ldm9sA3zreLfMwyRZ6CtJwzRR9ORw0= +github.com/phuslu/log v1.0.58/go.mod h1:kzJN3LRifrepxThMjufQwS7S35yFAB+jAV1qgA7eBW4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk= +github.com/russellhaering/goxmldsig v1.1.0/go.mod h1:QK8GhXPB3+AfuCrfo0oRISa9NfzeCpWmxeGnqEpDF9o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/zenazn/goji v0.9.1-0.20160507202103-64eb34159fe5/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI= +gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go new file mode 100644 index 0000000..8b9d444 --- /dev/null +++ b/http.go @@ -0,0 +1,103 @@ +package httpserver + +import ( + "crypto/tls" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pelletier/go-toml" + "github.com/phuslu/log" + "go.sebtobie.de/httpserver/auth" +) + +// Config that is used to map the toml config to the settings that are used. +type Config struct { + Addr string + TLSconfig *tls.Config + Certfile string + Keyfile string +} + +// MarshalLogObject adds the information over the object to the *log.Entry +func (c *Config) MarshalLogObject(e *log.Entry) { + e.Str("Address", c.Addr).Bool("TLS", c.TLSconfig != nil).Strs("Cert", []string{c.Certfile, c.Keyfile}) +} + +// Server is an wrapper for the *http.Server and *gin.Engine +type Server struct { + http *http.Server + conf *Config + router *gin.Engine + config *toml.Tree + authhf auth.AuthenticationHandler +} + +// StartServer starts the server as configured and sends the errormessage to the log. +func (s *Server) StartServer() { + log.Info().Msg("Starting server") + var err error + if s.conf.Certfile != "" && s.conf.Keyfile != "" { + err = s.http.ListenAndServeTLS(s.conf.Certfile, s.conf.Keyfile) + + } else { + err = s.http.ListenAndServe() + } + if err != http.ErrServerClosed { + log.Error().Err(err).Msg("Server unexpected exited") + } +} + +// CreateServer creates an server that can be run in a coroutine. +func CreateServer(config *toml.Tree) *Server { + log.Info().Msg("Redirect logging output to phuslu/log") + gin.DefaultErrorWriter = log.DefaultLogger.Std(log.ErrorLevel, log.Context{}, "GIN", 0).Writer() + gin.DefaultWriter = log.DefaultLogger.Std(log.DebugLevel, log.Context{}, "GIN", 0).Writer() + log.Info().Msg("Creating HTTP-Server") + var server = &Server{ + conf: &Config{ + Addr: "127.0.0.1:8080", + }, + router: gin.New(), + authhf: &auth.AnonAccountHandler{}, + } + server.router.Use(func(c *gin.Context) { + c.Set("account", server.authhf.Account(c)) + }) + if err := config.Unmarshal(server.conf); err != nil { + log.Error().Msg("Problem mapping config to Configstruct") + } + log.Debug().EmbedObject(server.conf).Msg("Config") + server.http = &http.Server{ + Addr: server.conf.Addr, + ErrorLog: log.DefaultLogger.Std(log.ErrorLevel, log.Context{}, "", 0), + Handler: server.router, + TLSConfig: server.conf.TLSconfig, + } + server.router.NoRoute(gin.WrapH(http.NotFoundHandler())) + gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { + log.Trace().Msgf("%-4s(%02d): %-20s %s", httpMethod, nuHandlers-1, absolutePath, handlerName) + } + return server +} + +// Use installs the middleware into the router. +// The Middleware must be able to detect multiple calls byy itself. Deduplication is not performed. +func (s *Server) Use(m ...gin.HandlerFunc) { s.router.Use(m...) } + +// UseAuthBackend is the funćtion that sets the Handler for the authentication +func (s *Server) UseAuthBackend(a auth.AuthenticationHandler) { + s.authhf = a +} + +// Site is an Interface to abstract the modularized group of pages. +// The Middleware must be able to detect multiple calls byy itself. Deduplication is not performed. +type Site interface { + Init(*gin.RouterGroup) + Middleware() []gin.HandlerFunc +} + +// RegisterSite adds an site to the engine as its own grouo +func (s *Server) RegisterSite(path string, site Site) { + site.Init(s.router.Group(path, site.Middleware()...)) + return +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..0f3e053 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,41 @@ +package httpserver + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/phuslu/log" +) + +// LogMiddleware is an middleware to log requests to phuslu/log +func LogMiddleware(c *gin.Context) { + var xid log.XID + var tmp interface{} + var exists bool + if tmp, exists = c.Get("xid"); !exists { + xid = log.NewXIDWithTime(time.Now().UnixNano()) + c.Set("xid", xid) + } else { + xid = tmp.(log.XID) + } + defer func() { + var entry *log.Entry + int := recover() + if int != nil { + err := int.(error) + c.Header("requestid", xid.String()) + c.AbortWithStatus(http.StatusInternalServerError) + entry = log.Error().Err(err).Int("statuscode", 500) + } else { + if c.Writer.Status() >= 400 { + entry = log.Error() + } else { + entry = log.Info() + } + entry = entry.Int("statuscode", c.Writer.Status()) + } + entry.Int64("goroutine", log.Goid()).Xid("ID", xid).Msg("Request") + }() + c.Next() +} diff --git a/modules/saml/account.go b/modules/saml/account.go new file mode 100644 index 0000000..33e2a5d --- /dev/null +++ b/modules/saml/account.go @@ -0,0 +1,104 @@ +package saml + +import ( + "fmt" + "net/http" + "time" + + "github.com/crewjam/saml" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/phuslu/log" + "go.sebtobie.de/httpserver" + "gopkg.in/dgrijalva/jwt-go.v3" +) + +var defaccount = &account{ + data: map[string]interface{}{ + httpserver.AccountID: "", + httpserver.AccountAnon: true, + }, +} + +func maptoarray(m map[string]interface{}) (output []interface{}) { + for k, v := range m { + output = append(output, []interface{}{k, v}) + } + return +} + +// Account returns the Account representation of the user +func (s *SAML) Account(c *gin.Context) httpserver.Account { + acc := &(*defaccount) + acc.s = s + cookie, err := c.Cookie(s.Cookiename) + if err != nil { + log.Debug().Err(err).Msg("Cookie error") + return acc + } + var ( + claim *jwt.MapClaims + ok bool + ) + token, err := jwt.ParseWithClaims(cookie, &jwt.MapClaims{}, s.signingkey) + if err != nil { + log.Debug().Err(err).Msg("Error while parsing token") + } + if claim, ok = token.Claims.(*jwt.MapClaims); ok && token.Valid { + log.Debug().KeysAndValues(claim).Msg("Got valid token") + acc.data = *claim + return acc + } + log.Debug().Bool("valid", token.Valid).KeysAndValues(maptoarray(*claim)...).Msg("problem vith token") + return acc +} + +func (s *SAML) signingkey(token *jwt.Token) (key interface{}, err error) { + if _, ok := token.Method.(*jwt.SigningMethodRSAPSS); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return &s.jwtprivatekey.PublicKey, nil +} + +type account struct { + s *SAML + data map[string]interface{} +} + +func (a *account) Anonymous() bool { + return a.data[httpserver.AccountAnon].(bool) +} + +func (a *account) Redirect(c *gin.Context) { + id := uuid.New().String() + tokenstring, err := jwttoken(jwt.MapClaims{ + httpserver.AccountID: id, + httpserver.AccountAnon: true, + }, a.s.jwtprivatekey) + if err != nil { + log.Error().Err(err).Msg("Failed to generate the token") + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.SetCookie(a.s.Cookiename, tokenstring, int(time.Hour), "", "", true, true) + request, err := a.s.sp.MakeAuthenticationRequest(a.s.sp.GetSSOBindingLocation(saml.HTTPRedirectBinding)) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + } + request.ID = id + log.Debug().Msgf("Leite weiter: %s", request.Redirect(c.Request.URL.String()).String()) + c.Redirect(http.StatusSeeOther, request.Redirect(c.Request.URL.String()).String()) +} + +func (a *account) Get(key string) interface{} { + return a.data[key] +} + +func (a *account) List() []string { + liste := make([]string, len(a.data)) + for key := range a.data { + liste = append(liste, key) + } + return liste +} diff --git a/modules/saml/funcs.go b/modules/saml/funcs.go new file mode 100644 index 0000000..4a5f151 --- /dev/null +++ b/modules/saml/funcs.go @@ -0,0 +1,98 @@ +package saml + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/crewjam/saml" + "github.com/phuslu/log" + "gopkg.in/dgrijalva/jwt-go.v3" +) + +func initcert(file string, verify func(interface{}) bool) (key interface{}, err error) { + var blocks []*pem.Block + if file == "" { + err = errors.New("SPPrivatekey empty") + return + } + blocks, err = loadcerts(file) + if err != nil { + return nil, err + } + for _, b := range blocks { + var key interface{} + key, err = x509.ParsePKCS8PrivateKey(b.Bytes) + if err != nil { + log.Warn().AnErr("parsingerror", err).Msgf("could not parse file %s as privatkey", file) + key, err = x509.ParseCertificate(b.Bytes) + if err != nil { + log.Warn().AnErr("parsingerror", err).Msgf("could not parse file %s as publickey", file) + continue + } + } + if key == nil { + return nil, fmt.Errorf("Cannot find an Key in %s", file) + } + if verify(key) { + return key, nil + } + } + return nil, errors.New("No key found") +} + +func loadcerts(filename string) (blocks []*pem.Block, err error) { + var file *os.File + if file, err = os.Open(filename); err != nil { + return + } + var buffer bytes.Buffer + read, err := buffer.ReadFrom(file) + log.Debug().Int64("read bytes", read).Err(err).Msgf("Read file %s", filename) + if err != nil { + return + } + data := buffer.Bytes() + var block *pem.Block + for { + if len(data) == 0 { + return + } + block, data = pem.Decode(data) + if block != nil { + blocks = append(blocks, block) + } + } +} +func empty(data []string) bool { + for _, t := range data { + if t == "" { + return true + } + } + return false +} + +func jwttoken(claim jwt.Claims, privatekey interface{}) (string, error) { + token := jwt.NewWithClaims( + jwt.SigningMethodPS512, + claim, + ) + return token.SignedString(privatekey) +} + +func attributeStatementstomap(a []saml.AttributeStatement) map[string][]string { + var output = map[string][]string{} + for _, b := range a { + for _, c := range b.Attributes { + output[c.FriendlyName] = []string{} + for _, d := range c.Values { + output[c.FriendlyName] = append(output[c.FriendlyName], d.Value) + } + } + } + return output +} diff --git a/modules/saml/saml.go b/modules/saml/saml.go new file mode 100644 index 0000000..bea9b9c --- /dev/null +++ b/modules/saml/saml.go @@ -0,0 +1,176 @@ +package saml + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/gin-gonic/gin" + "github.com/pelletier/go-toml" + "github.com/phuslu/log" + "go.sebtobie.de/httpserver" + "gopkg.in/dgrijalva/jwt-go.v3" +) + +const ( + HJWT = "jwt" + HSPPrivate = "sppriv" + HSPPublic = "sppub" +) + +var ( + defaultsaml = &SAML{ + Selfsigned: false, + UnkownAuthority: false, + IDP: "https://samltest.id/saml/idp", + Domain: "example.com", + Cookiename: "ILOVECOOKIES", + } +) + +type metadata struct{} + +// SAML is an Applicance to react on Events from the SAML-IDP and that provides an interface to get data from the IDP in a standartised fashion. +type SAML struct { + router *gin.RouterGroup + config *toml.Tree + publicroot string + Keyfiles []string + SPPublicKey string + sppublickey *x509.Certificate + SPPrivatekey string + spprivatekey *rsa.PrivateKey + JWTPrivatekey string + jwtprivatekey *rsa.PrivateKey + Selfsigned bool + UnkownAuthority bool + IDP string `comment:"URL of the Metadata of the IDP"` + sp *saml.ServiceProvider + HttpClient http.Client `toml:"-"` + Domain string + Cookiename string +} + +// NewSAMLEndpoint creates an endpoint which handles SAML Requests. +func NewSAMLEndpoint(config *toml.Tree) (*SAML, error) { + var key interface{} + var err error + var s SAML = *defaultsaml + s.config = config + if err := config.Unmarshal(&s); err != nil { + log.Error().Err(err).Msg("Error while mapping config to struct") + return nil, err + } + log.Trace().Interface("config", config).Msg("cofnig") + key, err = initcert(s.SPPrivatekey, func(key interface{}) bool { + _, ok := key.(*rsa.PrivateKey) + return ok + }) + if err != nil { + return nil, err + } + s.spprivatekey = key.(*rsa.PrivateKey) + + key, err = initcert(s.SPPublicKey, func(key interface{}) bool { + _, ok := key.(*x509.Certificate) + return ok + }) + if err != nil { + return nil, err + } + s.sppublickey = key.(*x509.Certificate) + + key, err = initcert(s.SPPrivatekey, func(key interface{}) bool { + _, ok := key.(*rsa.PrivateKey) + return ok + }) + if err != nil { + return nil, err + } + s.jwtprivatekey = key.(*rsa.PrivateKey) + s.sp = &saml.ServiceProvider{ + Key: s.spprivatekey, + Certificate: s.sppublickey, + } + var idpurl *url.URL + idpurl, err = url.ParseRequestURI(s.IDP) + if err != nil { + return nil, err + } + s.sp.IDPMetadata, err = samlsp.FetchMetadata(context.Background(), &s.HttpClient, *idpurl) + if err != nil { + return nil, err + } + s.sp.AuthnNameIDFormat = saml.UnspecifiedNameIDFormat + return &s, nil +} + +// Init initalizes the routes +func (s *SAML) Init(router *gin.RouterGroup) { + s.publicroot = router.BasePath() + s.router = router + s.sp.AcsURL = url.URL{ + Scheme: "https", + Host: s.Domain, + Path: s.publicroot + "/acs", + } + s.sp.MetadataURL = url.URL{ + Scheme: "https", + Host: s.Domain, + Path: s.publicroot + "/metadata.xml", + } + router.GET("/metadata.xml", s.metadataHF) + router.POST("/acs", s.acsHF) +} + +// Middleware returns the Required Middleware +func (s *SAML) Middleware() []gin.HandlerFunc { + return []gin.HandlerFunc{} +} + +func (s *SAML) metadataHF(c *gin.Context) { + m := s.sp.Metadata() + log.Debug().Time("Validuntil", m.ValidUntil).Msg("SP MEtadata") + c.XML(http.StatusOK, m) +} + +func (s *SAML) acsHF(c *gin.Context) { + account := c.MustGet("account").(httpserver.Account) + err := c.Request.ParseForm() + if err != nil { + c.AbortWithError(http.StatusNotAcceptable, err) + } + var assert *saml.Assertion + assert, err = s.sp.ParseResponse(c.Request, []string{account.Get("jti").(string)}) + if err != nil { + realerr, _ := err.(*saml.InvalidResponseError) + err = realerr.PrivateErr + log.Error().AnErr("Assertionerror", err).Msgf("Assertion Error") + fmt.Print(realerr.Response) + c.AbortWithStatus(http.StatusBadRequest) + return + } + data := attributeStatementstomap(assert.AttributeStatements) + token, err := jwttoken(jwt.MapClaims{ + httpserver.AccountAnon: false, + httpserver.AccountID: account.Get(httpserver.AccountID).(string), + httpserver.AccountUser: data["uid"][0], + }, s.jwtprivatekey) + if err != nil { + c.AbortWithStatus(http.StatusInternalServerError) + return + } + c.SetCookie(s.Cookiename, token, int(time.Hour*24*30), "", "", true, true) + redirect, found := c.GetPostForm("RelayState") + if !found { + c.AbortWithStatus(http.StatusNotAcceptable) + return + } + c.Redirect(http.StatusSeeOther, redirect) +}