$ ewserver -part=1

I’ve been doing a lot of small embedded system projects such as weather stations and a simple LCD display of weather/other data I’m interested in. My next big project is reverse engineering all of my home AC unit remotes to build a home automation system. For this I want a simple, but secured HTTPS server where the devices can post any remote controller updates and allow my family to change the AC units from anywhere. Unfortunately, it is surprisingly hard to find a decent web application with authentication and authorization written in Go.

To solve this, I decided to write my own since I needed a few things:

Not use JWT, because it’s crazy

Ability to have my MCUs post data and receive updates securely (so, over HTTPS)

Ability to add new users and manage them (wife, me etc.)

Have session management built in

Have role management built in and configurable from the UI

Ability to create new API users (API key based) for each unit to be authenticated to post/get data

So that’s how ewserver started. I am already pretty far along so this post will be pretty long explaining some of my decisions.

$ design

I spent probably a week looking for good web application designs. There is surprisingly little decent information out there. Almost everything I could find was either tiny sample apps that didn’t do anything. Super complicated things that did everything and had JWT tokens for no apparent reason. Or people hand waving the details about how everything should actually connect, be named or designed.

That all changed when I came across benbjohnson posts. If you are planning on designing/building your own, I highly recommend reading his standard package layout post. I adhered to it pretty closely, except creating the http package since I pretty much just stick with gin. Also, there is very, very little code inside of my http handlers, so I figure if I – or someone else – wanted to replace it, it would be quite easy.

This is probably the biggest handler I have so far, which handles authentication.

// Authenticate a user to create a session, add the user to the session func Authenticate(authnService ewserver.AuthnService, logService ewserver.LogService, e *gin.Engine) gin.HandlerFunc { type login struct { UserName ewserver.UserName Password string } return func(c *gin.Context) { sessions := c.MustGet("sessions").(session.Manager) attempt := &login{} if err := c.BindJSON(attempt); err != nil { c.JSON(500, gin.H{"error": err}) return } user, err := authnService.Authenticate(attempt.UserName, attempt.Password) if err != nil { logService.Info("auth failure", "user", attempt.UserName, "client", c.ClientIP()) c.JSON(401, gin.H{"error": err}) return } logService.Info("auth success", "user", attempt.UserName, "client", c.ClientIP()) user.LastAddress = c.ClientIP() authnService.Update(user) // Renew session token and add user details to the session sessions.Renew(c.Writer, c.Request) sessions.Add(c.Writer, c.Request, "user", user) c.JSON(200, gin.H{"status": "OK"}) } }

$ layout

Adhering to the standard package layout, the cmd/ewserver is what binds all the packages together. I have a root package under ewserver that contains the domain types and imports nothing else from within my project. This root package only contains the domain types and interfaces for services. The project structure looks like this:

├───api │ └───v1 // http handlers │ └───middleware // middleware for v1 api ├───cmd // commands │ ├───configure // helper to build the initial db for setup │ └───ewserver // actual server │ └───config // config.json / role data etc ├───etc // will contain init scripts / installer etc ├───ewserver // 'root' domain types package ├───internal // internal packages │ ├───authz // authorization interface │ │ └───casbinauth // authorization based off casbin │ ├───logger // logger service (wraps go-kit) │ └───session // session interface │ └───scssession // sessions based off scs ├───mock // mock services package for testing ├───store // store for domain types │ └───boltdb // boltdb implementation of services/domain types ├───vendor // vendor data updated via dep ensure └───web // web data ├───static // static data │ ├───css │ ├───img │ └───js └───templates // templates (will probably remove and just use vue.js) └───user

There are four primary services which manages the majority of the application logic:

UserService

APIUserService

RoleService

LogService

Each service has been defined as an interface. This allows me to implement the service differently depending on what type of data store I wish to use. Here’s an example of the UserService interface:

// UserService manages users and users only type UserService interface { // Init the user service (prepare the tables/bucket whatever) Init() error // Authenticate the user with provided password Authenticate(userName UserName, password string) (*User, error) // Create the user with the supplied password Create(u *User, password string) error // Update the user details Update(u *User) error // Reset the user's password (should only be admin level) ResetPassword(userName UserName, new string) error // ChangePassword for allowing users to change their password ChangePassword(userName UserName, current string, new string) error // Delete the user (admin only) Delete(userName UserName) error // User returns the entire user User(userName UserName) (*User, error) // Users returns all users Users() ([]*User, error) }

