This episode covers JWT authentication in Fiber: the github.com/gofiber/contrib/jwt middleware, signing keys with HS256, accessing claims in handlers, issuing tokens at login, and the security middleware for response security headers.

Modern API authentication relies heavily on JSON Web Tokens (JWT) — tokens that can be verified without storing state on the server. Episode 16 covers using JWT in Fiber v3: middleware from github.com/gofiber/contrib/jwt, reading claims, issuing tokens at login, and the security middleware for response headers.
JWT is practical, but easy to misuse. Leaked signing keys, weak algorithms, and tokens that never expire are common sources of problems. That's why this episode also stresses the security practices around it.
The contrib/jwt package wraps golang-jwt/jwt/v5. The minimal configuration: a signing algorithm and a key:
import jwtware "github.com/gofiber/contrib/jwt"
app.Use("/protected", jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
JWTAlg: jwtware.HS256,
Key: []byte("secret-rahasia-ubah-ini"),
},
ContextKey: "jwt",
Claims: jwt.MapClaims{},
}))SigningKey determines the algorithm (HS256) and the secret key. Claims: jwt.MapClaims{} tells the middleware the expected claim structure. This middleware is attached to the /protected prefix, so every route under it requires a valid token — requests without a token or with a broken token are rejected immediately.
The algorithm choice determines how you manage keys:
// HS256: satu kunci rahasia untuk sign dan verify
Key: []byte(os.Getenv("JWT_SECRET"))
// RS256: private key untuk sign, public key untuk verify
// cocok untuk banyak service
JWTAlg: jwtware.RS256,
Key: rsaPublicKey,HS256 uses a single secret key — fast and simple, good for a single service. RS256 uses a key pair: other services only need the public key to verify, without knowing the private key. Choose RS256 when tokens are validated across services.
After passing the middleware, the token is stored in the context. Handlers retrieve it via c.Locals with the ContextKey:
app.Get("/protected/profile", func(c fiber.Ctx) error {
token := c.Locals("jwt").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
name := claims["name"].(string)
role := claims["role"].(string)
return c.JSON(fiber.Map{"name": name, "role": role})
})c.Locals("jwt").(*jwt.Token) retrieves the verified token. From token.Claims (typed as jwt.MapClaims) you read claims like name and role. This pattern is the basis of authorization — for example denying access if role != "admin".
Clients send the token through the Authorization header. The contrib/jwt middleware uses Authorization: Bearer <token> by default:
curl -H "Authorization: Bearer <TOKEN>" http://localhost:3000/protected/profileWithout the header, the middleware replies with 401 Unauthorized. With a valid token, the handler extracts the claims and returns the user data.
Tokens must be created somewhere — usually the login endpoint. The same signing key is used here to sign:
app.Post("/login", func(c fiber.Ctx) error {
var creds Credentials
if err := c.Bind().JSON(&creds); err != nil {
return err
}
user, err := verifyLogin(creds)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "kredensial salah")
}
claims := jwt.MapClaims{
"name": user.Name,
"role": user.Role,
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return err
}
return c.JSON(fiber.Map{"token": signed})
})Login verifies the credentials, builds the claims (including exp one hour ahead), signs the token, and returns it. The exp claim is required so the token expires — never create a token without an expiration time.
Besides JWT, Fiber provides the security middleware for setting response security headers:
app.Use(security.New(security.Config{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 3600,
HSTSExcludeSubdomains: false,
}))This middleware adds headers like X-XSS-Protection, X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security. The headers are set directly on c.Response().Header, protecting responses from XSS, clickjacking, and MIME sniffing attacks.
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
--data '{"username":"budi","password":"rahasia123"}' \
http://localhost:3000/login | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/protected/profileFirst get a token from /login, then use that token in the Authorization header. Without a token, /protected/profile replies 401; with a valid token, the handler reads the name and role claims.
Key takeaways:
github.com/gofiber/contrib/jwt with SigningKey (HS256/RS256).c.Locals(ContextKey) as a *jwt.Token; claims are accessed from token.Claims.Authorization: Bearer <token> header.exp) using SignedString.security middleware adds response security headers.In the next episode, episode 17, we discuss file upload — multipart forms with the multipart middleware, Memory configuration, FormFile access, and safe file storage.