• Software Letters
  • Posts
  • Building a Robust Authentication Microservice with Golang and OAuth 2.0

Building a Robust Authentication Microservice with Golang and OAuth 2.0

A Step-by-Step Guide to Secure User Authentication in Go Using OAuth 2.0

Introduction

In the modern era of digital connectivity, securing user authentication is paramount for any application. With the increasing need for robust security measures, OAuth 2.0 has emerged as a standard protocol for authorization, providing a secure and reliable method for users to grant websites or applications access to their information without exposing passwords.

In this tutorial, we will walk you through the process of building a robust authentication micro-service using Golang and OAuth 2.0. We'll cover everything from setting up your project and configuring OAuth 2.0, to implementing secure API endpoints and integrating a database for user management. Whether you are a seasoned developer or just getting started with Go, this guide will equip you with the knowledge and tools needed to create a secure authentication system for your applications.

Join us as we delve into the intricacies of OAuth 2.0 and demonstrate how to harness its power to protect your users' data, ensuring a seamless and secure authentication process. Let's get started!

Outline

  1. Project Setup

    • Initialize Go module

    • Set up folder structure

  2. Dependencies

    • Install necessary packages

  3. Configuration

    • Create configuration files for OAuth 2.0 settings

  4. OAuth 2.0 Implementation

    • Define OAuth 2.0 structs and interfaces

    • Implement OAuth 2.0 authorization and token endpoints

  5. Database Integration

    • Set up a database for storing user information and tokens

    • Implement functions for user management

  6. API Endpoints

    • Create endpoints for user registration and login

    • Implement middleware for token validation

  7. Server Setup

    • Configure and start the HTTP server

  8. Testing

    • Write tests for the micro-service

Implementation

Let's walk through the implementation step by step.

1. Project Setup

Create a new directory for your project and initialize a Go module:

mkdir auth-microservice 
cd auth-microservice 
go mod init auth-microservice

2. Dependencies

In this section, we will install and explain the necessary packages for our authentication micro-service. Each package serves a specific purpose, contributing to the overall functionality of the service.

1. github.com/gin-gonic/gin

Purpose: gin is a web framework written in Go. It features a Martini-like API with much better performance, thanks to httprouter. Gin is designed to be easy to use and efficient.

Usage: We use gin to define and manage our HTTP routes, handling incoming API requests for user registration, login, and token management.

2. github.com/go-oauth2/oauth2/v4

Purpose: The oauth2 package provides a framework for implementing OAuth 2.0 authorization and token handling in Go. It simplifies the process of creating OAuth 2.0 compliant services.

Usage: This package is used to set up the core OAuth 2.0 functionality, including handling authorization requests and issuing access tokens.

3. github.com/go-oauth2/oauth2/v4/manage

Purpose: Part of the oauth2 package, manage provides tools for managing the lifecycle of OAuth 2.0 tokens, clients, and authorization codes.

Usage: We use manage to set up token storage and client management, ensuring that our tokens are securely generated, stored, and validated.

4. github.com/go-oauth2/oauth2/v4/store

Purpose: The store package offers various implementations for storing OAuth 2.0 data, such as tokens and client information. It includes in-memory and persistent storage options.

Usage: We use store to implement an in-memory token store and a client store, which manage our OAuth 2.0 tokens and client credentials.

5. github.com/go-oauth2/oauth2/v4/models

Purpose: The models package defines the data models used in the OAuth 2.0 framework, such as clients and tokens.

Usage: We use models to define our OAuth 2.0 client model, which includes client ID, secret, and redirect URL.

6. github.com/go-sql-driver/mysql

Purpose: This package is a MySQL driver for Go's database/sql package. It allows Go applications to interact with MySQL databases.

Usage: We use this driver to connect to our MySQL database, where we store user information and other persistent data required for our authentication service.

Installing Dependencies

To install these dependencies, run the following commands in your project directory:

go get -u github.com/gin-gonic/gin 
go get -u github.com/go-oauth2/oauth2/v4 
go get -u github.com/go-oauth2/oauth2/v4/manage 
go get -u github.com/go-oauth2/oauth2/v4/store 
go get -u github.com/go-oauth2/oauth2/v4/models 
go get -u github.com/go-sql-driver/mysql

By installing and configuring these dependencies, you set the foundation for building a secure and efficient authentication microservice using Golang and OAuth 2.0.