Since everything that touches data is an interface, I am free to choose or replace the datastore. I just need to implement the service, backed by the storage system I want. Currently, I only implement boltdb because it’s perfect for my use case. I needed a simple KV store that I can run on the same host as the web server.

$ globals=off

If you’ve ever worked on a large project, you know the pain of a system developed with globals everywhere. It’s super hard to debug, makes testing difficult and generally just causes more pain than it’s worth. As such, there are no globals in the system I built. Even the logger has been simplified to an interface with just Info and Error methods.

So without globals, you then of course run into the problem of, how do you pass what’s necessary to the required functions or methods? For the most part, I only pass what is necessary. For http handlers, I pass a ‘services’ container which has references to the major services to the route mapping function, then extract the necessary services from that. This way I only have to pass a single argument to the top level function, then pass the required services thereafter. Here is an example of the admin routes:

// RegisterAdminRoutes for managing the system func RegisterAdminRoutes(services *ewserver.Services, e *gin.Engine) { // setup admin routes apiRoutes := e.Group("v1") userRoutes := apiRoutes.Group("/admin/users") userRoutes.GET("/details/:user", AdminUserDetails(services.UserService, services.LogService, e)) userRoutes.GET("/list", AdminUsersDetails(services.UserService, services.LogService, e)) userRoutes.PUT("/create", AdminCreateUser(services.UserService, services.LogService, e)) userRoutes.POST("/reset_password", AdminResetPassword(services.UserService, services.LogService, e)) userRoutes.DELETE("/delete/:user", AdminDeleteUser(services.UserService, services.LogService, e)) apiAdminRoutes := apiRoutes.Group("/admin/api_users") apiAdminRoutes.GET("/details/:id", AdminAPIUserDetails(services.APIUserService, services.LogService, e)) apiAdminRoutes.GET("/list", AdminAPIUsersDetails(services.APIUserService, services.LogService, e)) apiAdminRoutes.PUT("/create", AdminCreateAPIUser(services.APIUserService, services.LogService, e)) apiAdminRoutes.DELETE("/delete/:id", AdminDeleteAPIUser(services.APIUserService, services.LogService, e)) roleRoutes := apiRoutes.Group("/admin/roles") roleRoutes.GET("/list", AdminRoleList(services.RoleService, services.LogService, e)) roleRoutes.PUT("/permissions", AdminAddPermission(services.RoleService, services.LogService, e)) roleRoutes.DELETE("/permissions", AdminDeletePermission(services.RoleService, services.LogService, e)) roleRoutes.POST("/group", AdminAddUserToGroup(services.RoleService, services.LogService, e)) roleRoutes.DELETE("/group", AdminDeleteUserFromGroup(services.RoleService, services.LogService, e)) }

$ log=on

In actuality, logging doesn’t really need to be done anywhere except some key functions/methods; authorization and http handlers. Because of this, I only pass the logger to services that need it. Everything else just returns errors down to the point where logging is necessary. The log service is super basic, but uses go-kit so I can have structured logging. The interface is very straightforward:

// ewserver/logger.go - interface description // LogService handles logging either info or errors type LogService interface { Info(fields ...interface{}) Error(fields ...interface{}) } // internal/logger/log.go - Log implementation of LogService // Log for logging in all components type Log struct { logger kitlog.Logger } // New JSON logger based off kitlog writing to stdout func New(out *os.File) *Log { l := &Log{} l.logger = kitlog.NewJSONLogger(kitlog.NewSyncWriter(out)) l.logger = kitlog.With(l.logger, "ts", kitlog.DefaultTimestampUTC, "caller", kitlog.Caller(4)) stdlog.SetOutput(kitlog.NewStdlibAdapter(l.logger)) return l } // Logger implementation of those methods: // Info level logs func (l *Log) Info(fields ...interface{}) { kvs := append([]interface{}{"info"}, fields...) l.logger.Log(kvs...) } // Error level logs func (l *Log) Error(fields ...interface{}) { kvs := append([]interface{}{"error"}, fields...) l.logger.Log(kvs...) }

Granted, the above is not the most performant due to creating a new slice just for appending info/error, it makes it easier to standardize when logging. The logs themselves look like:

l.Info("test info", "addr", 80) // {"addr":80,"caller":"log_test.go:22","info":"test info","ts":"2018-04-22T10:58:50"} l.Error("test error", "test", "abc") // {"caller":"log_test.go:23","error":"test error","test":"abc","ts":"2018-04-22T10:58:50"}

