Added JWT Authentication

This commit is contained in:
Ethan Knowlton 2023-10-10 15:52:05 -04:00
parent 712e2166d5
commit d9bf3259e0
9 changed files with 112 additions and 19 deletions

View File

@ -1,5 +1,18 @@
# Changelog
## 1.2.0 (2023-10-06)
### Features
- JWT Authentication
## 1.1.0 (2023-10-06)
### Features
- Environment Variable Configuration [712e216](https://github.com/levelsoftware/echoip/commit/712e2166d51fdb85229f52caa380743245f31dfa)
## 1.0.0 (2023-10-06)
### Features

View File

@ -133,7 +133,7 @@ AsnFile = ""
```
### Environment Variables for Configuration
You can also use environment variables for configuration, most likely used for Docker.
You can also use environment variables for configuration, most likely used for Docker. Configuration file takes precedence first, and then environment variables. Remove the value from the config file if you wish to use the environment variable.
```
ECHOIP_LISTEN=":8080"

View File

@ -102,14 +102,20 @@ func main() {
serverCache = &cache.Null{}
}
server := http.New(parser, serverCache, runConfig.CacheTtl, runConfig.Profile)
server := http.New(parser, serverCache, &runConfig)
server.IPHeaders = runConfig.TrustedHeaders
if _, err := os.Stat(runConfig.TemplateDir); err == nil {
server.Template = runConfig.TemplateDir
} else {
if _, err := os.Stat(runConfig.TemplateDir); err != nil {
runConfig.TemplateDir = ""
log.Printf("Not configuring default handler: Template not found: %s", runConfig.TemplateDir)
}
if runConfig.Jwt.Enabled {
log.Println("Enabling JWT Auth")
if len(runConfig.Jwt.Secret) == 0 {
log.Fatal("Please provide a JWT Token secret when JWT is enabled")
}
}
if runConfig.ReverseLookup {
log.Println("Enabling reverse lookup")
server.LookupAddr = iputil.LookupAddr
@ -118,9 +124,9 @@ func main() {
log.Println("Enabling port lookup")
server.LookupPort = iputil.LookupPort
}
if runConfig.ShowSponsor {
log.Println("Enabling sponsor logo")
server.Sponsor = runConfig.ShowSponsor
}
if len(runConfig.TrustedHeaders) > 0 {
log.Printf("Trusting remote IP from header(s): %s", runConfig.TrustedHeaders)

View File

@ -18,6 +18,11 @@ type GeoIP struct {
AsnFile string
}
type Jwt struct {
Enabled bool
Secret string
}
type Config struct {
Listen string
TemplateDir string
@ -30,9 +35,11 @@ type Config struct {
Database string
Profile bool
Debug bool
IPStack IPStack
GeoIP GeoIP
Jwt Jwt
}
func GetConfig() (Config, error) {
@ -41,6 +48,9 @@ func GetConfig() (Config, error) {
TemplateDir: getenv_string("ECHOIP_TEMPLATE_DIR", "html/"),
RedisUrl: getenv_string("ECHOIP_REDIS_URL", ""),
Database: getenv_string("ECHOIP_DATABASE", "geoip"),
Jwt: Jwt{
Secret: getenv_string("ECHOIP_JWT_SECRET", ""),
},
IPStack: IPStack{
ApiKey: getenv_string("ECHOIP_IPSTACK_API_KEY", ""),
},
@ -51,6 +61,18 @@ func GetConfig() (Config, error) {
},
}
jwtAuthEnabled, err := getenv_bool("ECHOIP_JWT_AUTH", false)
if err != nil {
return Config{}, err
}
defaultConfig.Jwt.Enabled = jwtAuthEnabled
debug, err := getenv_bool("ECHOIP_DEBUG", false)
if err != nil {
return Config{}, err
}
defaultConfig.Debug = debug
cacheTtl, err := getenv_int("ECHOIP_CACHE_TTL", 3600)
if err != nil {
return Config{}, err

View File

@ -7,7 +7,12 @@ PortLookup = true
ShowSponsor = true
Database = "ipstack" # use "IP Stack" or "GeoIP"
TrustedHeaders = []
Profile = false # enable debug / profiling
Profile = false # enable profiling
Debug = false # enable debugging, ex print jwt token errors
[Jwt]
Enabled = false
Secret = ""
[IPStack]
ApiKey = "HelloWorld"

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.18
require (
github.com/BurntSushi/toml v1.3.2
github.com/go-redis/cache/v9 v9.0.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/oschwald/geoip2-golang v1.5.0
github.com/qioalice/ipstack v1.0.1
github.com/redis/go-redis/v9 v9.2.1

2
go.sum
View File

@ -21,6 +21,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0=
github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=

View File

@ -1,6 +1,9 @@
package http
import "net/http"
import (
"errors"
"net/http"
)
type appError struct {
Error error
@ -22,6 +25,11 @@ func notFound(err error) *appError {
}
func badRequest(err error) *appError {
badAuth := new(InvalidTokenError)
if errors.As(err, &badAuth) {
return &appError{Error: err, Code: http.StatusUnauthorized}
}
return &appError{Error: err, Code: http.StatusBadRequest}
}

View File

@ -12,7 +12,9 @@ import (
"net/http/pprof"
rcache "github.com/go-redis/cache/v9"
"github.com/golang-jwt/jwt"
"github.com/levelsoftware/echoip/cache"
"github.com/levelsoftware/echoip/config"
parser "github.com/levelsoftware/echoip/iputil/paser"
"github.com/levelsoftware/echoip/useragent"
@ -27,15 +29,12 @@ const (
)
type Server struct {
Template string
IPHeaders []string
LookupAddr func(net.IP) (string, error)
LookupPort func(net.IP, uint64) error
cache cache.Cache
cacheTtl int
runConfig *config.Config
parser parser.Parser
profile bool
Sponsor bool
}
type PortResponse struct {
@ -44,8 +43,8 @@ type PortResponse struct {
Reachable bool `json:"reachable"`
}
func New(parser parser.Parser, cache cache.Cache, cacheTtl int, profile bool) *Server {
return &Server{cache: cache, cacheTtl: cacheTtl, parser: parser, profile: profile}
func New(parser parser.Parser, cache cache.Cache, runConfig *config.Config) *Server {
return &Server{cache: cache, parser: parser, runConfig: runConfig}
}
func ipFromForwardedForHeader(v string) string {
@ -104,6 +103,10 @@ func userAgentFromRequest(r *http.Request) *useragent.UserAgent {
}
func (s *Server) newResponse(r *http.Request) (parser.Response, error) {
if err := handleAuth(r, s.runConfig); err != nil {
return parser.Response{}, err
}
ctx := context.Background()
ip, err := ipFromRequest(s.IPHeaders, r, true)
@ -130,7 +133,7 @@ func (s *Server) newResponse(r *http.Request) (parser.Response, error) {
response, err = s.parser.Parse(ip, hostname)
log.Printf("Caching response for %s", ip.String())
if err := s.cache.Set(ctx, ip.String(), cachedResponse.Build(response), s.cacheTtl); err != nil {
if err := s.cache.Set(ctx, ip.String(), cachedResponse.Build(response), s.runConfig.CacheTtl); err != nil {
return parser.Response{}, err
}
@ -158,6 +161,10 @@ func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) {
}
func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
if err := handleAuth(r, s.runConfig); err != nil {
return badRequest(err).WithMessage(err.Error()).AsJSON()
}
ip, err := ipFromRequest(s.IPHeaders, r, true)
if err != nil {
return badRequest(err).WithMessage(err.Error()).AsJSON()
@ -259,10 +266,12 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
if err != nil {
return badRequest(err).WithMessage(err.Error())
}
t, err := template.ParseGlob(s.Template + "/*")
t, err := template.ParseGlob(s.runConfig.TemplateDir + "/*")
if err != nil {
return internalServerError(err)
}
json, err := json.MarshalIndent(response, "", " ")
if err != nil {
return internalServerError(err)
@ -287,7 +296,7 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
response.Longitude + 0.05,
string(json),
s.LookupPort != nil,
s.Sponsor,
s.runConfig.ShowSponsor,
}
if err := t.Execute(w, &data); err != nil {
@ -373,7 +382,7 @@ func (s *Server) Handler() http.Handler {
}
// Browser
if s.Template != "" {
if s.runConfig.TemplateDir != "" {
r.Route("GET", "/", s.DefaultHandler)
}
@ -383,7 +392,7 @@ func (s *Server) Handler() http.Handler {
}
// Profiling
if s.profile {
if s.runConfig.Profile {
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
@ -401,3 +410,30 @@ func (s *Server) ListenAndServe(addr string) error {
func formatCoordinate(c float64) string {
return strconv.FormatFloat(c, 'f', 6, 64)
}
type InvalidTokenError struct{}
func (m *InvalidTokenError) Error() string {
return "invalid_token"
}
func handleAuth(r *http.Request, runConfig *config.Config) error {
if !runConfig.Jwt.Enabled {
return nil
}
authorization := r.Header.Get("Authorization")
tokenString := strings.ReplaceAll(authorization, "Bearer ", "")
if _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(runConfig.Jwt.Secret), nil
}); err != nil {
if runConfig.Debug {
log.Printf("Error validating token ( %s ): %s \n", tokenString, err)
}
return new(InvalidTokenError)
}
return nil
}