จะใช้ Cookie ทำ API แทน JWT ต้องทำอะไรบ้าง ?

เนื่องจากบทความที่แล้ว ทำไมเราถึงควรกลับมาใช้ Cookie แทนการส่ง JWT Token ผ่าน Header มีคนสงสัยว่า แล้วถ้าจะใช้ Cookie ต้องทำยังไงหล่ะ ?

ถ้าเรามี

  • Web App รันอยู่บน http://localhost:3000
  • API รันอยู่บน http://localhost:8080

เราจะทำให้ Web App คุยกับ API ยังไง ผ่าน Cookie แทน Token ที่ส่งผ่าน Header

มาดูหน้าตา Web App ของเรากันก่อน

จะเห็นว่าพอใช้ cookie แล้ว เวลา login ไม่ต้องมานั่งเก็บ token ลงใน local storage เลย เขียนง่ายมาก

มาดูสิ่งที่ต้องทำเพิ่มกันก่อนดีกว่า

1. บอกให้ browser ส่ง cookie ไปด้วย

withCredentials: true

หรือถ้าเราใช้ fetch ก็แค่เพิ่ม

credentials: 'include'

ถ้าเราใส่ withCredentials เป็น true แล้ว browser จะส่ง cookie ไปด้วย แต่ browser ไม่ได้ส่งไปทันทีนะ มันจะถาม api ของเราด้วย preflight request ก่อน แต่เดี๋ยวมาดูกันว่าเราจะตอบ browser ยังไงให้มันส่ง cookie มาด้วย

2. บอก api ของเราว่า request ส่งมาจาก ajax

ด้วยการเพิ่ม header อะไรก็ได้ เช่น

X-Requested-With: XMLHttpRequest

เพราะว่าถ้าเป็น form หรือ iframe จะใส่ header มาด้วยไม่ได้

วิธีนี้เป็น optional จะทำหรือไม่ทำก็ได้ แต่ถ้าทำเราสามารถป้องกันไม่ให้เข้าผ่าน form ได้


มาดูส่วน API กันบ้าง

ตอนนี้ api เราหน้าตาแบบนี้

cookie login=1 เป็นแค่ตัวอย่างเฉย ๆ อย่าเอาไปใช้จริงหล่ะ 😬
ปกติก็ใช้ library ที่จัดการ session ทำให้

แน่นอนว่าตอนนี้เรายิง request ไม่ได้ เพราะติด Cross-Origin Resource Sharing (CORS)

Failed to load http://localhost:8080/login: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access.

ถึงแม้เราจะส่ง JWT ผ่าน header ยังไงเราก็ต้อง implement CORS อยู่ดี

เรามาเขียน CORS middleware กันก่อน

จะเห็นว่าเราจะ Allow Origin เฉพาะ http://localhost:3000 และมี Allow Credentials เป็น true ด้วย เพื่อบอก browser ว่าให้ส่ง cookie มาได้

เนื่องจากว่าเรา Allow Credentials เราจึงไม่สามารถ Allow Origin: * ได้
เพราะว่าถ้าทำได้ ทุกเว็บในโลกจะสามารถยิง api เราโดยมี cookie ได้
If the resource supports credentials add a single Access-Control-Allow-Origin header, with the value of the Origin header as value, and add a single Access-Control-Allow-Credentials header with the case-sensitive string “true” as value.
Otherwise, add a single Access-Control-Allow-Origin header, with either the value of the Origin header or the string “*” as value.
The string “*” cannot be used for a resource that supports credentials.
 — https://www.w3.org/TR/cors/ หัวข้อ 6.1.3

ตอนนี้เราก็สามารถยิง API ของเราได้แล้ว (หลังจากกด Login)

ยิง api หลังจาก login ได้

แต่เดี๋ยวก่อน!!! ยังไม่จบ มีอะไรให้ดูต่อ

ลองเอา html ตัวนี้มารันที่ http://localhost:5000 ดู

แน่นอนว่ายิง ajax ไม่ได้อยู่แล้ว เพราะเราไม่ได้ allow origin ที่ http://localhost:5000 ไว้

ยิง ajax ไม่ได้ เพราะติด CORS

แล้วถ้าใช้ iframe หล่ะ ???

iframe ยิงได้

แล้วถ้าลองใช้ form ดูหล่ะ ?

ใช้ form กดเข้ามาได้

คราวนี้มาดูกันว่า แล้วเราจะป้องกัน CSRF ได้ยังไง

1. เพิ่ม header X-Frame-Options

X-Frame-Options: deny

ก็จะใช้ iframe เปิดไม่ได้แล้ว

Refused to display 'http://localhost:8080/' in a frame because it set 'X-Frame-Options' to 'deny'.

2. เช็คว่า request มาจาก ajax

if r.Header.Get("X-Requested-With") != "XMLHttpRequest" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

เพราะว่า form ใส่ header ไม่ได้

ให้เฉพาะ request ที่มาจาก ajax ผ่านได้

3. เช็ค Origin เพิ่มด้วยก็ดี

if r.Header.Get("Origin") != "http://localhost:3000" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

แต่ถ้ากลัวว่าบาง browser ไม่ส่ง origin มา อาจจะเช็คเมื่อมี origin ก็ได้

if origin := r.Header.Get("Origin"); len(origin) > 0 {
if origin != "http://localhost:3000" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
}

มาดู code เต็ม ๆ กัน

เห็นไหม เขียน code เพิ่มนิดเดียวเอง แค่ set headers ให้ถูก ก็ไม่ต้องมานั่งทำ JWT เลย