We all know security is hard. Let’s walk through some basic security principles you can use to get your Golang web application up and running securely. If you just want to see the code check out the application on Github: Golang Secure Example Application (gosea).

Recently, I gave a lightning talk on using Golang middleware to implement some basic security controls at the Boston Golang Meetup. This post will include some of those concepts and expand upon topics not covered in the talk. Another great starting point without rolling some of this stuff yourself is to use the unrolled/secure package, which has a ton of great features.

Disclaimer: These are some good practices to follow and there are plenty of other ways your application can be insecure. This post does not make any guarantees for complete security for your application.

Serve over HTTPS

The first thing I think about when starting a dynamic web application is serving it over HTTPS. There are arguments that not everything needs to be served over HTTPS, but here we are securing a web application that has login or is backed by some information that may be sensitive.

Before we write any code we want to generate our keys that we will use and place them in our root directory. In production you probably want to put the cert in something like /root/certs and keys in something like /etc/ssl/private.

First generate the private key:

openssl genrsa -out server.key 2048 openssl ecparam -genkey -name secp384r1 -out server.key

Then generate the x509 self-signed public key:

openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

Now we can create our application in a main.go:

package main import ( "log" "net/http" "github.com/komand/gosea/services" ) func main() { certPath := "server.pem" keyPath := "server.key" api := NewAPI(certPath, keyPath) http.Handle("/hello", api.Hello) err := http.ListenAndServeTLS(":3000", certPath, keyPath, nil) if err != nil { log.Fatal("ListenAndServe: ", err) } }

Note: The API structure is left out of this discussion, but can be found in the gosea repo.

Then we create two directories called handlers and services that represent the various layers of our applications. We separate them for testability at the different layers.

Let’s create a hello.go in each:

handlers/hello.go

package handlers import ( "net/http" "github.com/komand/gosea/services" ) // Hello exposes an api for the hello service type Hello struct { Service services.HelloService } // NewHello creates a new handler for hello func NewHello(s services.HelloService) *Hello { return &Hello{s} } // Handler handles hello requests func (h *Hello) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": s := h.Service.SayHello() w.Write([]byte(s)) default: http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } }

services/hello.go

package services import "net/http" // HelloService provides a SayHello method type HelloService interface { SayHello() string } // NewHelloService creates a new hello world service func NewHelloService() HelloService { return &helloService{} } type helloService struct {} // SayHello says hello for the hello service func (h *helloService) SayHello() string { return "hello, world!" }

Before we can boot up the application, we first must install it with go install. Then run gosea. We can now access our application at https://localhost:3000/hello. You should get a warning if you are using Chrome, but it’s only because we are using a self-signed certificate. In production you would want to use a certificate from a legitimate CA. If you continue through, it should render “hello, world!” in the browser.

Authenticate Your Users

Once we are serving our application over HTTPS, the next thing we should think about is ensuring only authenticated users are allowed into our application. Authentication defines whether a user is admitted into your system and is the first barrier to someone gaining access to your application. Before we authenticate you must have users (the code for the users handler/service can be found at the gosea repo on github).

In Go, authentication can be implemented relatively simply with JSON Web Tokens (JWT) using an authentication endpoint and middleware. There are great articles on how to do that by Auth0 and Brainattica, but let’s walk through the exercise in a similar vein.

Let’s implement a tokens service. We can edit our main function to add a /tokens route:

http.Handle("/tokens", api.Tokens.Handler)

Now let’s add a token.go file to the handlers package:

package handlers import ( "net/http" "github.com/komand/gosea/services" ) // Tokens exposes an API to the tokens service type Tokens struct { Service services.TokenService } // NewTokens creates new handler for tokens func NewTokens(s services.TokenService) *Tokens { return &Tokens{s} } // ServeHTTP will return tokens func (t *Tokens) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": // TODO: Take in login information user := &services.User{ ID: 1, FirstName: "Admin", LastName: "User", Roles: []string{services.AdministratorRole}, } token, err := t.Service.Get(user) if err != nil { http.Error(w, "Failed to generate token", http.StatusInternalServerError) } w.Write([]byte(token)) default: http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } }

And a token.go file to the services package:

package services import ( "errors" "time" "github.com/dgrijalva/jwt-go" ) // Set our secret. // TODO: Use generated key from README var mySigningKey = []byte("secret") // Token defines a token for our application type Token string // TokenService provides a token type TokenService interface { Get(u *User) (string, error) } type tokenService struct { UserService UserService } // NewTokenService creates a new UserService func NewTokenService() TokenService { return &tokenService{} } // Get retrieves a token for a user func (s *tokenService) Get(u *User) (string, error) { // Create token token := jwt.New(jwt.SigningMethodHS256) // Try to log in the user user, err := s.UserService.Read(u.ID) if err != nil { return "", errors.New("Failed to retrieve user") } if user == nil { return "", errors.New("Failed to retrieve user") } // Set token claims token.Claims["admin"] = true token.Claims["user"] = u token.Claims["exp"] = time.Now().Add(time.Hour * 24).Unix() // Sign token with key tokenString, err := token.SignedString(mySigningKey) if err != nil { return "", errors.New("Failed to sign token") } return tokenString, nil }

