Understanding and Building Authentication Sessions with Golang

The Authentication Session of a web app is the heart of its defense against malicious threats. Hence, it is among the first points of recon for a security tester.

This article will discuss the authentication sessions of a web app in the “Go” programming language (Golang). It will also discuss the vulnerabilities and design flaws in authentication sessions, the difference between Session-Based and Token-Based Authentication methods, and when to apply each.

I will also share the example use cases for both methods with actual codes for building authentication sessions in Golang.

Authentication Sessions

A web application’s Authentication protocol is in its sessions. The procedure follows like this:

  • A client sends an authentication request to the login session of the web app
  • The web app receives it and sends it to the webserver
  • The web server matches it with existing credentials
  • The web server returns a cookie if a match is found, and then the relevant REST API is called to the needed page.

When web developers build authentication mechanisms, they often rely on either of these methods:

  • Using HTML forms
  • Using Multifactor mechanisms
  • Client SSL-Certificates
  • HTTP authentication
  • Authentication Services
  • JSON Web Tokens

Let’s have a look at each of these methods and what they imply.

Using HTML Forms

HTML forms are the most common methods of authenticating web applications, where username and password are collected and submitted to the application. This mechanism accounts for well over 90% of applications available on the internet.

Using Multifactor Mechanisms

A multistage form-filling session is initiated in platforms requiring more security, like online payment systems, and users must provide additional credentials. In banking apps, we often require physical tokens. These tokens typically produce a stream of one-time passcodes (OTPs) or provide challenges requiring user input.

The rule of thumb is to use OTPs for highly sensitive data.

Client SSL-Certificates

Some web apps make use of SSL Certificates or cryptographic mechanisms.

HTTP Authentication

HTTP-based authentication is extremely rare in internet adoption. It finds its use more on an intranet. It is the most basic form of authentication. In HTTP authentication, client login credentials are sent in the request headers with each request like:

Authorization: Basic YWxqY2U6cGE2NXdbcmQ=

Basic authentication doesn’t use encryption on username and password. As such, its application is limited to cases with low-value data and the need for easy access.

Even at that, it is advisable to use it:

  • On HTTPS connections
  • With really strong passwords
  • With rate limiting added to prevent brute-forcing attacks

Authentication Services

Authentication services are gaining weight in terms of application and credibility. A popular and widely used authentication & authorization service is Auth0.

JSON Web Tokens

JWT, or JSON Web Token, is an open standard authentication protocol to share security information between two parties – a client and a server. The client credentials are not stored on the server but transferred back to the client for safekeeping and reuse.

Vulnerabilities in Authentication Sessions

The vulnerabilities and attacks that are possible in HTML forms will most likely work in other authentication methods, albeit with a bit of upgrade.

Let’s look at design flaws that a hacker can exploit in an attack.

Design Flaws in Web Applications

1. Weak Passwords

Weak passwords remain a significant design flaw in many web applications, posing serious security risks. Despite advancements in security practices, the prevalence of easily guessable passwords—such as short phrases, predictable words, or personal information easily found on social media—remains high. In the context of Golang-based web applications, addressing this flaw involves:

  • Enforcing Strong Password Policies: Implementing stringent password requirements, like minimum length and complexity, to prevent common weak passwords.
  • User Education: Highlighting the importance of strong passwords to users and providing guidelines for creating secure passwords.
  • Alternative Authentication Methods: Incorporating methods like two-factor authentication (2FA) and biometrics can enhance security beyond traditional passwords.
  • Utilizing Golang’s Features: Leveraging Golang’s robust standard library and security features to build more secure authentication systems.

By focusing on these areas, web applications can significantly reduce the risks associated with weak passwords and enhance overall security.

Exploitation

First, hackers attempt to log into the web application and take note of the rules specified by the software in filling password boxes.

2. Password Change Functionality

Web developers often fail to provide password change Functionality in their apps. This feature is vital in web apps, though, for two particular reasons:

  • For the user to quickly change their password when they detect malicious activity
  • For periodic testing and validation of a password change session

The flaws in web apps that don’t use a Password Change Functionality include:

  • Verbose error messages
  • Allowing unrestricted guessing in the existing password field
  • Matching “new password” and “confirm new password” fields only after validating the existing password

