Authentication and Authorization in Web Apps
Introduction
Authentication (who you are) and authorization (what you can do) are critical for web application security. This guide covers implementing both in Go web applications.
Core Concepts
Authentication vs Authorization
- Authentication: Verifying user identity (login)
- Authorization: Checking user permissions (access control)
Common Methods
- Session-based: Server stores session data
- Token-based: Client stores token (JWT)
- OAuth2: Third-party authentication
- API Keys: Simple token authentication
Good: JWT Authentication
Implementing JWT
package main
import (
"github.com/golang-jwt/jwt/v4"
"time"
)
// โ
GOOD: JWT claims
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// โ
GOOD: Generate JWT token
func generateToken(userID, email, role string) (string, error) {
claims := &Claims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("secret-key"))
}
// โ
GOOD: Verify JWT token
func verifyToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("secret-key"), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
return claims, nil
}
JWT Middleware
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
// โ
GOOD: JWT middleware
func jwtMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing token"})
c.Abort()
return
}
// Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token format"})
c.Abort()
return
}
claims, err := verifyToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
c.Abort()
return
}
// Store claims in context
c.Set("user_id", claims.UserID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Next()
}
}
// โ
GOOD: Role-based middleware
func requireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
if userRole != role {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
c.Abort()
return
}
c.Next()
}
}
Good: Session-Based Authentication
Session Management
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: Setup session store
func setupSessions(router *gin.Engine) {
store := cookie.NewStore([]byte("secret-key"))
router.Use(sessions.Sessions("session", store))
}
// โ
GOOD: Login handler
func handleLogin(c *gin.Context) {
var loginReq struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&loginReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify credentials
user, err := verifyCredentials(loginReq.Email, loginReq.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
return
}
// Store in session
session := sessions.Default(c)
session.Set("user_id", user.ID)
session.Set("email", user.Email)
session.Save()
c.JSON(http.StatusOK, gin.H{"message": "Logged in"})
}
// โ
GOOD: Session middleware
func sessionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
userID := session.Get("user_id")
if userID == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Set("user_id", userID)
c.Next()
}
}
// โ
GOOD: Logout handler
func handleLogout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
session.Save()
c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
}
func verifyCredentials(email, password string) (*User, error) {
// Verify credentials
return nil, nil
}
Good: OAuth2 Integration
OAuth2 Setup
package main
import (
"context"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"net/http"
)
// โ
GOOD: OAuth2 configuration
var googleOAuthConfig *oauth2.Config
func initOAuth2() {
googleOAuthConfig = &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RedirectURL: "http://localhost:8080/auth/google/callback",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
}
// โ
GOOD: OAuth2 login handler
func handleOAuth2Login(c *gin.Context) {
url := googleOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// โ
GOOD: OAuth2 callback handler
func handleOAuth2Callback(c *gin.Context) {
code := c.Query("code")
token, err := googleOAuthConfig.Exchange(context.Background(), code)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to exchange token"})
return
}
// Get user info
client := googleOAuthConfig.Client(context.Background(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
// Store token and create session
c.JSON(http.StatusOK, gin.H{"message": "Authenticated"})
}
Advanced Patterns
Multi-Factor Authentication
package main
import (
"github.com/pquerna/otp/totp"
)
// โ
GOOD: TOTP setup
func setupTOTP(userID string) (string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "MyApp",
AccountName: userID,
})
if err != nil {
return "", err
}
return key.Secret(), nil
}
// โ
GOOD: Verify TOTP
func verifyTOTP(secret, code string) bool {
return totp.Validate(code, secret)
}
Permission-Based Access Control
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
// โ
GOOD: Permission checking
func requirePermission(permission string) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// Check if user has permission
hasPermission := checkUserPermission(userID.(string), permission)
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{"error": "Forbidden"})
c.Abort()
return
}
c.Next()
}
}
func checkUserPermission(userID, permission string) bool {
// Check permission in database
return true
}
Best Practices
1. Never Store Passwords in Plain Text
// โ
GOOD: Hash passwords
import "golang.org/x/crypto/bcrypt"
func hashPassword(password string) (string, error) {
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
func verifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// โ BAD: Store plain text
func storePassword(password string) {
// db.Save(password) // NEVER!
}
2. Use HTTPS
// โ
GOOD: Use HTTPS in production
router.RunTLS(":443", "cert.pem", "key.pem")
// โ BAD: Use HTTP
router.Run(":80")
3. Secure Token Storage
// โ
GOOD: Store tokens securely
// Use httpOnly cookies for web apps
// Use secure storage for mobile apps
// โ BAD: Store in localStorage
// localStorage.setItem("token", token) // Vulnerable to XSS
Resources
- JWT: https://jwt.io/
- OAuth2: https://oauth.net/2/
- OWASP Authentication: https://owasp.org/www-community/attacks/authentication_cheat_sheet
Summary
Proper authentication and authorization are essential for web application security. Use JWT for stateless authentication, sessions for traditional web apps, and OAuth2 for third-party integration. Always hash passwords, use HTTPS, and follow security best practices.
Comments