แผนการเรียนรู้
/
DRF Backend | Week 12 • Part 1

หมวดที่ 1: แนะนำวิชาและโครงสร้างระบบยืนยันตัวตน

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 1

💡 คำชี้แนะ: คลิกที่เมนูเพื่อเลือกศึกษาแต่ละสไลด์ หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 1 • Week 12 Introduction

เป้าหมายการเรียนรู้ในสัปดาห์ที่ 12

ในสัปดาห์นี้เราจะพักจากการเขียนโค้ดฝั่งหน้าบ้าน Flutter ชั่วคราว เพื่อร่วมกันวางโครงร่าง REST API Backend ดึงข้อมูลทริปเดินทางจำลอง และออกแบบระบบรักษาความปลอดภัยแบบไร้สถานะ (Stateless Authentication) โดยใช้ระบบ Token ร่วมกับ Django REST Framework (DRF).

🔑 ภารกิจหลักของเราในวันนี้:

  • สร้างระบบล็อกอินและตรวจสอบสิทธิ์ด้วยคู่รหัสผ่าน
  • ออกใบรับรองสิทธิ์ (Access Token) และรหัสยืนยันตัวใหม่ (Refresh Token)
  • สร้าง Protected API Endpoint ที่กำหนดสิทธิ์อนุญาตผู้เข้าถึงทริป

📖 หัวข้อเรียนรู้อ้างอิงมาตรฐานอุตสาหกรรม:

ระบบยืนยันตัวตนที่เราจะพัฒนาสอดคล้องกับแนวปฏิบัติด้านความปลอดภัยของ OWASP และสถาปัตยกรรมระดับ Production

🔗 เอกสารอ้างอิงและไกด์ความปลอดภัยระบบ Token: LogRocket: JWT Authentication Best Practices
Slide 2 • Auth Systems Comparison

Session-Based vs. Token-Based (JWT) Authentication ⚔️

🔴 Session-Based (Stateful)

เซิร์ฟเวอร์จะสร้างเซสชันเก็บไว้ในหน่วยความจำ/ฐานข้อมูล และส่ง Session ID ไปกักเก็บไว้ในเบราว์เซอร์ผ่านคุกกี้.

  • ข้อจำกัดในการขยายระบบ: เซิร์ฟเวอร์ต้องจำสถานะผู้ใช้งานทุกคน (สร้างภาระให้ฐานข้อมูล)
  • ปัญหาการใช้คุกกี้: เกิดช่องโหว่การยิงคำขอข้ามเว็บไซต์ (CSRF Attacks)

🟢 Token-Based / JWT (Stateless)

เซิร์ฟเวอร์ตรวจสอบบัญชีสำเร็จแล้วจะส่ง JSON Web Token (JWT) กลับไปให้ไคลเอนต์เก็บไว้ และเซิร์ฟเวอร์จะไม่เก็บสถานะล็อกอินใดๆ ไว้ในฝั่งหลังบ้านเลย.

  • ขยายขนาดง่าย (Stateless): ตรวจสอบสิทธิ์โดยแกะรหัสผ่าน Signature ของกุญแจลับทันที
  • เหมาะกับแอปมือถือ: สามารถส่งผ่านค่าใน Authorization Header ได้อิสระ

💡 คำแนะนำเชิงระบบ: "อะไรสเกลได้ดีกว่าในระยะยาว?" นี่คือโจทย์ที่แยกสถาปัตยกรรมระดับโปรดักชันออกจากโปรเจกต์ระดับเริ่มต้น. การใช้ Token ช่วยแก้ปัญหานี้ให้แอปพลิเคชันอย่าง Compass App.

Slide 3 • Token Propagation Architecture

สถาปัตยกรรมการส่งผ่านรหัสยืนยันสิทธิ์ (Identity Flow) 🔄

ในสภาพแวดล้อมสถาปัตยกรรมระดับ Production การสื่อสารระบบจะเดินทางแบบทิศทางเดียว (Unidirectional Flow) และการแกะข้อมูลเพื่อสกัดบัญชีทำงานจะเป็นไปตามลำดับชั้นอย่างเป็นระเบียบ:

1. Request: ไคลเอนต์ยิงคำขอแนบ Access Token ใน Header: Authorization: Bearer <token>
2. Verification: API Middleware สกัดและส่งให้ Custom JWT Provider ตรวจลายเซ็นลายมือชื่อกุญแจลับ
3. Decision: ตรวจสอบสิทธิ์ Claims ชุดข้อมูลผู้ใช้งาน (เช่น Username) และอนุญาตเข้าถึงเนื้อหาสำเร็จ

