Authentication

For web applications, OAuth or password-based authentication is commonly used.

For APIs, token-based authentication is more prevalent.

Authentication with JWT

In simple terms, JWTs (JSON Web Tokens) are signed JSON documents that adhere to a specific format.

Add jwt/main.go:

package jwt

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"
)

type Claims struct {
	Issuer    string `json:"iss"`
	Subject   string `json:"sub"`
	Audience  string `json:"aud"`
	ExpiresAt int64  `json:"exp"`
	IssuedAt  int64  `json:"iat"`
	JwtId     string `json:"jti"`
}

func WriteJWT(secretKey []byte, claims Claims) (string, error) {
	payload, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}
	message := base64.URLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) +
		"." + base64.URLEncoding.EncodeToString(payload)

	hash := hmac.New(sha256.New, secretKey)
	hash.Write([]byte(message))
	return message + "." + base64.URLEncoding.EncodeToString(hash.Sum(nil)), nil
}

func ReadJWT(secretKey []byte, jwt string) (Claims, error) {
	parts := strings.Split(jwt, ".")
	if len(parts) != 3 {
		return Claims{}, fmt.Errorf("invalid JWT format")
	}

	payloadBytes, err := base64.URLEncoding.DecodeString(parts[1])
	if err != nil {
		return Claims{}, fmt.Errorf("payload decode error: %w", err)
	}

	signatureBytes, err := base64.URLEncoding.DecodeString(parts[2])
	if err != nil {
		return Claims{}, fmt.Errorf("signature decode error: %w", err)
	}

	hash := hmac.New(sha256.New, secretKey)
	hash.Write([]byte(parts[0] + "." + parts[1]))
	if !hmac.Equal(signatureBytes, hash.Sum(nil)) {
		return Claims{}, fmt.Errorf("invalid JWT signature")
	}

	var claims Claims
	if err := json.Unmarshal(payloadBytes, &claims); err != nil {
		return Claims{}, fmt.Errorf("unmarshal claims error: %w", err)
	}

	if time.Now().Unix() > claims.ExpiresAt {
		return Claims{}, fmt.Errorf("JWT expired")
	}

	return claims, nil
}

Issuing a JWT Token

initialize Go modules:

$ go mod init wdgfs

Update server.go with following content:

package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"wdgfs/jwt"
)

func main() {
	if os.Args[1] == "token" {
		claims := jwt.Claims{
			Issuer:    "issuer",
			Subject:   "subject",
			Audience:  "audience",
			ExpiresAt: time.Now().Add(time.Hour * 1).Unix(),
			IssuedAt:  time.Now().Unix(),
			JwtId:     os.Args[2],
		}
		token, _ := jwt.WriteJWT([]byte(os.Getenv("JWT_SECRET")), claims)
		fmt.Println(token)
		return
	}

	http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
		token := strings.Split(r.Header.Get("Authorization"), " ")
		if (len(token)) != 2 {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}
		claims, err := jwt.ReadJWT([]byte(os.Getenv("JWT_SECRET")), token[1])
		if err != nil {
			http.Error(w, err.Error(), http.StatusUnauthorized)
			return
		}
		fmt.Fprintf(w, "%+v", claims)
	})

	if err := http.ListenAndServe(":3000", nil); err != nil {
		panic(err)
	}
}

Issue a token

$ JWT_SECRET=secret go run server.go token admin
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Start server

$ JWT_SECRET=secret go run server.go serve

Make a request

$ curl localhost:3000/api
Invalid Token

Make a request with token

$ curl localhost:3000/api -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
{Issuer:issuer Subject:subject Audience:audience ExpiresAt:1730035781 IssuedAt:1730032181 JwtId:admin}