🔐 Security✍️ Khoa📅 20/04/2026☕ 10 phút đọc

Authentication & Authorization

Authentication (AuthN) và Authorization (AuthZ) là hai khái niệm cốt lõi trong security, nhưng rất dễ bị nhầm lẫn. Hiểu rõ sự khác biệt và cách implement chúng đúng là prerequisite để xây dựng hệ thống an toàn.

🎭 Ẩn dụ: Authentication là "bạn là ai?" (check ID card), Authorization là "bạn được phép làm gì?" (check access pass).


Authentication vs Authorization

Authentication Authorization
Câu hỏi "Bạn là ai?" "Bạn có quyền làm việc này không?"
Thời điểm Trước tiên Sau Authentication
Mechanisms Password, biometrics, MFA RBAC, ABAC, ACLs
Fails when Credentials sai User không có permission
HTTP Status 401 Unauthorized 403 Forbidden
┌─────────────────────────────────────────┐
│  1. Request với credentials             │
│     (username/password, token, etc)     │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│  2. AUTHENTICATION                      │
│     "Bạn có phải là Alice không?"       │
│     → Check password, verify token      │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│  3. AUTHORIZATION                       │
│     "Alice có quyền DELETE post này?"   │
│     → Check permissions, roles          │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│  4. Access granted or denied            │
└─────────────────────────────────────────┘

Password-based Authentication

Nguyên tắc cơ bản

NEVER store plain-text passwords:

// 🔥 NEVER DO THIS
user := User{Email: email, Password: password}
db.Create(&user)  // password lưu trực tiếp?!

ALWAYS hash passwords:

// ✅ Correct approach
import "golang.org/x/crypto/bcrypt"

func hashPassword(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hash), err
}

func checkPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

Password Hashing Algorithms

Algorithm Status Use case
MD5, SHA1 ❌ Broken NEVER use for passwords
SHA256 ⚠️ Too fast OK for checksums, not passwords
bcrypt ✅ Good Default choice for most apps
scrypt ✅ Good More resistant to hardware attacks
argon2 ✅ Best Winner of password hashing competition

Tại sao SHA256 không đủ?
SHA256 quá nhanh → attacker có thể brute-force billions hashes/giây với GPU. Bcrypt/scrypt/argon2 được thiết kế để cố tình chậm (computationally expensive) → khó brute-force hơn.

Best Practices

  1. Use bcrypt/argon2 with proper cost

    // bcrypt cost = 12 là reasonable (tăng lên nếu hardware cải thiện)
    bcrypt.GenerateFromPassword([]byte(password), 12)
  2. Salt is automatic với bcrypt (mỗi hash có random salt riêng)

  3. Rate limiting cho login attempts:

    // Max 5 attempts per 15 minutes
    if loginAttempts[userID] > 5 {
        return errors.New("too many login attempts, try again later")
    }
  4. Account lockout sau X failed attempts

  5. Password strength requirements:

    • Minimum 12 characters (16+ is better)
    • Mix of upper/lower/numbers/symbols
    • Check against common passwords list (Have I Been Pwned)

Session-based Authentication

Cách truyền thống: server lưu session state, client giữ session ID trong cookie.

┌─────────┐                              ┌─────────┐
│ Client  │                              │ Server  │
└────┬────┘                              └────┬────┘
     │                                        │
     │  POST /login (username, password)     │
     ├──────────────────────────────────────►│
     │                                        │
     │                         ┌──────────────▼────────┐
     │                         │ 1. Verify password     │
     │                         │ 2. Create session      │
     │                         │    sessionID = uuid()  │
     │                         │ 3. Store in Redis:     │
     │                         │    SET sess:abc userID │
     │                         └──────────────┬────────┘
     │                                        │
     │   Set-Cookie: sessionID=abc; HttpOnly │
     │◄──────────────────────────────────────┤
     │                                        │
     │  GET /api/profile                     │
     │  Cookie: sessionID=abc                │
     ├──────────────────────────────────────►│
     │                                        │
     │                         ┌──────────────▼────────┐
     │                         │ 1. GET sess:abc        │
     │                         │    → userID = 123      │
     │                         │ 2. Fetch user data     │
     │                         └──────────────┬────────┘
     │                                        │
     │   { "name": "Alice", "email": "..." } │
     │◄──────────────────────────────────────┤
     │                                        │

Implementation Example (Go)

type SessionStore interface {
    Set(sessionID string, userID int64, ttl time.Duration) error
    Get(sessionID string) (int64, error)
    Delete(sessionID string) error
}

// Redis implementation
type RedisSessionStore struct {
    client *redis.Client
}

func (s *RedisSessionStore) Set(sessionID string, userID int64, ttl time.Duration) error {
    return s.client.Set(ctx, "session:"+sessionID, userID, ttl).Err()
}