📋 แผนผังจำลองระบบส่งผ่านข้อมูลแบบ Stateless

📱 Mobile App (Client/Flutter)
⬇️ HTTP GET /profiles {Bearer Token}
⚙️ DRF API Middleware (Backend)
⬇️ Verify Cryptographic Signature
💾 Database / Protected API View (Return JSON)
🔗 เอกสารอ้างอิงและโมเดลสถาปัตยกรรม Identity Flow: International Science Journal: Auth & Session Management
DRF Backend | Week 12 • Part 2

หมวดที่ 2: พื้นฐาน Django REST Framework & API Endpoints

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 2

💡 คำชี้แนะ: คลิกที่เมนูเพื่อเลือกศึกษาแต่ละสไลด์ หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 5 • Requirements & Project Setup

ระบบที่จำเป็นและการติดตั้งเครื่องมือฝั่งเซิร์ฟเวอร์ 🛠️

ก่อนเริ่มต้นเขียนระบบฐานข้อมูลหลังบ้าน เราจะใช้ uv ตัวจัดการแพ็กเกจ Python ความเร็วสูงยุคใหม่ ที่ทำงานแทนคู่หู pip + venv ได้ครบวงจร (จัดการ interpreter, dependencies และ lockfile ให้อัตโนมัติ):

# 1. ติดตั้ง uv (macOS/Linux)

$ curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell): irm https://astral.sh/uv/install.ps1 | iex

# 2. สร้างโปรเจกต์และติดตั้ง dependencies (uv สร้าง .venv + uv.lock ให้อัตโนมัติ)

$ uv init wk12-backend && cd wk12-backend

$ uv add django djangorestframework djangorestframework-simplejwt django-cors-headers django-oidc-provider

# 3. สร้างโปรเจกต์ Django และรันเซิร์ฟเวอร์ (ไม่ต้อง activate venv เอง)

$ uv run django-admin startproject config .

$ uv run manage.py runserver

🔗 เอกสารอ้างอิงและคู่มือการสร้างโปรเจกต์ DRF (PythonTutorials): https://www.pythontutorials.net/blog/generate-jwt-when-signing-in-with-allauth/
Slide 6 • Django Models & Serializers

การออกแบบโมเดลฐานข้อมูลและการจัดการ Serializers

Serializers มีบทบาทสำคัญในการเชื่อมโครงสร้างข้อมูลฝั่ง Backend และแอป Flutter โดยจะทำหน้าที่แปลงอ็อบเจกต์ฐานข้อมูล (Models) เป็นข้อความดิบชนิด JSON และในทางกลับกันก็ช่วยตรวจสอบความปลอดภัยข้อมูลที่ไคลเอนต์ยิงขึ้นมาจัดเก็บด้วย:

# app/serializers.py

from rest_framework import serializers

from django.contrib.auth.models import User

from .models import Booking

class BookingSerializer(serializers.ModelSerializer):

class Meta:

model = Booking

fields = ['id', 'destination_name', 'start_date', 'end_date', 'price']

# custom serializer สำหรับล็อกอินเพื่อส่งกลับ Custom Token Claims

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):

@classmethod

def get_token(cls, user):

token = super().get_token(user)

# เพิ่ม Custom Claim ลงใน Token

token['name'] = user.username

return token

🔗 คู่มือการฝังข้อมูลผู้ใช้และปรับแต่ง Token Claims (Simple JWT): https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
Slide 30 • Routing & Permissions Configuration

การสร้างเส้นทาง URL และการกำหนดสิทธิ์ในการเข้าใช้งาน (DRF)

ในการเชื่อมระบบยืนยันตัวตน simple_jwt เราต้องประกาศเส้นทางการออก Token คู่สิทธิ์ล็อกอินและจุดเชื่อมต่อขออัปเดตสิทธิ์ (Token Refresh) ลงในระบบ URL Routing ของแอปหลังบ้าน:

# config/urls.py

from django.urls import path, include

from rest_framework_simplejwt.views import (

TokenObtainPairView,

TokenRefreshView,

)

urlpatterns = [

path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),

path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

path('api/', include('app.urls')),

]

🔗 เอกสารอ้างอิงและการออกแบบระบบ API รักษาความปลอดภัย (GeeksforGeeks): https://www.geeksforgeeks.org/web-tech/json-web-token-jwt/
Slide 31 • Protected Endpoint & Testing

ปฏิบัติการสร้าง Protected API View และทดสอบยิงข้อมูล 📡

เขียนโค้ดเพื่อคุ้มครองช่องทางเรียกดูข้อมูลด้วยสิทธิ์ IsAuthenticated เพื่อสกัดกั้นผู้ใช้งานภายนอกที่ไม่ได้ผ่านกระบวนการตรวจสอบสิทธิ์:

