- 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
Project Setup
Initialize Go module
Set up folder structure
Dependencies
Install necessary packages
Configuration
Create configuration files for OAuth 2.0 settings
OAuth 2.0 Implementation
Define OAuth 2.0 structs and interfaces
Implement OAuth 2.0 authorization and token endpoints
Database Integration
Set up a database for storing user information and tokens
Implement functions for user management
API Endpoints
Create endpoints for user registration and login
Implement middleware for token validation
Server Setup
Configure and start the HTTP server
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
Signup:
Users create an account.
The account information is stored in the database.
No OAuth 2.0 interaction is required at this stage.
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.
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.