func (s *RedisSessionStore) Get(sessionID string) (int64, error) {
    val, err := s.client.Get(ctx, "session:"+sessionID).Int64()
    if err == redis.Nil {
        return 0, errors.New("session not found")
    }
    return val, err
}

// Middleware
func SessionAuth(store SessionStore) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            cookie, err := r.Cookie("sessionID")
            if err != nil {
                http.Error(w, "Unauthorized", 401)
                return
            }

            userID, err := store.Get(cookie.Value)
            if err != nil {
                http.Error(w, "Unauthorized", 401)
                return
            }

            // Attach userID to context
            ctx := context.WithValue(r.Context(), "userID", userID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Pros & Cons

Pros:

  • ✅ Server có full control (có thể revoke session bất cứ lúc nào)
  • ✅ Stateful → có thể track active sessions
  • ✅ Dễ implement logout (just delete session)

Cons:

  • ❌ Requires session storage (Redis, DB)
  • ❌ Horizontal scaling cần shared session store
  • ❌ Cross-domain auth khó hơn

JWT (JSON Web Token) Authentication

JWT là stateless token: tất cả info cần thiết nằm trong token, server không cần lưu state.

JWT Structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

├─────────── HEADER ────────────┤├────────────── PAYLOAD ──────────────┤├─────────── SIGNATURE ────────┤

Header (Base64):

{
  "alg": "HS256",    // Algorithm
  "typ": "JWT"
}

Payload (Base64):

{
  "sub": "1234567890",     // Subject (user ID)
  "name": "John Doe",
  "iat": 1516239022,       // Issued at
  "exp": 1516242622        // Expiry
}

Signature:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Implementation Example

import "github.com/golang-jwt/jwt/v5"

type Claims struct {
    UserID int64  `json:"user_id"`
    Email  string `json:"email"`
    jwt.RegisteredClaims
}

func GenerateJWT(userID int64, email string, secret []byte) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secret)
}

func ValidateJWT(tokenString string, secret []byte) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return secret, nil
    })

    if err != nil {
        return nil, err
    }

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }

    return nil, errors.New("invalid token")
}