Other design flaws in authentication sessions are present in the Forgotten Password Functionality, Remember Me Functionality, and Invalid Credentials Functionality

Let’s learn how to build authentication sessions in Go.

Basic HTTP Authentication in Golang

As explained earlier, the basic HTTP authentication method is not the safest. However, we can implement it with hashing in the following way:

func basicAuth(next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    username, password, ok := r.BasicAuth()
        if ok {
        usernameHash := sha256.Sum256([]byte(username))
            passwordHash := sha256.Sum256([]byte(password))
              expectedUsernameHash := sha256.Sum256([]byte("username"))
              expectedPasswordHash := sha256.Sum256([]byte("password"))
        usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
              passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)

        if usernameMatch && passwordMatch {
                next.ServeHTTP(w, r)
                return
            }
        }
    w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
    })
}

It is important to emphasize that the purpose of hashing the username and password is not for storage but rather to generate two equal-length byte slices. These slices are then compared in “constant time” to enhance security and prevent timing attacks.

What is “Constant Time”?

“Constant time” in the context of comparing hashed values, such as usernames and passwords, refers to an important concept in computer security. It means that the time it takes to compare two values does not depend on the values themselves. This property is crucial for security, especially in authentication systems. Here’s why:

  1. Preventing Timing Attacks: If a comparison takes different amounts of time based on how much of the input matches, an attacker could potentially use the time it takes to compare values to infer information about the stored data. For example, if a system is checking a password, and it takes longer to respond when the first few characters are correct, an attacker can use this information to guess the password one character at a time.
  2. Uniform Response Time: In constant-time comparison, the operation takes the same amount of time regardless of the input data. This means that whether the comparison is correct or incorrect or how much of the input data matches, it won’t affect the time it takes to complete the comparison. This uniformity helps in masking which part of the data, if any, was correct, thereby thwarting timing attacks.

In summary, performing constant-time comparisons of hashed values is a security best practice to prevent attackers from gaining insights into sensitive data based on the time it takes to perform these comparisons.

Use of the HTTP Basic Authentication method, the imported packages include:

import (
    "crypto/sha256"
    "crypto/subtle"
    “fmt”
    “log”
    "net/http"
    “os”
    “time”
)

An application instance of struct type should be created to contain the username and password:

type application struct {
    username string
    password string
}

The main function will contain the entire operations and the server instance:

func main() {
    webapp := new(application)

    webapp.auth.username = os.Getenv("AUTH_USERNAME")
    webapp.auth.password = os.Getenv("AUTH_PASSWORD")

    if webapp.auth.username == "" {
        log.Fatal(“Illegal username provided”)
    }

    if webapp.auth.password == "" {
        log.Fatal(“Illegal password provided”)
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/unprotected", webapp.unprotectedHandler)
    mux.HandleFunc("/protected", webapp.basicAuth(app.protectedHandler))

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        IdleTimeout:  time.Minute,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
    }

    log.Printf("starting server on %s", srv.Addr)
    err := srv.ListenAndServeTLS("./localhost.pem", "./localhost-key.pem")
    log.Fatal(err)
}

Session-Based Authentication Session in Golang

Session-based authentication systems are often used in web apps that implement server-side templating. OAuth and OpenID could be added to secure the application further.

In Golang, the popular Gorilla Mux package has a gorilla/sessions package that can be used to create authentication sessions.

Hence, the first step in creating an authentication session is to install the gorilla/mux and gorilla/sessions packages. The code for creating an authentication session is as follows:

go get github.com/gorilla/mux
go get github.com/gorilla/sessions

Creation of Local Directory

After this, create a local directory for the project.

Next, import the gorilla/mux package and other important packages. Please check the code below:

package main
import (
    “log”
    "net/http"
    “os”
    “time”
    “github.com/gorilla/mux”
    “github.com/gorilla/sessions”
)

Since we are creating session-based authentication for API endpoints, we will create a cookie store using the “sessions.NewCookieStore” method in the imported sessions package. Please check the code below:

var store =sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

Assuming we are logging into a dashboard, then the necessary API endpoints are:

  • /login
  • /dashboard
  • /logout

