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.

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 maps roles to a list of 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.
Middleware checks the user's role, then matches permissions:
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 moves authentication to a provider like Keycloak, Google, or GitHub. Set up the config, then redirect the user:
go get golang.org/x/oauth2oauthCfg := &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.
Browsers honor a number of headers that reduce attacks:
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.
Web forms are vulnerable to cross-site requests. The most common protection is a synchronizer 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.
Episode 12 built a per-IP limiter. For a more mature version, use a limiter with a per-IP fallback and a separating key:
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.
Key takeaways:
c.GetString("role").Exchange swaps the code for a token.nosniff shrink browser attacks.HttpOnly cookies prevent cross-site requests.rate.Limiter safely limits requests per IP.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.