Implementation

Let's walk through the implementation step by step.

1. Project Setup

Create a new directory for your project and initialize a Go module:

mkdir auth-microservice 
cd auth-microservice 
go mod init auth-microservice

2. Configuration

Create a config package for managing configurations:

mkdir config

Inside config/config.go:

package config

import (
    "os"
)

type Config struct {
    ClientID     string
    ClientSecret string
    RedirectURL  string
    AuthURL      string
    TokenURL     string
    Scopes       []string
}

func LoadConfig() *Config {
    return &Config{
        ClientID:     os.Getenv("CLIENT_ID"),
        ClientSecret: os.Getenv("CLIENT_SECRET"),
        RedirectURL:  os.Getenv("REDIRECT_URL"),
        AuthURL:      os.Getenv("AUTH_URL"),
        TokenURL:     os.Getenv("TOKEN_URL"),
        Scopes:       []string{"read", "write"},
    }
}

3. OAuth 2.0 Implementation

Create an oauth package for handling OAuth 2.0 logic:

mkdir oauth

Inside oauth/oauth.go:

package oauth

import (
    "auth-microservice/config"
    "github.com/go-oauth2/oauth2/v4/manage"
    "github.com/go-oauth2/oauth2/v4/server"
    "github.com/go-oauth2/oauth2/v4/store"
    "github.com/go-oauth2/oauth2/v4/models"
)

func SetupOAuthServer(cfg *config.Config) *server.Server {
    manager := manage.NewDefaultManager()

    // Token store
   manager.MustTokenStorage(store.NewMemoryTokenStore())

    // Client store
    clientStore := store.NewClientStore()
    clientStore.Set(cfg.ClientID, &models.Client{
        ID:     cfg.ClientID,
        Secret: cfg.ClientSecret,
        Domain: cfg.RedirectURL,
    })
    manager.MapClientStorage(clientStore)

    srv := server.NewDefaultServer(manager)
    srv.SetAllowGetAccessRequest(true)
    srv.SetClientInfoHandler(server.ClientFormHandler)

    return srv
}

4. Database Integration

Set up a database connection and implement user management functions. Here, we'll use MySQL:

Inside main.go:

package main

import (
    "auth-microservice/config"
    "auth-microservice/handlers"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

var db *sql.DB

func main() {
    dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
    var err error
    db, err = sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Test the connection
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    // Create users table if it doesn't exist
    createTable := `
        CREATE TABLE IF NOT EXISTS users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            username VARCHAR(50) UNIQUE NOT NULL,
            password VARCHAR(255) NOT NULL
        );`
    _, err = db.Exec(createTable)
    if err != nil {
        log.Fatal(err)
    }

    cfg := config.LoadConfig()
    r := handlers.SetupRoutes(cfg, db)

    log.Println("Starting server on :8080")
    r.Run(":8080")
}

5. User Management

Create a models package for user-related functions:

mkdir models

Inside models/user.go:

package models

import (
    "database/sql"
    "golang.org/x/crypto/bcrypt"
)

type User struct {
    ID       int
    Username string
    Password string
}

func CreateUser(db *sql.DB, username, password string) error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }

    _, err = db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, hashedPassword)
    return err
}

func AuthenticateUser(db *sql.DB, username, password string) (bool, error) {
    var hashedPassword string
    err := db.QueryRow("SELECT password FROM users WHERE username = ?", username).Scan(&hashedPassword)
    if err != nil {
        if err == sql.ErrNoRows {
            return false, nil
        }
        return false, err
    }

    err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
    if err != nil {
        return false, nil
    }

    return true, nil
}

6. API Endpoints

Create a handlers package for handling API requests:

mkdir handlers

Inside handlers/handlers.go:

package handlers

import (
    "auth-microservice/config"
    "auth-microservice/models"
    "auth-microservice/oauth"
    "database/sql"
    "github.com/gin-gonic/gin"
    "net/http"
)