There will be a list of users with their own dashboards. So, the server must have a map in which it can search for yours. Initializing an example user credentials:

var users = map[string]string{
    "Mac":   "username",
    "admin": "password"
}

Retrieving Clinet’s Credentials

A login handler function would be responsible for the client’s request, credential matching, and the /dashboard endpoint return.

The function will initially parse the POST form. Then, it will retrieve the client’s credentials as follows:

func LoginHandler(w http.ResponseWriter, r *http.Request) {
   ...
   err := r.ParseForm()
      if err != nil {
      http.Error(w, "Please pass the data as URL form encoded", http.StatusBadRequest)
     return
   }
   username := r.PostForm.Get("username")
   password := r.PostForm.Get("password")}

The function will also match the collected data with the ones stored on the server. Finally, the function will authenticate an existing match and return an error when there’s no match.

//continued in the LoginHandler function
if originalPassword, ok := users[username]; ok {
    session, _ := store.Get(r, "session.id")
    if password == originalPassword {
        session.Values["authenticated"] = true
        session.Save(r, w)
    } else {
        http.Error(w, "Invalid Credentials",http.StatusUnauthorized)
        return
    }
} else {
    http.Error(w, "User is not found", http.StatusNotFound)
    return
}
w.Write([]byte("Logged In successfully"))

Login and Logout Sessions

The login either succeeds or not, depending on the availability of the client on the server’s credential store.

The log-out session, on the other hand, receives a GET request and turns the “session.Values” method to false:

func LogoutHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    session.Values["authenticated"] = false
    session.Save(r, w)
    w.Write([]byte(""))
}

The “session.save” method saves the cookie state after modification.

Endpoints

Next, it is important to create the /dashboard API endpoint. The /dashboard function would return the time of login:

func DashboardHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session.id")
    if (session.Values["authenticated"] != nil) && session.Values    ["authenticated"] != false { 
       w.Write([]byte(time.Now().String()))
    } else {
        http.Error(w, "Forbidden", http.StatusForbidden)
    }
}

After we finish writing the endpoints, the next step is to write the main function. This function will connect and start up all endpoints and the server at an open port:

main() {
    server := mux.NewRouter().StrictSlash(True)
    server.HandleFunc("/login", LoginHandler)
    server.HandleFunc("/dashboard", DashboardHandler)
    server.HandleFunc("/logout", LogoutHandler)
    http.Handle("/", server)
    srv := &http.Server{
        Handler: server,
        Addr:    "127.0.0.1:8080",
        // Good practice: enforce timeouts for servers you create!
        WriteTimeout: 15 * time.Second,
        ReadTimeout:  15 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

This completes the secure authentication session. Running the command go run main.go starts the server at localhost/8080

Token-Based Authentication Session in Golang

One of the downsides of this token- or session-based authentication system is that it stores credentials to the program memory or on a special server software like Redis.

However, JWT poses a solution to this. JWT resends the login credentials to the client to store in a database. The entire procedure is explained below:

  • The client sends login credentials as a POST request to the login API endpoint.
  • The server authenticates the details, and if successful, it generates a JWT.
  • The server returns this instead of creating a cookie. It becomes the client’s responsibility to store this token.
  • Since the client is in charge of the token, it must add this in the headers section to make subsequent REST API calls.
  • The server checks the JWT from the header, and if it is successfully decoded, the server authenticates the client.

For RESTful APIs, token-based authentication is the best and recommended approach, given that it is stateless.

In creating a JWT Token-Based session with go, the “jwt-go” package will be imported. It has a NewWithClaims method that accepts a signing method and a claims map.

A great guide on implementing JWT with Go is available on Auth0 blog.

Conclusion

In this article, we discussed what authentication sessions are under the covers. We also discussed the kinds of vulnerabilities possible in authentications, popular options in authenticating web apps, and the differences and ways to write session- and token-based authentication sessions.

MacBobby Chibuzor is a Robotics Hardware Engineer and a Tech Polyglot. He also has practical experience in Software Engineering and Machine Learning, with an interest in embedded systems and Blockchain technology. In addition, Mac loves to spend his free time in technical writing.

Need help?

Let us know about your question or problem and we will reach out to you.