# app/views.py (Protected API)

from rest_framework.views import APIView

from rest_framework.response import Response

from rest_framework.permissions import IsAuthenticated

class BookingListView(APIView):

permission_classes = (IsAuthenticated, )

def get(self, request):

content = {

'bookings': [{'id': 1, 'destination_name': 'Tokyo', 'price': 35000.0}]

}

return Response(content)

# 🧪 ติดตั้ง HTTPie ครั้งแรกด้วย uv (เครื่องมือทดสอบ API ที่ใช้ง่ายกว่า curl)

$ uv tool install httpie

# 🧪 ขอรับคู่ Token — httpie แปลง key=value เป็น JSON ให้อัตโนมัติ

$ http POST :8000/api/token/ username=student password=password123

# 🧪 ดึงข้อมูลทริปการท่องเที่ยวโดยแนบ Access Token ใน Header

$ http :8000/api/bookings/ "Authorization: Bearer <access_token>"

🔗 ศึกษารูปแบบคำสั่งเรียกใช้ JWT ยิงรีเฟรชข้อมูลเพิ่ม (Simple JWT): https://django-rest-framework-simplejwt.readthedocs.io/en/stable/getting_started.html
JWT & Cryptography | Week 12 • Part 3

หมวดที่ 3: เจาะลึกโครงสร้าง JSON Web Token (JWT) และความปลอดภัยคริปโทกราฟี

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 3

💡 คำชี้แนะ: คลิกที่เมนูเพื่อเลือกศึกษาแต่ละสไลด์ หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 10 • Anatomy of a JWT

เจาะลึกโครงสร้าง 3 ส่วนหลักของ JWT 🧩

ตามมาตรฐาน RFC 7519 ตัวโครงสร้างของ JSON Web Token (JWT) จะเป็นสายข้อความกะทัดรัดและปลอดภัยในการส่งผ่าน URL (URL-safe string) ประกอบขึ้นจากการแปลงข้อมูลแบบ Base64url จำนวน 3 ส่วนหลักคั่นด้วยเครื่องหมายจุด (.) :

1

Header (ส่วนหัว): บอกประเภทของ Token และอัลกอริทึมที่ใช้เข้าลายเซ็นคริปโทกราฟี (เช่น HS256 หรือ RS256) .

2

Payload (ส่วนข้อมูล): บรรจุชุดข้อมูลเรียกว่า Claims เกี่ยวกับผู้ใช้งานที่ระบุตัวตนและวันเวลาหมดอายุสิทธิ์ .

3

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

📊 แผนผังจำลองโครงร่าง JWT Structure (Base64url Encoded)

🔴 HEADER eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0
.
🟣 PAYLOAD (Claims) UmVkbW9uZCBXQSA5ODA1Mg
.
🔵 SIGNATURE (Verification Hash) AxY8DCtDaGlsbGljb3RoZQ
🔗 อ่านข้อมูลสเปคโครงสร้าง JWT เพิ่มเติมได้ที่: IETF RFC 7519 Specification
Slide 11 • Registered Claims Set

ชุดข้อมูลผู้ใช้ตามมาตรฐานสากล (Registered Claims) 📝

มาตรฐาน **RFC 7519** ได้กำหนดชุดข้อมูลระบุตัวตนและอายุการใช้งานของ Token ที่เป็นสากลและเป็นทางเลือก (Optional แต่แนะนำสำหรับระบบส่งต่อสิทธิ์ร่วมกัน) ไว้ดังนี้ :

`iss` (Issuer): ระบุเซิร์ฟเวอร์ผู้ทำการออกสิทธิ์ Token นี้

`sub` (Subject): ข้อมูลประจำตัวผู้ใช้งานเพื่อตรวจสอบยืนยันบัญชี (เช่น User ID)

`aud` (Audience): ระบุกลุ่มผู้รับหรือเครื่องปลายทางที่ Token นี้ตั้งเป้าหมายไว้

`exp` (Expiration Time): วันหมดอายุของรหัส Token ห้ามใช้ประมวลผลหลังเวลานี้

`iat` (Issued At): ระบุเวลาที่จัดสร้างและออกสิทธิ์ Token นี้ขึ้นมา

`jti` (JWT ID): รหัสจำเพาะสุ่ม (Unique ID) สำหรับสกัดการโจมตีนำสิทธิ์เก่ามารันซ้ำ

🔗 เอกสารยืนยันสเปค Claims สากล (IETF RFC): https://doi.org/10.17487/RFC7519
Slide 12 • Custom Claims & Cryptography

