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
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)Salt is automatic với bcrypt (mỗi hash có random salt riêng)
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") }Account lockout sau X failed attempts
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
- Use strong secret: Minimum 256 bits (32 bytes) random
- Short expiry: 15 minutes cho access token
- Use refresh tokens: Long-lived refresh token để lấy access token mới
- Store in memory: Không lưu JWT trong localStorage (XSS risk)
- Validate signature: Luôn verify signature trước khi trust claims
- Check expiry: Đừng quên validate
expclaim - 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-offsoauth2-and-oidc.md— Third-party authenticationadvanced-auth.md— MFA, SSO, Zero Trust