Learn Gin - RBAC, OAuth2 & Security Middleware
Series/Learn Gin/Episode 14
Episode 14 of 23

Learn Gin - RBAC, OAuth2 & Security Middleware

This episode completes the security side: RBAC and permission middleware, OAuth2 and OpenID Connect integration with Keycloak, helmet-style security headers, CSRF protection for forms, and rate limiting with golang.org/x/time/rate.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Authentication answers the question "who is this user". This episode 14 answers the next question: what are they allowed to do? You'll dissect role-based authorization (RBAC), OAuth2 and OpenID Connect integration with Keycloak, and a collection of security middleware: CORS, helmet-style security headers, CSRF protection for forms, and mature rate limiting.

Why does this matter? Letting every user perform every operation is a recipe for disaster. Add to that, wrong headers or overly open CORS can open holes that get exploited from the browser. The middleware you install here closes those holes one by one.

RBAC and Permission Middleware

The Role and Permission Map

RBAC maps roles to a list of permissions:

Defining roles and permissions
type Permission string
 
const (
    PermUserRead  Permission = "user:read"
    PermUserWrite Permission = "user:write"
)
 
var rolePermissions = map[string][]Permission{
    "admin": {PermUserRead, PermUserWrite},
    "editor": {PermUserRead},
}

rolePermissions maps roles like admin and editor to the list of permissions they may perform. This structure is easy to read and extend: adding a new role is just a new entry in the map.

The requirePermission Middleware

Middleware checks the user's role, then matches permissions:

Permission middleware
func requirePermission(perm Permission) gin.HandlerFunc {
    return func(c *gin.Context) {
        role := c.GetString("role")
        allowed := false
        for _, p := range rolePermissions[role] {
            if p == perm {
                allowed = true
                break
            }
        }
        if !allowed {
            c.AbortWithStatusJSON(403, gin.H{"error": "akses ditolak"})
            return
        }
        c.Next()
    }
}
 
r.PUT("/users/:id", authMiddleware(), requirePermission(PermUserWrite), updateUser)

c.GetString("role") retrieves the role stored by the authentication middleware from episode 13. If the user doesn't have the requested permission, the middleware returns 403 Forbidden. Note the order: authentication first (who), then authorization (what's allowed).

OAuth2 and OpenID Connect

Redirecting to Authorization

OAuth2 moves authentication to a provider like Keycloak, Google, or GitHub. Set up the config, then redirect the user:

Install OAuth2
go get golang.org/x/oauth2
Keycloak OAuth2 config
oauthCfg := &oauth2.Config{
    ClientID:     "belajar-gin",
    ClientSecret: "client-rahasia",
    RedirectURL:  "http://localhost:8080/auth/callback",
    Endpoint: oauth2.Endpoint{
        AuthURL:  "https://keycloak.example.com/realms/dev/protocol/openid-connect/auth",
        TokenURL: "https://keycloak.example.com/realms/dev/protocol/openid-connect/token",
    },
    Scopes: []string{"openid", "profile"},
}
 
r.GET("/auth/login", func(c *gin.Context) {
    url := oauthCfg.AuthCodeURL("state-acak")
    c.Redirect(302, url)
})

oauthCfg.AuthCodeURL("state-acak") generates the OAuth2 provider URL where the user logs in. The state parameter prevents CSRF attacks on the OAuth flow — always use a random value and verify it in the callback.

Security Headers

Security Header Middleware

Browsers honor a number of headers that reduce attacks:

Security headers middleware
func securityHeaders() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("X-Content-Type-Options", "nosniff")
        c.Header("X-Frame-Options", "DENY")
        c.Header("Referrer-Policy", "no-referrer")
        c.Header("Content-Security-Policy", "default-src 'self'")
        c.Next()
    }
}

c.Header("Content-Security-Policy", "default-src 'self'") restricts which content sources a page may load — the main defense against XSS. The combination of nosniff, X-Frame-Options: DENY, and Referrer-Policy shrinks the browser attack surface. For HTTPS, add Strict-Transport-Security as discussed in episode 15.

CSRF for Forms

Session-Based CSRF Tokens

Web forms are vulnerable to cross-site requests. The most common protection is a synchronizer token:

Set a CSRF token
func setCSRF(c *gin.Context) {
    token := csrfToken() // acak, disimpan di session
    c.SetCookie("csrf_token", token, 3600, "/", "", true, true)
    c.HTML(200, "form.html", gin.H{"csrf_token": token})
}

c.SetCookie("csrf_token", token, 3600, "/", "", true, true) writes the token to a cookie with the Secure and HttpOnly flags. When the form is submitted, c.Cookie("csrf_token") is compared to the form token using a constant-time comparison. Malicious sites can't read the cookie because of the HttpOnly flag, so they can't forge a token. For APIs using session cookies, this is a must — the combination of CSRF and unprotected cookies is very dangerous.

Rate Limiting with golang.org/x/time/rate

Global Rate Limit Middleware

Episode 12 built a per-IP limiter. For a more mature version, use a limiter with a per-IP fallback and a separating key:

Per-IP rate limit with a nested map
var (
    limitMu   sync.Mutex
    limiters  = make(map[string]*rate.Limiter)
    interval  = rate.Every(time.Minute)
)
 
func rateLimit() gin.HandlerFunc {
    return func(c *gin.Context) {
        ip := c.ClientIP()
        limitMu.Lock()
        lim, ok := limiters[ip]
        if !ok {
            lim = rate.NewLimiter(interval, 60)
            limiters[ip] = lim
        }
        limitMu.Unlock()
 
        if !lim.Allow() {
            c.AbortWithStatusJSON(429, gin.H{"error": "terlalu banyak request"})
            return
        }
        c.Next()
    }
}

rate.NewLimiter(interval, 60) allows 60 requests per minute per IP with a burst of 60. c.ClientIP() uses the engine's trusted proxies — make sure they're configured correctly (episode 15) so the IP can't be easily spoofed. Clean the map periodically so old IP limiters don't pile up.

Closing

Key takeaways:

  • RBAC maps roles to permission lists; middleware checks c.GetString("role").
  • OAuth2 redirects login to a provider; Exchange swaps the code for a token.
  • Security headers like CSP and nosniff shrink browser attacks.
  • CSRF tokens in forms + HttpOnly cookies prevent cross-site requests.
  • rate.Limiter safely limits requests per IP.
  • Always install authentication before authorization in the middleware chain.

In the next episode, episode 15, we'll dissect HTTPS, trusted proxies & input hardening — configuring trusted proxies correctly, running TLS, integrating nginx and caddy reverse proxies, and preventing injection and SSRF with safe pagination.

Learn Gin - RBAC, OAuth2 & Security Middleware | Learn Gin