การออกแบบ Custom Claims และข้อต่างทางคริปโทกราฟี 🔐

💡 การออกแบบ Custom Claims

นอกเหนือจาก Claims มาตรฐานแล้ว นักศึกษาสามารถออกแบบเสริมคุณลักษณะเฉพาะงาน (Custom Claims) เช่น ชื่อผู้ใช้ (name) หรือสิทธิ์หน้าที่เข้าถึง (role) ลงในก้อน Payload ได้โดยตรงเพื่อใช้กรองสิทธิ์แสดงหน้าจอฝั่ง Flutter ได้ทันทีโดยไม่ต้องยิงดึงข้อมูลจาก API ซ้ำซ้อน .

⚠️ ข้อพึงระวังด้านความปลอดภัย: ห้ามนำข้อมูลความลับส่วนบุคคล (เช่น รหัสผ่านดิบ) ใส่ไว้ใน Payload เด็ดขาด เพราะ Base64url สามารถโดนสกัดและถอดออกอ่านข้อมูลได้ทันทีผ่านเว็บไซต์สาธารณะ!

🔑 อัลกอริทึมลายเซ็น: HS256 vs RS256

คุณลักษณะ HS256 (Symmetric) RS256 (Asymmetric)
กุญแจที่ใช้ ใช้กุญแจลับร่วมกันแชร์คีย์เดี่ยว (Symmetric Shared Secret) กุญแจคู่: ส่วนตัวสำหรับออกสิทธิ์ (Private) และส่วนตัวสาธารณะใช้แกะคีย์ (Public)
ระดับความปลอดภัย เสี่ยงต่อการรั่วไหลสูง หากเซิร์ฟเวอร์อื่นโดนเข้าถึงรหัสลับ ปลอดภัยสูงมาก: ปลายทางผู้ตรวจสอบตรวจสอบผ่านค่า Public JWK เท่านั้น
การแชร์คีย์ ต้องส่งมอบคีย์ความลับผ่านช่องทางเชื่อมโยงเสี่ยงดักจับ เผยแพร่กุญแจยืนยันด้วยความเสถียรผ่านทางพอร์ตเว็บบอร์ด /.well-known/jwks.json
🔗 เจาะลึกความปลอดภัยระบบ RS256 / JWKS และแนวทาง Penetration Testing: JUTIF: MITIGATING JWT ATTACK VECTORS
DRF Simple JWT | Week 12 • Part 4

หมวดที่ 4: การติดตั้งและกำหนดค่า Simple JWT ร่วมกับ DRF

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 4

💡 คำชี้แนะ: คลิกที่เมนูเพื่อเลือกศึกษาแต่ละสไลด์ หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 14 • settings.py Configurations

การกำหนดค่า Simple JWT ใน settings.py ⚙️

เมื่อติดตั้งคลังโปรแกรมสำเร็จแล้ว ให้นำคลาสตรวจสอบยืนยันสิทธิ์ JWTAuthentication ไปกำหนดเข้าสู่ระบบตรวจสอบสิทธิ์กลางของโปรเจกต์ Django REST Framework ในไฟลตั้งค่าหลัก :

# config/settings.py

REST_FRAMEWORK = {

'DEFAULT_AUTHENTICATION_CLASSES': (

'rest_framework_simplejwt.authentication.JWTAuthentication',

),

}

🎯 หลักการทำงาน: ระบบ DRF จะประมวลผลคำขอขาเข้าจากแอป Flutter โดยอัตโนมัติ โดยการถอดลายเซ็นตรวจสอบสิทธิ์ใน HTTP Authorization header หากระบบแกะลายเซ็นและถอดคีย์ JWT สำเร็จ จะผูกอ็อบเจกต์บัญชีผู้ใช้เข้ากับตัวแปร request.user ให้กับ API View ทันที .

🔗 แหล่งข้อมูลอ้างอิงคู่มือการกำหนดค่า (Simple JWT settings.py): https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
Slide 15 • URL Routing for Simple JWT

การประกาศเส้นทางตรวจสอบและฟื้นฟู Token 🧭

การเชื่อมต่อสิทธิ์ความปลอดภัยในระบบ Simple JWT คอนฟิกสำเร็จรูปผ่านวิวยิงสำเร็จ ได้แก่ TokenObtainPairView สำหรับใช้เป็นด่านแรกในการล็อกอิน และ TokenRefreshView สำหรับใช้เรียกขอรับคู่สิทธิ์เข้าทำงานชุดใหม่ :

# config/urls.py

from django.urls import path

from rest_framework_simplejwt.views import (

TokenObtainPairView,

TokenRefreshView,

)