Once the token is issued, we can then implement middleware on our API that will wrap our handlers and authenticate users using jwt.

// Authenticate provides Authentication middleware for handlers func (a *API) Authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var token string // Get token from the Authorization header // format: Authorization: Bearer tokens, ok := r.Header["Authorization"] if ok && len(tokens) >= 1 { token = tokens[0] token = strings.TrimPrefix(token, "Bearer ") } // If the token is empty... if token == "" { // If we get here, the required token is missing http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } // Now parse the token parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) return nil, msg } return a.encryptionKey, nil }) if err != nil { http.Error(w, "Error parsing token", http.StatusUnauthorized) return } // Check token is valid if parsedToken != nil && parsedToken.Valid { // Everything worked! Set the user in the context. context.Set(r, "user", parsedToken) next.ServeHTTP(w, r) fmt.Println("test") } // Token is invalid http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return }) }

Authorize Your Users

What happens after users are logged into your system? You probably have various roles for your application Admin, User, etc. Do they all have access to everything in your application? I doubt it. This is where authorization comes in. I find this is best done as middleware, similar to authentication. Details about the AclService that this middleware uses can be found in the repo.

// Authorize provides authorization middleware for our handlers func (a *API) Authorize(permissions ...services.Permission) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // TODO: Get User Information from Request user := &services.User{ ID: 1, FirstName: "Admin", LastName: "User", Roles: []string{services.AdministratorRole}, } for _, permission := range permissions { if err := a.AclService.CheckPermission(user, permission); err != nil { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } } next.ServeHTTP(w, r) }) } }

Now if you hit the /users endpoint, you should get an Unauthorized message.

Use Secure Headers

Let’s create a shell middleware for our secure headers:

// SecureHeaders adds secure headers to the API func (a *API) SecureHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // We will add our headers here next.ServeHTTP(w, r) }) }

Allowed Hosts

Allowed Hosts header provides a list of fully qualified domain names (FQDN) that are allowed to serve your site. This prevents cache poisoning and ensures random domains cannot be pointed at your site.

var err error if len(a.AllowedHosts) > 0 { isGoodHost := false for _, allowedHost := range a.options.AllowedHosts { if strings.EqualFold(allowedHost, r.Host) { isGoodHost = true break } } if !isGoodHost { a.errorHandler.ServeHTTP(w, r) err = fmt.Errorf("Bad host name: %s", r.Host) } } // If there was an error, do not continue request if err != nil { http.Error(w, “Failed to check allowed hosts”, http.StatusInternalServerError) }`

X-XSS-Protection

Set it to “1; mode=blockFilter” enabled. Rather than sanitize the page, when a XSS attack is detected, the browser will prevent rendering of the page.

// Add X-XSS-Protection header w.Header().Add(xssProtectionHeader, xssProtectionValue)

Content-Type

Content type tells the browser what type of content you are sending. If you do not include it, the browser will try to guess the type and may get it wrong.

// Add Content-Type header w.Header().Add("Content-Type", "application/json")

Based on the type of application, you may need to add this header to your handlers (if you are returning other types of data), but since this is just an API that will return JSON, it is OK for now. Additionally, many frameworks like gin handle this for you.

X-Content-Type-Options

Content Sniffing is the inspecting the content of a byte stream to attempt to deduce the file format of the data within it. Browsers will do this to try to guess at the content type you are sending. By setting this header to “nosniff”, it prevents IE and Chrome from content sniffing a response away from its actual content type. This reduces exposure to drive-by download attacks.

// Add X-Content-Type-Options header w.Header().Add("X-Content-Type-Options", "nosniff")

X-Frame-Options

// Prevent page from being displayed in an iframe w.Header().Add("X-Frame-Options", "DENY")

Add the Middleware to Handlers

Finally, we add the middleware to our handlers so that they can use the features we just implemented. Let’s define a function that will add all our middleware for our handlers.

// AddMiddleware adds middleware to a Handler func AddMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler { for _, mw := range middleware { h = mw(h) } return h }

Now for an example, we can add authentication and authorization to users:

http.Handle("/users", AddMiddleware(api.Users, api.Authenticate, api.Authorize(services.Permission("user_modify")), api.SecureHeaders, ))

More Features!

In the next part of this series, I will describe and implement some additional security practices into gosea. These are primarily for use cases in the front end of an application and thus have not been covered with these backend topics, including using secure cookies and preventing cross site request forgery.