This episode builds access control: JWT with middleware.JWT, Basic Auth, session cookies, refresh tokens, RBAC middleware for role-based authorization, and OAuth2 and OpenID Connect integration with Keycloak along with the scopes concept.

Authentication answers "who are you", authorization answers "what are you allowed to do". Both are the first line of defense for your API. Echo provides middleware that cuts a lot of manual work, but understanding the flow is still mandatory.
Episode 13 builds access control: JWT with middleware.JWT, Basic Auth, session cookies, refresh tokens, RBAC middleware for role control, and OAuth2 and OpenID Connect integration with Keycloak.
middleware.JWT validates the token on every request you protect. The token is taken from the Authorization header in the Bearer <token> format:
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(cfg.JWTSecret),
ContextKey: "user",
}))Echo v5 supports github.com/golang-jwt/jwt/v5. When the token is valid, the token claims are stored in the context under the key user and can be retrieved with c.Get("user").
Create the token at login, then read the claims in handlers:
func loginHandler(c echo.Context) error {
claims := jwt.MapClaims{
"sub": user.ID,
"role": user.Role,
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(cfg.JWTSecret))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"token": signed})
}In other handlers, get the user's identity:
func profileHandler(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
role := claims["role"].(string)
return c.JSON(http.StatusOK, map[string]string{"role": role})
}c.Get("user") retrieves the token validated by the JWT middleware. Claims like role are available for authorization decisions.
For internal services or admin endpoints, Basic Auth is enough:
admin := e.Group("/admin", middleware.BasicAuth(func(user, pass string, c echo.Context) (bool, error) {
return user == "admin" && pass == cfg.AdminPassword, nil
}))middleware.BasicAuth compares the credentials from the header. Remember: Basic Auth sends credentials in base64 — only safe over HTTPS.
For classic web applications, cookie-based sessions are the natural choice. Store the session ID in a cookie, and the data in a store (memory, database, or Redis):
c.SetCookie(&http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})HttpOnly prevents JavaScript from reading the cookie, Secure ensures it's only sent over HTTPS, and SameSiteStrictMode prevents CSRF attacks.
Access tokens are short-lived; a long-lived refresh token is used to obtain a new access token:
func refreshHandler(c echo.Context) error {
refresh := c.Get("user").(*jwt.Token)
claims := refresh.Claims.(jwt.MapClaims)
sub := claims["sub"].(string)
newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": sub,
"exp": time.Now().Add(time.Hour).Unix(),
})
signed, err := newToken.SignedString([]byte(cfg.JWTSecret))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"token": signed})
}Store the refresh token in a safe place (for example in server-side storage), cap its validity, and give it the ability to be revoked if a leak is suspected.
JWT already carries the role; now create middleware that checks the role before the handler runs:
func requireRole(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
role := claims["role"].(string)
for _, r := range roles {
if role == r {
return next(c)
}
}
return echo.NewHTTPError(http.StatusForbidden, "peran tidak diizinkan")
}
}
}Install this middleware on routes that require special privileges:
adminOnly := e.Group("/admin")
adminOnly.Use(requireRole("admin"))
adminOnly.DELETE("/users/:id", deleteUser)Routes in the /admin group can only be accessed by users with the admin role; everyone else gets a 403.
For enterprise ecosystems, hand authentication over to an Identity Provider like Keycloak through OAuth2 and OpenID Connect. Point Echo's JWT middleware at Keycloak's JWKS:
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(cfg.KeycloakPublicKey),
Claims: jwt.MapClaims{},
}))The scopes concept determines the granted access rights: a token from Keycloak carries a scope claim containing scopes like profile or email. The RBAC middleware above can be extended to check scopes in addition to roles.
Episode 13 builds complete access control: JWT with middleware.JWT for stateless APIs, Basic Auth for simple cases, session cookies for web applications, refresh tokens for extending sessions, RBAC middleware for role control, and OAuth2 integration with Keycloak for enterprise scale.
Key takeaways:
middleware.JWT validates tokens and stores claims in the context.HttpOnly, Secure, and SameSite.scope claim.In episode 14 next, we'll discuss security middleware & input hardening — strict CORS configuration, security headers like CSP and HSTS, CSRF, BodyLimit, rate limiting, strict input validation, sanitization, and preventing injection, SSRF, and protecting sensitive headers.