urlpatterns = [

# เส้นทางล็อกอินเพื่อขอรับ Access + Refresh Token

path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),

# เส้นทางสำหรับยิงเพื่อขอรับ Access Token รอบสิทธิ์อัปเกรดใหม่

path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

]

🔗 คู่มือการทำงานใน Getting Started (Simple JWT): https://django-rest-framework-simplejwt.readthedocs.io/en/stable/getting_started.html
Slide 16 • Customizing Token Claims

ขั้นตอนปฏิบัติ: เขียน Serializer แทรก Custom Claims

หากต้องการเขียนโปรแกรมดึงข้อมูลเพื่อฝังคุณสมบัติลงสู่ก้อน Token ให้สร้างคลาสย่อย (Subclass) สืบทอดความสามารถมาจาก TokenObtainPairSerializer และทำซ้ำ (Override) เมธอด get_token ดังนี้ :

# app/serializers.py

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):

@classmethod

def get_token(cls, user):

token = super().get_token(user)

# ปรับแต่ง: แทรกชื่อและอีเมลผู้ใช้ลงใน Payload

token['username'] = user.username

token['email'] = user.email

return token

เมื่อพัฒนาเสร็จสิ้น ให้นำตัวแปรคลาสนี้ระบุลงในพจนานุกรม TOKEN_OBTAIN_SERIALIZER ภายใต้ไดเรกทอรี settings.py ของโปรเจกต์ Django หลังบ้านเพื่อเปิดใช้งานจริงแทนคลาสสถิติเดิมของระบบ [12].

🔗 เอกสารสอนการ Customizing Token Claims ฉบับสมบูรณ์ (Read the Docs): https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
Slide 17 • SIMPLE_JWT Lifetime & Summary

การลงรหัสตั้งเวลาจำกัดสิทธิ์ในระบบพิจารณาหลังบ้าน ⏳

ในระบบ Simple JWT เราสามารถควบคุมระยะเวลาความสมบูรณ์และพฤติกรรมความปลอดภัยได้อย่างอิสระ ผ่านการกำหนดตัวแปรโครงข่าย SIMPLE_JWT พจนานุกรมการอัปเดตดังต่อไปนี้ [9]:

# config/settings.py (SIMPLE_JWT Dict)

from datetime import timedelta

SIMPLE_JWT = {

'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),

'REFRESH_TOKEN_LIFETIME': timedelta(days=7),

'ROTATE_REFRESH_TOKENS': True,

'BLACKLIST_AFTER_ROTATION': True,

}

🛡️ คำจำกัดความความปลอดภัย:

  • ACCESS_TOKEN_LIFETIME: ระยะหมดอายุโทเค็นเข้าถึงข้อมูลเพื่อจำกัดความเสี่ยง (แนะนำ 5-15 นาที) [9, 13].
  • ROTATE_REFRESH_TOKENS: หมุนเวียนเปลี่ยนลายเซ็น Token ทันทีเมื่อผู้ใช้งานยื่นขอสิทธิ์รอบใหม่เพื่อการสับเปลี่ยนสิทธิ์ที่รวดเร็ว [9, 14].
  • BLACKLIST_AFTER_ROTATION: เพิกถอนยกเลิกลายเซ็น Token ชุดเดิมขึ้นบัญชีดำเพื่อลดความเสี่ยงจากการขโมยสายข้อมูล [9].
🔗 เอกสารตั้งค่า SIMPLE_JWT ฉบับทางการ: https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
OAuth 2.0 & OpenID Connect | Week 12 • Part 5

หมวดที่ 5: มาตรฐานการยืนยันตัวตนยุคใหม่ OAuth 2.0 และ OpenID Connect

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 5

💡 คำชี้แนะ: คลิกที่เมนูเพื่อเลือกศึกษาแต่ละสไลด์ หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 19 • OAuth2 Roles & OIDC Tokens

บทบาททั้ง 4 ของ OAuth 2.0 และชนิด Token ของ OIDC 🎭

OAuth 2.0 (RFC 6749) คือมาตรฐานการ "มอบสิทธิ์เข้าถึง" (Authorization) โดยไม่ต้องส่งรหัสผ่านให้แอป ส่วน OpenID Connect (OIDC) เป็นชั้นข้อมูลตัวตน (Identity Layer) ที่วางทับบน OAuth2 เพื่อบอกว่า "ผู้ใช้คนนี้เป็นใคร":

  • 🧑‍💻 Resource Owner: ผู้ใช้ปลายทาง เจ้าของข้อมูลและรหัสผ่าน
  • 📱 Client: แอปของเรา (Flutter Web App) ที่ขอสิทธิ์เข้าถึง
  • 🔐 Authorization Server (OP): เซิร์ฟเวอร์ยืนยันตัวตนที่ออก Token
  • 💾 Resource Server: REST API ที่รับ Access Token เพื่อส่งข้อมูล