$ go test

Testing has been simplified using a mock package where I mock out each individual service. This idea was taken from benbjohnson’s post. The great thing about this technique is being able to individually implement custom methods depending on your unit test. During testing of the authorization service, I needed to pull data out of the http request as well as from the HTTP session. Using this testing method I can implement a mock Sessions.Load method that just returns the username from a user object. I do not need to implement any mock versions of the other methods I don’t call. Here’s the mock Session object with other methods removed, but you get the idea:

// Sessions mocks our session implementation type Sessions struct { ... LoadFn func(req *http.Request, key string, result interface{}) error LoadInvoked bool } ... // Load a value from the session into the result interface func (s *Sessions) Load(req *http.Request, key string, result interface{}) error { s.LoadInvoked = true return s.LoadFn(req, key, result) }

This way each individual test can implement LoadFn however it needs. This is important because my Authorize method uses the session service to load a user object and validate the user is allowed access to a resource. Here is the actual authorization method:

// Authorize validates the user data from a request is authorized to access a resource func (a *CasbinAuthorizer) Authorize(r *http.Request) bool { // Check API based auth first apiKey := r.Header.Get(ewserver.APIKeyHeader) // if apikey is not empty and they are authorized, return true. if apiKey != "" && a.APIAuthorize(r, apiKey) { return true } user := &ewserver.User{} if err := a.sessions.Load(r, "user", user); err != nil { return false } return a.UserAuthorize(r, string(user.UserName)) }

As you can see I need to have a valid session property (which is also an interface) that loads the user object from the request. When I test the Authorize method, I just implement the single Load method of the mock session service and have it assign an ‘anonymous’ user to the val interface.