func SetupRoutes(cfg *config.Config, db *sql.DB) *gin.Engine {
    router := gin.Default()
    oauthServer := oauth.SetupOAuthServer(cfg)

    router.POST("/signup", func(c *gin.Context) {
        var json struct {
            Username string `json:"username" binding:"required"`
            Password string `json:"password" binding:"required"`
        }

        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        err := models.CreateUser(db, json.Username, json.Password)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
    })

    router.POST("/signin", func(c *gin.Context) {
        var json struct {
            Username string `json:"username" binding:"required"`
            Password string `json:"password" binding:"required"`
        }

        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        authenticated, err := models.AuthenticateUser(db, json.Username, json.Password)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to authenticate user"})
            return
        }

        if !authenticated {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
            return
        }

        // Issue token upon successful authentication
        c.JSON(http.StatusOK, gin.H{"message": "User authenticated successfully"})
    })

    router.POST("/token", func(c *gin.Context) {
        oauthServer.HandleTokenRequest(c.Writer, c.Request)
    })

    router.GET("/authorize", func(c *gin.Context) {
        oauthServer.HandleAuthorizeRequest(c.Writer, c.Request)
    })

    return router
}

6. Server Setup

In main.go, set up and start the HTTP server:

package main

import (
    "auth-microservice/config"
    "auth-microservice/handlers"
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "log"
)

var db *sql.DB

func main() {
    dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
    var err error
    db, err = sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Test the connection
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    // Create users table if it doesn't exist
    createTable := `
        CREATE TABLE IF NOT EXISTS users (
            id INT AUTO_INCREMENT PRIMARY KEY,
            username VARCHAR(50) UNIQUE NOT NULL,
            password VARCHAR(255) NOT NULL
        );`
    _, err = db.Exec(createTable)
    if err != nil {
        log.Fatal(err)
    }

    cfg := config.LoadConfig()
    r := handlers.SetupRoutes(cfg, db)

    log.Println("Starting server on :8080")
    r.Run(":8080")
}

7. Testing

Write tests for the microservice. Create a tests folder and add test files for different components. For example, tests/oauth_test.go:

package tests

import (
    "testing"
    "net/http"
    "net/http/httptest"
    "auth-microservice/config"
    "auth-microservice/handlers"
)

func TestTokenEndpoint(t *testing.T) {
    cfg := config.LoadConfig()
    r := handlers.SetupRoutes(cfg)

    req, _ := http.NewRequest("POST", "/token", nil)
    rr := httptest.NewRecorder()
    r.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    // Add more checks for the response here
}

The relationship between the signin/signup functions and the OAuth 2.0 methods can be understood as follows:

Signup

  • Purpose: Allows new users to register by creating an account in your system.

  • Flow: When a user signs up, their information (e.g., username and password) is saved to your database. This process does not directly involve OAuth 2.0.

Signin

  • Purpose: Allows registered users to log in by verifying their credentials.

  • Flow: When a user signs in, their credentials are verified against the stored data in your database. Upon successful authentication, an OAuth 2.0 token can be issued to grant the user access to protected resources.

OAuth 2.0 Methods

  • Purpose: Provides a secure and standardized way to handle authorization and access tokens.

  • Flow:

    • Authorization Endpoint: Users authenticate and authorize your application to act on their behalf. This typically involves redirecting the user to a login page where they enter their credentials.

    • Token Endpoint: Once the user is authenticated and authorized, an access token is issued. This token is used by the client application to access protected resources.

Integration

  1. Signup:

    • Users create an account.

    • The account information is stored in the database.

    • No OAuth 2.0 interaction is required at this stage.

  2. Signin:

    • Users provide their credentials.

    • The system authenticates the user against the database.

    • Upon successful authentication, the user is typically redirected to the OAuth 2.0 authorization endpoint to initiate the token generation process.

  3. OAuth 2.0 Authorization and Token Issuance:

    • After a successful signin, the user is redirected to the OAuth 2.0 authorization endpoint to grant permission to the application.

    • Once the user grants permission, the application exchanges this authorization for an access token via the token endpoint.

    • This access token is then used to access protected resources on behalf of the user.

Code Integration

Here is how the signin and OAuth 2.0 methods integrate in the context of the micro-service:

Signup and Signin endpoints handle user creation and authentication, respectively. Upon successful signin, the application can proceed to use the OAuth 2.0 endpoints to issue tokens.

Conclusion

This implementation guide provides a basic structure for building an authentication microservice using Golang and OAuth 2.0. It includes setting up the project, integrating OAuth 2.0, managing user information, and creating endpoints for authentication. For a complete solution, you will need to expand on error handling, security measures, and comprehensive testing.