Token หน้าที่
🪪 ID TokenJWT ที่พิสูจน์ตัวตนผู้ใช้ (claims เช่น name, email)
🎫 Access Tokenกุญแจสำหรับเรียก API / userinfo
♻️ Refresh Tokenใช้ขอ Access Token ชุดใหม่เมื่อหมดอายุ

📊 แผนผังบทบาทในระบบ OAuth2/OIDC

🧑‍💻 Resource Owner (ผู้ใช้)
① ขอใช้งานผ่านแอป
📱 Client — Flutter Web App (localhost:50000)
② ขอ code → แลก tokens
🔐 Authorization Server — Django OIDC Provider (localhost:8000)
③ นำ Access Token ไปเรียก API
💾 Resource Server — REST API / Database
🔗 อ่านสเปคฉบับเต็ม: OpenID Connect Core 1.0
Slide 20 • Authorization Code Flow + PKCE

Authorization Code Flow พร้อม PKCE 🔄

1

📱 Flutter เปิดเบราว์เซอร์ไปที่ GET /authorize/?response_type=code&code_challenge=…&scope=openid

2

👤 ผู้ใช้ล็อกอินที่หน้าเว็บ Django แล้วกดยืนยัน (Consent)

3

🔐 Redirect กลับ redirect_uri?code=abc123 — code ใช้ได้ครั้งเดียว อายุสั้น

4

📱 แอปส่ง POST /token/ พร้อม code + code_verifier

5

🔐 ตรวจ verifier กับ challenge สำเร็จ → ออก ID Token + Access Token (+ Refresh)

6

📱 ถอดรหัส ID Token เพื่อรู้จักผู้ใช้ และเก็บ Access Token ไว้เรียก API

💡 ทำไมต้อง PKCE? แอปมือถือ/เว็บ SPA เป็น public client (เก็บ client_secret ไม่ได้) PKCE จึงใช้คู่ code_verifier/code_challenge แทน secret ป้องกันผู้ดักฟังขโมย code ไปแลก Token (RFC 7636) 🔗 RFC 7636: Proof Key for Code Exchange

📊 แผนภาพลำดับเหตุการณ์ (Sequence Diagram)

📱 Flutter App 👤 User 🔐 Django OP ① GET /authorize/ + code_challenge ② หน้า Login + Consent ③ username / password ✓ ④ redirect_uri?code=abc123 ⑤ POST /token/ + code + verifier ⑥ ID Token + Access Token (+Refresh) 🔓 สำเร็จ! แอปรู้จักผู้ใช้แล้ว getUserInfo() → name, email | เรียก API ด้วย Access Token
Slide 21 • ID Token, Discovery & django-oidc-provider

เชื่อมโยงความรู้ JWT สู่การสร้าง OIDC Server ของเราเอง 🔗

🧩 ทุกอย่างที่เรียนมาแล้ววนกลับมาอีกครั้ง:

  • ID Token ก็คือ JWT 3 ส่วน Header.Payload.Signature (สไลด์ที่ 10)
  • Claims iss sub aud exp iat ตาม RFC 7519 (สไลด์ที่ 11)
  • เซ็นด้วย RS256 — ใครก็ตรวจได้ด้วยกุญแจสาธารณะจาก /jwks/ (สไลด์ที่ 12)
📡 OIDC Discovery Document

Endpoint มาตรฐาน /.well-known/openid-configuration จะบอก URL ของ authorize/token/userinfo/jwks ทั้งหมด — ไลบรารีฝั่งแอปใช้ URL นี้ตั้งค่าอัตโนมัติ ไม่ต้อง hard-code

🚀 แนะนำ django-oidc-provider

Django app สำเร็จรูปที่เปลี่ยนโปรเจกต์ของเราเป็น OpenID Provider (OP) ได้ในไม่กี่บรรทัด — เป็นตัว Authorization Server ในภาพสไลด์ที่ 19:

  • Endpoints ครบ: /authorize/ /token/ /userinfo/ /jwks/ + discovery
  • สร้าง/จัดการ Client (RP) ผ่าน Django Admin UI
  • ออก ID Token เซ็น RS256 (creatersakey)
  • รองรับ PKCE สำหรับ public client (RFC 7636) ✓
  • Django 3.2–5.x, Python 3.13 ✓ — ใช้กับ uv ได้ทันที