// Middleware
func JWTAuth(secret []byte) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                http.Error(w, "Missing Authorization header", 401)
                return
            }

            // Expect "Bearer <token>"
            tokenString := strings.TrimPrefix(authHeader, "Bearer ")
            claims, err := ValidateJWT(tokenString, secret)
            if err != nil {
                http.Error(w, "Invalid token", 401)
                return
            }

            ctx := context.WithValue(r.Context(), "userID", claims.UserID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

JWT Best Practices

  1. Use strong secret: Minimum 256 bits (32 bytes) random
  2. Short expiry: 15 minutes cho access token
  3. Use refresh tokens: Long-lived refresh token để lấy access token mới
  4. Store in memory: Không lưu JWT trong localStorage (XSS risk)
  5. Validate signature: Luôn verify signature trước khi trust claims
  6. Check expiry: Đừng quên validate exp claim
  7. Avoid sensitive data: JWT có thể decode → không lưu passwords, credit cards

Pros & Cons

Pros:

  • ✅ Stateless → no server storage needed
  • ✅ Horizontal scaling dễ dàng
  • ✅ Cross-domain / cross-service auth
  • ✅ Mobile-friendly

Cons:

  • ❌ Không thể revoke (phải đợi expire hoặc dùng blacklist)
  • ❌ Token size lớn hơn session ID
  • ❌ Cần cẩn thận với secret management

Session vs JWT: Khi nào dùng cái nào?

Scenario Recommendation
Monolith web app với server-rendered pages Session-based ✅
Microservices / distributed systems JWT ✅
Mobile app JWT ✅
Cần revoke access ngay lập tức Session-based ✅
High security requirements (banking, etc) Session-based + MFA ✅
Third-party API access JWT (OAuth 2.0) ✅
Internal admin panel Session-based ✅

Hybrid approach: Dùng JWT cho access token (short-lived), session-based cho refresh token (có thể revoke).


Authorization Models

1. Role-Based Access Control (RBAC)

User được assign roles, mỗi role có một set permissions.

User ──► Role ──► Permissions
Alice → Admin  → [read, write, delete]
Bob   → Editor → [read, write]
Charlie → Viewer → [read]
type Role string

const (
    RoleAdmin  Role = "admin"
    RoleEditor Role = "editor"
    RoleViewer Role = "viewer"
)

var rolePermissions = map[Role][]string{
    RoleAdmin:  {"read", "write", "delete"},
    RoleEditor: {"read", "write"},
    RoleViewer: {"read"},
}

func hasPermission(userRole Role, requiredPerm string) bool {
    perms, ok := rolePermissions[userRole]
    if !ok {
        return false
    }
    for _, p := range perms {
        if p == requiredPerm {
            return true
        }
    }
    return false
}

// Middleware example
func RequirePermission(perm string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userRole := r.Context().Value("role").(Role)
            if !hasPermission(userRole, perm) {
                http.Error(w, "Forbidden", 403)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Pros: Đơn giản, dễ hiểu, đủ cho majority use cases
Cons: Không flexible với complex permission requirements

2. Attribute-Based Access Control (ABAC)

Permissions dựa trên attributes của user, resource, environment.

Rule: User có thể edit document nếu:
  - User là owner của document, HOẶC
  - User có role Editor VÀ document.status = "draft"
type Document struct {
    ID      int64
    OwnerID int64
    Status  string
}

func canEdit(userID int64, userRole Role, doc Document) bool {
    // Rule 1: Owner can always edit
    if doc.OwnerID == userID {
        return true
    }

    // Rule 2: Editors can edit drafts
    if userRole == RoleEditor && doc.Status == "draft" {
        return true
    }

    return false
}

Pros: Rất flexible, có thể model complex rules
Cons: Phức tạp hơn, khó debug

3. Access Control Lists (ACLs)

Mỗi resource có list các (user, permission) pairs.

Document #123:
  - Alice: read, write
  - Bob: read
  - Team:Marketing: read

Pros: Granular control per resource
Cons: Khó scale khi số lượng users/resources lớn


Common Mistakes

❌ Mistake 1: Client-side authorization

// 🔥 NEVER DO THIS
if (user.role === 'admin') {
    // Show delete button
    deletePost(postID);
}

Client-side checks chỉ là UX enhancement. ALWAYS enforce authorization ở server-side:

func deletePost(w http.ResponseWriter, r *http.Request) {
    userRole := r.Context().Value("role").(Role)
    if userRole != RoleAdmin {
        http.Error(w, "Forbidden", 403)
        return
    }
    // Proceed with delete
}

❌ Mistake 2: Không check resource ownership

// 🔥 BAD: Bất kỳ ai cũng có thể delete bất kỳ post nào
func deletePost(postID int64) error {
    return db.Delete(&Post{}, postID).Error
}

// ✅ GOOD: Check ownership
func deletePost(userID, postID int64) error {
    var post Post
    if err := db.First(&post, postID).Error; err != nil {
        return err
    }
    
    if post.AuthorID != userID {
        return errors.New("forbidden: not the author")
    }
    
    return db.Delete(&post).Error
}

❌ Mistake 3: Hardcoded role checks everywhere

// 🔥 BAD: Logic lặp lại khắp nơi
func handler1() {
    if user.Role != "admin" { return }
    // ...
}
func handler2() {
    if user.Role != "admin" { return }
    // ...
}
// ✅ GOOD: Centralized authorization
func RequireRole(role Role) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.Context().Value("role") != role {
                http.Error(w, "Forbidden", 403)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

router.With(RequireRole(RoleAdmin)).Post("/admin/users", createUser)

Testing Authentication/Authorization

func TestJWTAuth(t *testing.T) {
    secret := []byte("test-secret")

    t.Run("valid token", func(t *testing.T) {
        token, _ := GenerateJWT(123, "alice@example.com", secret)
        claims, err := ValidateJWT(token, secret)
        
        assert.NoError(t, err)
        assert.Equal(t, int64(123), claims.UserID)
    })

    t.Run("expired token", func(t *testing.T) {
        // Generate token with past expiry
        claims := Claims{
            UserID: 123,
            RegisteredClaims: jwt.RegisteredClaims{
                ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
            },
        }
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        tokenString, _ := token.SignedString(secret)

        _, err := ValidateJWT(tokenString, secret)
        assert.Error(t, err)
    })

    t.Run("invalid signature", func(t *testing.T) {
        token, _ := GenerateJWT(123, "alice@example.com", secret)
        _, err := ValidateJWT(token, []byte("wrong-secret"))
        assert.Error(t, err)
    })
}

Tóm tắt

  • Authentication = verify identity, Authorization = verify permissions
  • Session-based: Stateful, server control, good for monoliths
  • JWT: Stateless, scalable, good for distributed systems
  • RBAC: Simple role-based permissions (most common)
  • ABAC: Attribute-based, flexible but complex
  • Always enforce authorization server-side, never trust client
  • Test auth logic thoroughly — security bugs are costly

Bước tiếp theo

  • session-vs-jwt.md — Deep dive vào trade-offs
  • oauth2-and-oidc.md — Third-party authentication
  • advanced-auth.md — MFA, SSO, Zero Trust