diff --git a/api/rest-api.go b/api/rest-api.go index b9278ef..dc15bb4 100644 --- a/api/rest-api.go +++ b/api/rest-api.go @@ -4,28 +4,42 @@ import ( "encoding/json" "fmt" "net/http" + "regexp" + "time" "weather-data/config" "weather-data/storage" "weather-data/weathersource" + "github.com/dgrijalva/jwt-go" "github.com/google/uuid" "github.com/gorilla/handlers" "github.com/gorilla/mux" ) +var bearerTokenRegexPattern = "^(?i:Bearer\\s+)([A-Za-z0-9-_=]+\\.[A-Za-z0-9-_=]+\\.?[A-Za-z0-9-_.+\\/=]*)$" + +var bearerTokenRegex *regexp.Regexp = regexp.MustCompile(bearerTokenRegexPattern) + +type customClaims struct { + Username string `json:"username"` + jwt.StandardClaims +} + type weatherRestApi struct { connection string + config config.RestConfig weaterStorage storage.WeatherStorage weatherSource weathersource.WeatherSourceBase sensorRegistry storage.SensorRegistry } //SetupAPI sets the REST-API up -func NewRestAPI(connection string, weatherStorage storage.WeatherStorage, sensorRegistry storage.SensorRegistry) *weatherRestApi { +func NewRestAPI(connection string, weatherStorage storage.WeatherStorage, sensorRegistry storage.SensorRegistry, config config.RestConfig) *weatherRestApi { api := new(weatherRestApi) api.connection = connection api.weaterStorage = weatherStorage api.sensorRegistry = sensorRegistry + api.config = config return api } @@ -33,7 +47,7 @@ func NewRestAPI(connection string, weatherStorage storage.WeatherStorage, sensor func (api *weatherRestApi) Start() error { router := api.handleRequests() - originsOk := handlers.AllowedOrigins([]string{config.RestConfiguration.AccessControlAllowOriginHeader}) + originsOk := handlers.AllowedOrigins([]string{api.config.AccessControlAllowOriginHeader}) return http.ListenAndServe(api.connection, handlers.CORS(originsOk)(router)) } @@ -48,10 +62,13 @@ func (api *weatherRestApi) handleRequests() *mux.Router { router.HandleFunc("/", api.homePageHandler) + //random weather data router.HandleFunc("/{_dummy:(?i)random}", api.randomWeatherHandler).Methods("GET") router.HandleFunc("/{_dummy:(?i)randomlist}", api.randomWeatherListHandler).Methods("GET") + //sensor specific stuff sensorRouter := router.PathPrefix("/{_dummy:(?i)sensor}").Subrouter() + sensorRouter.Use(api.IsAuthorized) sensorRouter.HandleFunc("/{id}/{_dummy:(?i)weather-data}", api.getWeatherDataHandler).Methods("GET") sensorRouter.HandleFunc("/{id}/{_dummy:(?i)weather-data}", api.addWeatherDataHandler).Methods("POST") @@ -61,7 +78,13 @@ func (api *weatherRestApi) handleRequests() *mux.Router { sensorRouter.HandleFunc("/{id}", api.updateWeatherSensorHandler).Methods("PUT") sensorRouter.HandleFunc("/{id}", api.deleteWeatherSensorHandler).Methods("DELETE") + //registration router.HandleFunc("/{_dummy:(?i)register/sensor}/{name}", api.registerWeatherSensorHandler).Methods("POST") + + //token generation + if api.config.AllowTokenGeneration { + router.HandleFunc("/{_dummy:(?i)generateToken}", api.generateToken).Methods("GET") + } return router } @@ -83,6 +106,28 @@ func (api *weatherRestApi) randomWeatherListHandler(w http.ResponseWriter, r *ht json.NewEncoder(w).Encode(storage.ToMap(datapoints)) } +func (api *weatherRestApi) generateToken(w http.ResponseWriter, r *http.Request) { + claims := customClaims{ + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Minute * 30).Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signedToken, err := token.SignedString([]byte(config.RestConfiguration.JwtTokenSecret)) + if err != nil { + return + } + + response := map[string]string{ + "Autohrization": signedToken, + } + + w.Header().Add("content-type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + func (api *weatherRestApi) getWeatherDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] @@ -247,6 +292,41 @@ func (api *weatherRestApi) homePageHandler(w http.ResponseWriter, r *http.Reques fmt.Fprintf(w, "Welcome to the Weather API!") } +func (api *weatherRestApi) IsAuthorized(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !api.config.UseTokenAuthorization { + next.ServeHTTP(w, r) + return + } + + authorizationHeader := r.Header["Authorization"] + if authorizationHeader == nil { + http.Error(w, "no bearer token", http.StatusUnauthorized) + return + } + + jwtFromHeader := bearerTokenRegex.FindStringSubmatch(authorizationHeader[0])[1] + + token, err := jwt.ParseWithClaims( + jwtFromHeader, + &customClaims{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(api.config.JwtTokenSecret), nil + }, + ) + + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + if token.Valid { + next.ServeHTTP(w, r) + return + } + }) +} + //AddNewWeatherDataCallback adds a new callbackMethod for incoming weather data func (api *weatherRestApi) AddNewWeatherDataCallback(callback weathersource.NewWeatherDataCallbackFunc) { api.weatherSource.AddNewWeatherDataCallback(callback) diff --git a/config/config.go b/config/config.go index 68803b4..2f1a1d4 100644 --- a/config/config.go +++ b/config/config.go @@ -32,6 +32,9 @@ type MqttConfig struct { type RestConfig struct { AccessControlAllowOriginHeader string + UseTokenAuthorization bool + AllowTokenGeneration bool + JwtTokenSecret string } var MongoConfiguration = MongoConfig{ @@ -60,6 +63,9 @@ var MqttConfiguration = MqttConfig{ var RestConfiguration = RestConfig{ AccessControlAllowOriginHeader: getEnv("ACCESS_CONTROL_ALLOW_ORIGIN_HEADER", "*"), + UseTokenAuthorization: getEnvBool("USE_TOKEN_AUTHORIZATION", false), + AllowTokenGeneration: getEnvBool("ALLOW_TOKEN_GENERATION", false), + JwtTokenSecret: getEnv("JWT_TOKEN_SECRET", "jwt-token-secret"), } var AllowUnregisteredSensors = getEnvBool("ALLOW_UNREGISTERED_SENSORS", false) diff --git a/go.mod b/go.mod index 523284b..e4e15c0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module weather-data go 1.16 require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/eclipse/paho.mqtt.golang v1.3.2 github.com/google/uuid v1.2.0 github.com/gorilla/handlers v1.5.1 // indirect diff --git a/go.sum b/go.sum index d3cf5f8..42972fa 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,7 @@ 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/deepmap/oapi-codegen v1.3.13 h1:9HKGCsdJqE4dnrQ8VerFS0/1ZOJPmAhN+g8xgp8y3K4= github.com/deepmap/oapi-codegen v1.3.13/go.mod h1:WAmG5dWY8/PYHt4vKxlt90NsbHMAOCiteYKZMiIRfOo= +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/eclipse/paho.mqtt.golang v1.3.2 h1:ICzfxSyrR8bOsh9l8JBBOwO1tc2C26oEyody0ml0L6E= github.com/eclipse/paho.mqtt.golang v1.3.2/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= diff --git a/main.go b/main.go index a8e55a3..8f03230 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ func main() { weatherSource.AddNewWeatherDataCallback(handleNewWeatherData) //setup a API -> REST - weatherAPI = api.NewRestAPI(":10000", weatherStorage, sensorRegistry) + weatherAPI = api.NewRestAPI(":10000", weatherStorage, sensorRegistry, config.RestConfiguration) defer weatherAPI.Close() weatherAPI.AddNewWeatherDataCallback(handleNewWeatherData)