Hands-on Lab | Week 12 • Part 6

หมวดที่ 6: ปฏิบัติการ — OIDC Server ด้วย Django + เชื่อมต่อจาก Flutter Web

🧪 สารบัญขั้นตอนปฏิบัติการ (ทำตามลำดับ)

💡 เตรียมเครื่องมือให้พร้อม: uv, Flutter SDK และ Google Chrome แล้วไล่ทำตามทีละ Step
Step 0 • System Architecture Overview

ภาพรวมระบบที่เรากำลังจะสร้างร่วมกัน 🗺️

🖥️ Chrome Browser
Flutter Web App
(localhost:50000)
openid_client package
🔐 Django + django-oidc-provider
(localhost:8000)
/authorize/ • /token/ • /userinfo/
/jwks/ • /.well-known/openid-configuration
+ Admin จัดการ Users & Clients
💾 SQLite Database
Users (student01)
Clients (flutter-web-app)
RS256 Signing Keys
+ REST API (สัปดาห์ถัดไป)
① Discover
.well-known
② Authorize
+ Login
③ Redirect
?code=…
④ POST /token/
→ ID+Access
⑤ getUserInfo()
แสดงชื่อ/อีเมล

✅ เกณฑ์ความสำเร็จของ Lab นี้

  • ☐ ล็อกอินผ่านหน้า Login ของ Django ได้
  • ☐ กลับมาที่แอป Flutter พร้อมแสดงชื่อ/อีเมลผู้ใช้
  • ☐ นำ ID Token ไปวางบน jwt.io แล้วเห็น claims

🧰 เครื่องมือที่ต้องมี

uv Flutter SDK Google Chrome HTTPie (uv tool install)

⚠️ เครื่องต้องรันทั้ง backend (:8000) และ flutter (:50000) พร้อมกัน — เปิด 2 terminal

Step 1 • Build the OIDC Backend with uv

Step 1: สร้างโปรเจกต์ Backend ด้วย uv 🏗️

# Terminal 1 — สร้างโปรเจกต์และติดตั้ง dependencies

$ uv init oidc-backend && cd oidc-backend

$ uv add django django-oidc-provider django-cors-headers

$ uv run django-admin startproject config .

# สร้างตารางฐานข้อมูล + กุญแจ RS256 + บัญชี admin

$ uv run manage.py migrate

$ uv run manage.py creatersakey

$ uv run manage.py createsuperuser

# เริ่มเซิร์ฟเวอร์ที่ http://localhost:8000

$ uv run manage.py runserver

# config/urls.py — เปิด endpoints ของ OIDC provider

from django.urls import path, include

urlpatterns = [

path('admin/', admin.site.urls),

path('', include('oidc_provider.urls',

namespace='oidc_provider')),

]

# config/settings.py — เพิ่ม 4 จุดนี้

# 1) ลงทะเบียน apps

INSTALLED_APPS = [

'django.contrib.sites', # ← เพิ่ม

'corsheaders', # ← เพิ่ม

'oidc_provider', # ← เพิ่ม

...apps เดิมของ Django...

]

# 2) ตั้งค่าที่จำเป็น

SITE_ID = 1

LOGIN_URL = '/admin/login/' # ใช้หน้า login ของ admin

# 3) CORS — อนุญาตให้ Flutter Web เรียกได้ (DEV เท่านั้น!)

MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware',

*MIDDLEWARE] # ต้องวางบนสุด

CORS_ALLOW_ALL_ORIGINS = True

จุดตรวจสอบ: เปิด http://localhost:8000/admin แล้วล็อกอินด้วยบัญชี superuser ได้ = ผ่าน Step 1
Step 2 • Register an OIDC Client via Admin

Step 2: ลงทะเบียน Client และผู้ใช้ทดสอบ 👥

แอป Flutter ของเราคือ Relying Party (RP) ที่ต้องขึ้นทะเบียนกับ OP ก่อน — ไปที่ http://localhost:8000/admin → เมนู Oidc provider › Clients › Add แล้วกรอกตามตารางด้านขวา:

👤 สร้างผู้ใช้ทดสอบด้วย

เมนู Authentication and Authorization › Users › Add user — ตั้งชื่อ student01 รหัสผ่าน test1234 (กรอก email ด้วยเพื่อให้ claim email มีค่า)

⚠️ สำคัญ: หลังบันทึก Client ระบบจะสร้าง client_id ให้อัตโนมัติ — คัดลอกค่านี้ไว้ใช้ในโค้ด Flutter ของ Step 3 (client_type เป็น public จะไม่มี client_secret)
🔗 รายละเอียดฟิลด์ทั้งหมดของ Client: Docs: Relying Parties — Using the admin