sessions := &mock.Sessions{} sessions.LoadFn = func(req *http.Request, key string, val interface{}) error { if user, ok := val.(*ewserver.User); ok { user.UserName = "anonymous" } return nil } user := &ewserver.User{} usapi := &mock.APIUserService{} logger := &mock.Log{} auth := NewAuthorizer(enforcer, usapi, sessions, logger) req := httptest.NewRequest("GET", "http://ewserver/v1/admin/users/all_details", nil) if auth.Authorize(req) { t.Fatalf("error GET /v1/admin/users/all_details should not be authorized

") }

During auth.Authorize, this mock method will be called. This particular test ensures anonymous users should not be able to access the admin API.

$ session | authorize | handler

Both session management and authorization is handled via separate middleware prior to passing any requests to the http handlers. The normal request flow is, an agent accesses some URI on my site, the site will 301 redirect to /login and add a Set-Cookie: session=… in the response. Sessions are currently implemented using scs and backed by boltdb. For some reason I have an aversion to importing any gorilla stuff in my code, sessions included. I just wanted your classic ‘secure random id to be backed to a data store’ and look up user specific data. I was shocked to find almost everyone appears to be using JWT, even when it’s completely unnecessary, hence I settled on scs.

Once again, session management is passed around as an interface in the event I need to replace it at any point. My session middleware needs to take into account the fact that I have API Users which use API keys and not cookies. It becomes a two step check, looking to see if an API key header is there or not, then checking for cookies. Here it is in its entirety:

func EnsureSession(sessions session.Manager) gin.HandlerFunc { return func(c *gin.Context) { // Check if request has API header first apiKey := c.GetHeader(ewserver.APIKeyHeader) if apiKey != "" { c.Next() return } user := &ewserver.User{} if err := sessions.Load(c.Request, "user", user); err != nil || user.UserName == "" { user.UserName = "anonymous" sessions.Add(c.Writer, c.Request, "user", user) } // Add the session to the context c.Set("sessions", sessions) c.Next() } }

If we have an API key header, we simply pass it off to the authorize middleware which has its own check for API keys. Otherwise we attempt to load the User from the session manager. If we don’t have a user, we create an anonymous user and add it to our sessions. Finally, since some http handlers may need to extract the user from the session, we add the session interface to the gin.Context and continue to the authorization handler.

Authorization middleware is even smaller:

// Require authorization token (api or session) func Require(authorizer authz.Authorizer) gin.HandlerFunc { return func(c *gin.Context) { if authorizer.Authorize(c.Request) { c.Next() return } c.Redirect(301, v1.LoginPath) c.AbortWithStatus(301) } }

The authorizer service (again, an interface, are you starting to see a pattern here?) extracts all necessary details out of the http.Request to do the validation. Currently, authorization is done using casbin. I’ll probably do another blog post on casbin since it’s a rather complicated subject. Also, I have not 100% vetted it for security, so I may have to change it out, but for now it’s what I’m prototyping with. Basically, I have a RBAC based model and policy. The model is loaded on start but the policy is dynamically created since I want to allow role/user management. Since I’m still in ‘development’ mode, I just load the following policy from the cmd/ewserver:

// allow admin access to everything enforcer.AddPolicy("admin", "/", ".*") // apiuser can access /v1/api/ only enforcer.AddPolicy("apiuser", "/v1/api/:", ".*") // only allow anonymous to access the top folder enforcer.AddPolicy("anonymous", "/:", "(GET|POST)") // add root to the admin role enforcer.AddGroupingPolicy("root", "admin")

This creates three users, admin which can access everything under /, and use any HTTP method. The apiuser can access /v1/api/: (where : means no / after this directory), using any HTTP method. The anonymous user may only access files in the root of the server (so /login, but not /v1/) using only GET or POST methods. I then have my root user added as an admin. That’s it for now, but I do implement handlers for creating more complex groups/roles/permissions.

To actually authorize a request, the Authorizer pulls the User.UserName out of the session and calls the following method:

// UserAuthorize for regular users func (a *CasbinAuthorizer) UserAuthorize(r *http.Request, username string) bool { subject := username object := r.URL.Path action := r.Method a.logger.Info("user authorization attempt", "subject", subject, "object", object, "action", action, "ipaddr", r.RemoteAddr) return a.enforcer.Enforce(subject, object, action) }

API user authorization works similarly, except we extract the key from the header and look it up from the APIUserService:

// APIAuthorize for API Users func (a *CasbinAuthorizer) APIAuthorize(r *http.Request, apiKey string) bool { user, err := a.apiUserService.APIUser(ewserver.APIKey(apiKey)) if err != nil || user.Name == "" { return false } subject := user.Name object := r.URL.Path action := r.Method a.logger.Info("apiuser authorization attempt", "subject", subject, "object", object, "action", action, "ipaddr", r.RemoteAddr) return a.enforcer.Enforce(subject, object, action) }

If the user does not have a session they are redirected to the login page, which, after entering their credentials will post the username/password to the Authenticate http handler (which was shown above in the first code callout). The only interesting bit from the AuthnService is that it is actually just the UserService. I just created an AuthnService interface that is a subset of the UserService interface. The reason is, if I ever wanted to use a different authentication service it’s separated from the UserService. Note the method names are the same:

// UserService manages users and users only type UserService interface { ... // Authenticate the user with provided password Authenticate(userName UserName, password string) (*User, error) // Update the user details Update(u *User) error // ChangePassword for allowing users to change their password ChangePassword(userName UserName, current string, new string) error // User returns the entire user User(userName UserName) (*User, error) } // AuthnService allows a user to authenticate or change their password type AuthnService interface { // Authenticate the user with provided password Authenticate(userName UserName, password string) (*User, error) // Update the user details (such as last login address) Update(u *User) error // ChangePassword for allowing users to change their password ChangePassword(userName UserName, current string, new string) error // User returns the entire user User(userName UserName) (*User, error) }

The actual implementation of both UserService and AuthnService is just my bolt store based UserService. Here’s the authentication method if you are curious:

// Authenticate a user to grant access, returns the User on success, error otherwise. func (u *UserService) Authenticate(userName ewserver.UserName, pass string) (*ewserver.User, error) { validUser, err := u.User(userName) if err != nil { return nil, err } if err := bcrypt.CompareHashAndPassword(validUser.Password, []byte(pass)); err != nil { return nil, ewserver.ErrInvalidPassword } return validUser, nil }

$ if err

I treat my error messages as a Domain Type, as such, it exists in the root ewserver package under error.go. They are defined for common errors such as:

// Error represents any constant error string we can return type Error string func (e Error) Error() string { return string(e) } // common errors const ( ErrUserNotFound = Error("user not found") ErrInvalidUser = Error("invalid username or fields") ... )

$ EOF

I do plan on releasing this once I’ve finished, I may even open the repo so others can take a look, but since I have not vetted casbin yet, I’m weary of releasing something built as a template that is not relatively secure. I’ll continue to post here on other design decisions if they are of interest.