Advanced Authentication: MFA, SSO & Zero Trust
Khi authentication cơ bản (username/password) không đủ an toàn, chúng ta cần các phương pháp nâng cao hơn: Multi-Factor Authentication (MFA), Single Sign-On (SSO), và Zero Trust Architecture. Đây là những gì enterprise systems và security-critical apps sử dụng.
🔐 Ẩn dụ: Password là chìa khóa nhà. MFA là thêm vân tay + mã PIN. SSO là master key mở nhiều cửa. Zero Trust là "kiểm tra mọi người ở mọi cửa, mọi lúc".
Multi-Factor Authentication (MFA)
MFA yêu cầu user chứng minh identity bằng 2 hoặc nhiều factors từ 3 categories:
┌─────────────────────────────────────────────────┐
│ 1. Something you KNOW (password, PIN) │
│ 2. Something you HAVE (phone, hardware key)│
│ 3. Something you ARE (fingerprint, face) │
└─────────────────────────────────────────────────┘
Ví dụ MFA:
- Password (know) + SMS code (have)
- Password (know) + Fingerprint (are)
- Hardware key (have) + PIN (know)
TOTP (Time-based One-Time Password)
TOTP là dạng MFA phổ biến nhất (Google Authenticator, Authy, 1Password).
How TOTP works
┌────────────┐ ┌────────────┐
│ Server │ │ Client │
│ │ │ (Auth app) │
└─────┬──────┘ └─────┬──────┘
│ │
│ 1. User enrolls MFA │
│ Generate secret key (32 bytes) │
│ │
│ 2. Show QR code with secret │
├──────────────────────────────────────────►│
│ otpauth://totp/App:user@example.com │
│ ?secret=BASE32_ENCODED_SECRET │
│ │
│ 3. Client scans QR & stores secret
│ HMAC-SHA1(secret, time_counter)
│ → 6-digit code (changes every 30s)
│ │
│ 4. User enters code: 123456 │
│◄──────────────────────────────────────────┤
│ │
│ 5. Server computes: │
│ current_code = HMAC(secret, current_time/30)
│ Verify user's code matches │
│ (allow ±1 time window for clock skew) │
│ │
│ 6. Grant access if codes match │
│ │
Implementation (Go)
import (
"crypto/rand"
"encoding/base32"
"fmt"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
// Generate TOTP secret for new user
func generateTOTPSecret(issuer, accountName string) (*otp.Key, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer, // "MyApp"
AccountName: accountName, // "user@example.com"
Period: 30, // New code every 30 seconds
Digits: 6, // 6-digit code
})
return key, err
}
// Enroll MFA
func handleMFAEnroll(w http.ResponseWriter, r *http.Request) {
userID := getUserIDFromSession(r)
email := getEmailFromSession(r)
// Generate secret
key, err := generateTOTPSecret("MyApp", email)
if err != nil {
http.Error(w, "Failed to generate secret", 500)
return
}
// Store secret in DB (encrypted!)
encryptedSecret := encrypt(key.Secret())
db.Exec("UPDATE users SET totp_secret = ? WHERE id = ?", encryptedSecret, userID)
// Return QR code to user
qrCode := key.String() // otpauth://totp/...
// Generate QR image from qrCode string (use library like github.com/skip2/go-qrcode)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"qr_code": qrCode,
"secret": key.Secret(), // Show in case user can't scan QR
})
}
// Verify TOTP code during login
func verifyTOTP(secret, code string) bool {
return totp.Validate(code, secret)
}
// Login flow with MFA
func handleLoginWithMFA(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
totpCode := r.FormValue("totp_code")
// Step 1: Verify password
user, err := db.GetUserByEmail(email)
if err != nil || !checkPassword(password, user.PasswordHash) {
http.Error(w, "Invalid credentials", 401)
return
}
// Step 2: If MFA enabled, verify TOTP
if user.TOTPSecret != "" {
decryptedSecret := decrypt(user.TOTPSecret)
if !verifyTOTP(decryptedSecret, totpCode) {
http.Error(w, "Invalid MFA code", 401)
return
}
}
// Step 3: Create session
createSession(w, user.ID)
w.WriteHeader(200)
}
// Generate backup codes (in case user loses device)
func generateBackupCodes(userID int64) []string {
codes := make([]string, 10)
for i := range codes {
b := make([]byte, 4)
rand.Read(b)
codes[i] = fmt.Sprintf("%08X", b)
}
// Store hashed backup codes in DB
for _, code := range codes {
hashedCode := hashPassword(code)
db.Exec("INSERT INTO backup_codes (user_id, code_hash) VALUES (?, ?)", userID, hashedCode)
}
return codes // Show to user once, tell them to save
}
// Verify backup code
func verifyBackupCode(userID int64, code string) bool {
var codeHash string
err := db.QueryRow("SELECT code_hash FROM backup_codes WHERE user_id = ? AND used = false", userID).Scan(&codeHash)
if err != nil {
return false
}
if !checkPassword(code, codeHash) {
return false
}
// Mark as used (one-time use)
db.Exec("UPDATE backup_codes SET used = true WHERE code_hash = ?", codeHash)
return true
}
Security Best Practices for TOTP
✅ Store secret encrypted in database
✅ Generate backup codes (10x codes for recovery)
✅ Allow ±1 time window (to handle clock skew)
✅ Rate limit verification attempts (max 5 per minute)
✅ Require re-authentication before disabling MFA
WebAuthn / FIDO2 (Passwordless)
WebAuthn là standard mới nhất cho passwordless authentication sử dụng hardware keys (YubiKey) hoặc biometrics (Touch ID, Face ID).
How WebAuthn works
┌─────────────────────────────────────────────────────────┐
│ Registration Flow │
└─────────────────────────────────────────────────────────┘
Browser Server Authenticator (YubiKey)
│ │ │
│ 1. POST /register │ │
├─────────────────────►│ │
│ │ 2. Generate challenge │
│ │ (random bytes) │
│ 3. Challenge │ │
│◄─────────────────────┤ │
│ │ │
│ 4. navigator.credentials.create() │
├──────────────────────────────────────────────────►
│ │ 5. User touches key │
│ │ Generate key pair │
│ │ Sign challenge │
│ 6. Public key + signature │
│◄──────────────────────────────────────────────────┤
│ │ │
│ 7. POST public key │ │
├─────────────────────►│ │
│ │ 8. Store public key in DB │
│ │ │
│ 9. Success │ │
│◄─────────────────────┤ │
│ │ │
┌─────────────────────────────────────────────────────────┐
│ Authentication Flow │
└─────────────────────────────────────────────────────────┘
│ 1. POST /login │ │
├─────────────────────►│ │
│ │ 2. Generate challenge │
│ 3. Challenge │ │
│◄─────────────────────┤ │
│ │ │
│ 4. navigator.credentials.get() │
├──────────────────────────────────────────────────►
│ │ 5. User touches key │
│ │ Sign with private key
│ 6. Signature │ │
│◄──────────────────────────────────────────────────┤
│ │ │
│ 7. POST signature │ │
├─────────────────────►│ │
│ │ 8. Verify with public key │
│ │ (from DB) │
│ 9. Token │ │
│◄─────────────────────┤ │
Implementation (Go)
import "github.com/go-webauthn/webauthn/webauthn"
var (
webAuthn *webauthn.WebAuthn
)
func init() {
wconfig := &webauthn.Config{
RPDisplayName: "My App",
RPID: "example.com",
RPOrigin: "https://example.com",
}
webAuthn, _ = webauthn.New(wconfig)
}
// Begin registration
func handleBeginRegistration(w http.ResponseWriter, r *http.Request) {
user := getUserFromSession(r) // Implement WebAuthnUser interface
options, session, err := webAuthn.BeginRegistration(user)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Store session (contains challenge)
storeSession("webauthn_session", session)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(options)
}
// Finish registration
func handleFinishRegistration(w http.ResponseWriter, r *http.Request) {
user := getUserFromSession(r)
session := getStoredSession("webauthn_session")
credential, err := webAuthn.FinishRegistration(user, session, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Store credential in DB
db.Exec("INSERT INTO webauthn_credentials (user_id, credential_id, public_key) VALUES (?, ?, ?)",
user.ID, credential.ID, credential.PublicKey)
w.WriteHeader(200)
}
// Begin login
func handleBeginLogin(w http.ResponseWriter, r *http.Request) {
user := getUserByUsername(r.FormValue("username"))
options, session, err := webAuthn.BeginLogin(user)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
storeSession("webauthn_session", session)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(options)
}
// Finish login
func handleFinishLogin(w http.ResponseWriter, r *http.Request) {
user := getUserFromSession(r)
session := getStoredSession("webauthn_session")
_, err := webAuthn.FinishLogin(user, session, r)
if err != nil {
http.Error(w, "Authentication failed", 401)
return
}
// Create session token
createSession(w, user.ID)
w.WriteHeader(200)
}
Frontend (JavaScript)
// Registration
async function registerWebAuthn() {
// Get options from server
const optionsRes = await fetch('/webauthn/register/begin', {method: 'POST'});
const options = await optionsRes.json();
// Convert base64 to ArrayBuffer (needed for WebAuthn API)
options.publicKey.challenge = base64ToArrayBuffer(options.publicKey.challenge);
options.publicKey.user.id = base64ToArrayBuffer(options.publicKey.user.id);
// Create credential (triggers browser prompt / hardware key)
const credential = await navigator.credentials.create(options);
// Send to server
await fetch('/webauthn/register/finish', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
response: {
attestationObject: arrayBufferToBase64(credential.response.attestationObject),
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
},
type: credential.type,
}),
});
}
// Login
async function loginWebAuthn() {
const optionsRes = await fetch('/webauthn/login/begin', {
method: 'POST',
body: JSON.stringify({username: 'alice'}),
});
const options = await optionsRes.json();
options.publicKey.challenge = base64ToArrayBuffer(options.publicKey.challenge);
const assertion = await navigator.credentials.get(options);
await fetch('/webauthn/login/finish', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: assertion.id,
rawId: arrayBufferToBase64(assertion.rawId),
response: {
authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
signature: arrayBufferToBase64(assertion.response.signature),
userHandle: arrayBufferToBase64(assertion.response.userHandle),
},
type: assertion.type,
}),
});
}
Benefits of WebAuthn
✅ Phishing-resistant: Public key cryptography, không gửi secret qua network
✅ No password: Không lo password leaks/breaches
✅ Hardware-backed: Private key không rời khỏi device
✅ Standards-based: W3C standard, support bởi all major browsers
✅ Better UX: Touch fingerprint hoặc tap YubiKey
Single Sign-On (SSO)
SSO cho phép user login một lần và access nhiều apps.
SSO Architecture
┌─────────────────────────────────────────────────────────┐
│ │
│ User logs in once │
│ ↓ │
│ ┌────────────────┐ │
│ │ Identity │ │
│ │ Provider (IdP) │ │
│ │ (Okta, Auth0) │ │
│ └────────┬───────┘ │
│ │ │
│ ┌────────┼─────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────┐ ┌──────┐ ┌──────┐ │
│ │ App │ │ App │ ... │ App │ │
│ │ A │ │ B │ │ Z │ │
│ └─────┘ └──────┘ └──────┘ │
│ (All apps trust IdP) │
└─────────────────────────────────────────────────────────┘
SSO Protocols
1. SAML 2.0 (Security Assertion Markup Language)
Use case: Enterprise SSO (legacy, XML-based)
Flow diagram:
Browser Service Provider (SP) Identity Provider (IdP)
│ (Your App) (Okta)
│ │ │
│ 1. Visit app │ │
├──────────────────────►│ │
│ │ 2. Not authenticated │
│ │ Redirect to IdP │
│ 3. SAMLRequest │ with SAMLRequest │
├───────────────────────┴────────────────────────►│
│ │
│ 4. User logs in (if needed)
│ & approves │
│ │
│ 5. SAMLResponse (signed XML assertion) │
│◄─────────────────────────────────────────────────┤
│ │ │
│ 6. POST SAMLResponse │ │
├──────────────────────►│ │
│ │ 7. Verify signature │
│ │ Extract user info │
│ │ Create session │
│ 8. App access │ │
│◄──────────────────────┤ │
SAML Assertion Example (simplified):
<saml:Assertion>
<saml:Subject>
<saml:NameID>user@example.com</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>user@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="groups">
<saml:AttributeValue>admins</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<Signature>...</Signature>
</saml:Assertion>
2. OIDC (OpenID Connect)
Use case: Modern SSO (JSON-based, built on OAuth 2.0)
OIDC đã cover ở oauth2-and-oidc.md.
→ For SSO, IdP (Google, Okta) acts as OIDC provider.
Comparison:
| SAML | OIDC | |
|---|---|---|
| Format | XML | JSON |
| Transport | HTTP POST/Redirect | HTTP + REST |
| Mobile support | ❌ Poor | ✅ Great |
| Modern | ❌ Legacy | ✅ Modern |
| Use case | Enterprise | Consumer + Enterprise |
Recommendation: Dùng OIDC cho new projects, SAML nếu bắt buộc (enterprise requirements).
Zero Trust Architecture
Old model (Perimeter security):
┌─────────────────────────────────────┐
│ Firewall │
│ ┌───────────────────────────────┐ │
│ │ Internal Network (TRUSTED) │ │
│ │ • No auth between services │ │
│ │ • Assume all traffic is safe│ │
│ └───────────────────────────────┘ │
│ │
│ Outside = Dangerous │
│ Inside = Safe │
└─────────────────────────────────────┘
Problem: Nếu attacker vào được network → full access.
Zero Trust model:
┌─────────────────────────────────────┐
│ NEVER TRUST, ALWAYS VERIFY │
└─────────────────────────────────────┘
Every request is authenticated
& authorized, regardless of source
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Service │ mTLS │ Service │ mTLS │ Service │
│ A ├────────►│ B ├────────►│ C │
└─────────┘ └─────────┘ └─────────┘
↓ ↓ ↓
Verify Verify Verify
identity identity identity
Zero Trust Principles
- Verify explicitly: Authenticate & authorize based on all data points (identity, device, location, etc.)
- Least privilege access: Just-In-Time & Just-Enough-Access (JIT/JEA)
- Assume breach: Minimize blast radius, segment access, verify end-to-end encryption
Implementing Zero Trust
1. Mutual TLS (mTLS)
Both client AND server verify each other's certificates.
Normal TLS:
Client verifies server certificate
✅ Server is who it claims to be
❌ Server doesn't know WHO the client is
mTLS:
Client verifies server certificate
Server verifies client certificate
✅ Both parties authenticated
Implementation (Go):
// Server with mTLS
func startMTLSServer() {
// Load server certificate
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
// Load CA certificate (to verify clients)
caCert, _ := os.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
server.ListenAndServeTLS("", "")
}
// Client with mTLS
func callWithMTLS(url string) {
// Load client certificate
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
// Load CA certificate (to verify server)
caCert, _ := os.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, _ := client.Get(url)
// ...
}
2. Service Mesh (Istio, Linkerd)
Service mesh automatically injects mTLS, authentication, authorization.
# Istio: Require mTLS for all services
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: default
spec:
mtls:
mode: STRICT
---
# Authorization policy: only service-a can call service-b
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: service-b-authz
namespace: default
spec:
selector:
matchLabels:
app: service-b
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/service-a"]
to:
- operation:
methods: ["GET", "POST"]
3. Identity-based Access (not IP-based)
// ❌ OLD: IP whitelist
if req.RemoteAddr == "10.0.1.5" {
// Allow
}
// ✅ ZERO TRUST: Verify identity
claims := verifyJWT(req.Header.Get("Authorization"))
if claims.Service == "service-a" && claims.Role == "admin" {
// Allow
}
4. Device Trust
Check device health before granting access:
- Is device managed by company?
- Is disk encrypted?
- Is OS up to date?
- Is antivirus running?
Example (BeyondCorp by Google):
- User device must be registered & healthy
- Verify identity (OAuth/OIDC)
- Check context (location, time)
- Grant access based on least privilege
Comparing MFA, SSO, Zero Trust
| Feature | Traditional | MFA | SSO | hZero Trust |
|---|---|---|---|---|
| Auth method | Password | Password + 2nd factor | One login for all apps | Continuous verification |
| Security | ⚠️ Weak | ✅ Strong | ⚠️ Depends on IdP | ✅ Strongest |
| UX | Simple | Extra step | Best (1 login) | Transparent |
| Use case | Low security apps | Critical apps | Enterprise | High security orgs |
Real-world combo: SSO + MFA + Zero Trust
→ Login once with MFA, then every service call is verified (Zero Trust).
Best Practices
MFA
✅ Prefer authenticator apps (TOTP) over SMS (SMS can be intercepted)
✅ Support hardware keys (YubiKey) for high-value accounts
✅ Provide backup codes for account recovery
✅ Enforce MFA for admins and sensitive operations
SSO
✅ Use OIDC for new systems (SAML if required by enterprise)
✅ Short-lived ID tokens (15-60 min)
✅ Validate ID token signatures
✅ Implement logout (single logout across all apps)
Zero Trust
✅ Never trust, always verify
✅ Use mTLS for service-to-service
✅ Implement least privilege (RBAC/ABAC)
✅ Monitor & log all access attempts
✅ Segment network (micro-segmentation)
Tóm tắt
| Method | What | When | How |
|---|---|---|---|
| MFA | Multiple factors to prove identity | Login to critical apps | TOTP, WebAuthn, SMS |
| SSO | Login once, access all apps | Enterprise with many apps | SAML, OIDC |
| Zero Trust | Verify every request | High-security environments | mTLS, identity-based access |
| Passwordless | No password needed | Modern apps | WebAuthn, passkeys |
Golden rule: Defense in depth — combine multiple layers (SSO + MFA + Zero Trust) for maximum security.
Bước tiếp theo
../crypto-basics.md— How TOTP, signatures work under the hood../distributed-systems-security.md— mTLS, service mesh securitysession-vs-jwt.md— Token management for SSO