This episode builds the authentication and authorization layer: JWT with golang-jwt as middleware, chi's built-in BasicAuth, session cookies, and the refresh token pattern. You will also learn role-based RBAC middleware and OAuth2 and OpenID Connect integration with Keycloak.

Authentication answers "who are you"; authorization answers "what are you allowed to do". Episode 13 builds both on top of chi with patterns widely used in production: JWT for API tokens, Basic Auth for simple admin access, session cookies for web, and RBAC for role-based control. Because all of these mechanisms are func(http.Handler) http.Handler middleware, you can attach them to the whole router, a subrouter, or a single route — exactly the pattern you learned in episode 6.
go get github.com/golang-jwt/jwt/v5func issueToken(userID int, role string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"role": role,
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(jwtSecret))
}jwt.NewWithClaims(jwt.SigningMethodHS256, claims) creates a token that carries the subject (user ID), role, and expiry time. token.SignedString(...) signs it with the secret.
func authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
header := req.Header.Get("Authorization")
tokenStr, _ := strings.CutPrefix(header, "Bearer ")
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
http.Error(w, "token tidak valid",
http.StatusUnauthorized)
return
}
next.ServeHTTP(w, req)
})
}strings.CutPrefix(header, "Bearer ") strips the Bearer scheme from the header. The middleware rejects the request with 401 Unauthorized when the token is malformed or expired.
For internal or admin areas, middleware.BasicAuth is enough:
r.Route("/admin", func(admin chi.Router) {
admin.Use(middleware.BasicAuth("chi-service",
map[string]string{"admin": "s3cret"}))
admin.Get("/stats", statsHandler)
})middleware.BasicAuth("chi-service", map[string]string{...}) validates the username and password from the Authorization header. Every route inside the /admin subrouter is protected.
For web apps, store the identity in a signed cookie:
func loginHandler(w http.ResponseWriter, req *http.Request) {
token, _ := issueToken(userID, role)
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
writeJSON(w, http.StatusOK, map[string]string{"login": "sukses"})
}http.Cookie{HttpOnly: true, Secure: true} prevents access from JavaScript and restricts the cookie to HTTPS only. The cookie is read in middleware to obtain the token without a manual header.
Short-lived access tokens (e.g., 15 minutes) and long-lived refresh tokens are stored safely:
r.Post("/auth/refresh", func(w http.ResponseWriter, req *http.Request) {
newAccess, _ := issueToken(userID, role)
writeJSON(w, http.StatusOK,
map[string]string{"access_token": newAccess})
})r.Post("/auth/refresh", Handle(...)) issues a new access token after the refresh token is verified — this pattern minimizes how long a leaked token remains valid.
u, ok := currentUser(req)
if !ok || u.Role != role {
http.Error(w, "akses ditolak", http.StatusForbidden)
return
}requireRole("admin") wraps routes that only admins may access. This middleware is mounted after authenticate, so the user is already identified before their role is checked.
For enterprise SSO, chi can accept tokens from an identity provider:
jwksURL := "https://auth.example.com/realms/myrealm/protocol/openid-connect/certs"
provider, _ := oidc.NewProvider(context.Background(), jwksURL)
verifier := provider.Verifier(&oidc.Config{
ClientID: "chi-service",
})provider.Verifier(...) validates the JWT signature against public keys from Keycloak. A valid ID token automatically carries the sub, email, and scope claims for further authorization.
The scope claim carries granular permissions:
scopes := strings.Fields(tokenScopes(req))
if !slices.Contains(scopes, scope) {
http.Error(w, "scope tidak dimiliki",
http.StatusForbidden)
return
}slices.Contains(scopes, scope) checks whether the token has the requested scope. The scopes pattern lets an API serve clients with different permissions.
Key takeaways:
middleware.BasicAuth is enough for simple admin areas.HttpOnly and Secure.requireRole and requireScope add an authorization layer.In the next episode 14 we strengthen our defenses: security middleware and input hardening — CORS with rs/cors, CSP and HSTS security headers, CSRF, body limits, rate limiting, input validation, and preventing injection and SSRF with safe X-Forwarded-For handling.