📋 ค่าที่แนะนำสำหรับฟอร์มสร้าง Client

nameflutter-web-app
client_typepublic — SPA/มือถือเก็บ secret ไม่ได้
response_typescode — Authorization Code Flow
redirect_urishttp://localhost:50000
↳ ต้องตรงกับ --web-port ของ flutter run!
jwt_algRS256 (default)
reuse_consent✔ — ไม่ถาม Consent ซ้ำทุกครั้ง
💡 เหตุผลที่ fix port ไว้ที่ 50000: redirect URI ต้อง “ตรงเป๊ะ” ทุกครั้ง — ถ้าปล่อย Flutter สุ่ม port เอง จะต้องแก้ admin ใหม่ทุกรอบ
Step 3 • Flutter Web connects to our OIDC Server

Step 3: เขียน Flutter Web App เชื่อมต่อ OIDC 📱

# Terminal 2 — สร้างแอปและรันบน Chrome ที่ port 50000

$ flutter create oidc_demo --platforms web

$ cd oidc_demo

$ flutter pub add openid_client # รองรับเว็บ

$ flutter run -d chrome --web-port 50000

// lib/main.dart (ส่วนสำคัญ)

import 'package:openid_client/openid_client_browser.dart';

Future<Credential?> signIn() async {

// ① ค้นหา endpoints จาก discovery document

final issuer = await Issuer.discover(

Uri.parse('http://localhost:8000'));

final client = Client(issuer, '<client_id จาก Step 2>');

final auth = Authenticator(client,

scopes: ['openid', 'profile', 'email']);

final c = await auth.credential;

if (c == null) {

auth.authorize(); // ② ไปหน้า login ของ Django

// ③ Django redirect กลับมาหน้านี้พร้อม ?code=…

return null; // reload แล้ว c จะมีค่า

}

return c; // ④ ได้ credential สำเร็จ

}

// ปุ่มล็อกอิน + แสดงผลผู้ใช้ (ใน StatefulWidget)

ElevatedButton.icon(

icon: Icon(Icons.login),

label: Text('Sign in with OIDC'),

onPressed: () async {

final cred = await signIn();

if (cred != null) {

final info = await cred.getUserInfo();

setState(() => _user =

'${info.name} (${info.email})');

}

},

);

🔄 วงจรการทำงานโดยสรุป

ครั้งแรก credential == nullauthorize() เด้งไปหน้า Login ของ Django → ล็อกอินด้วย student01/test1234 → กลับมาหน้าเดิม → ตัวแปร credential ถูก restore จาก session → กดปุ่มอีกครั้งจะเห็นชื่อ+อีเมลทันที

🔗 pub.dev: openid_client (รองรับ Android/iOS/Web)

Step 4 • Verify with HTTPie & Troubleshooting

Step 4: ทดสอบความสมบูรณ์และแก้ปัญหาที่พบบ่อย 🩺

# ตรวจ discovery document — ต้องเห็น endpoints ครบ

$ http :8000/.well-known/openid-configuration

# ตรวจกุญแจ RS256 ที่ใช้เซ็น ID Token

$ http :8000/jwks/

🔍 พิสูจน์ว่า ID Token เป็น JWT จริง

คัดลอกค่า id_token จากแอป (print ออก console) แล้วนำไปวางบน jwt.io — จะเห็น header alg=RS256 และ claims iss, sub, aud, exp ตรงกับที่เรียนในหมวดที่ 3

🎓 สรุปวันนี้: ใช้ uv แทน pip/venv • ใช้ HTTPie แทน curl • สร้าง OIDC Server เองด้วย django-oidc-provider • Flutter Web ล็อกอินสำเร็จด้วย openid_client — สัปดาห์หน้า (Week 13 Data Layer) เราจะนำ access token ไปเรียก protected API ของ backend นี้

🔧 ตารางแก้ปัญหาที่พบบ่อย

อาการ สาเหตุ / วิธีแก้
redirect_uri_mismatch URI ใน admin ไม่ตรง — ต้องเป็น http://localhost:50000 เป๊ะๆ (รวม port)
CORS error ใน DevTools CorsMiddleware ต้องอยู่บนสุดของ MIDDLEWARE แล้ว restart runserver
invalid_client client_id คัดลอกผิด — เปิด admin › Clients ตรวจอีกครั้ง
404 /.well-known/... ยังไม่ได้ include oidc_provider.urls ใน config/urls.py
login แล้วไม่กลับแอป flutter ต้องรันด้วย --web-port 50000 ให้ตรงกับ redirect_uris ที่ลงทะเบียน