เขียนระบบ Login เองทำไม ใช้ Firebase Authentication สิ

คิดว่าหลายคนน่าจะได้ลองเล่น Firebase กันแล้ว แต่บางทีก็อาจจะต้องเขียน Backend เอง เพราะว่า Data บางอย่างเก็บใน Firebase Database ไม่ได้

แทนที่เราจะเขียนเองทั้งหมด เราลองเอา Firebase Authentication มาใช้ดู จะช่วยลดเวลาทำระบบ Login ได้เยอะเลย เพราะเราสามารถใช้ Firebase SDK บน Client ได้ แทบไม่ต้องเขียน code อะไรเลย

ถ้าใครได้อ่านบทความก่อนหน้านี้ของผมเรื่อง Token-based Authentication (https://acoshift.me/token-based-authentication-golang-307f18b03dc3#.loy3xo13k) ก็พอจะรู้แล้วว่า Firebase Authentication ทำงานยังไง

Firebase SDK ช่วย Refresh Token ให้เรา ทำให้เราไม่ต้องเขียน Code ส่วนนี้เลย ก็แค่ดึงเอา Access Token ออกมาจาก Firebase SDK แล้วส่งให้ Backend ของเราก็จบแล้ว

firebase.auth().currentUser.getToken().then((accessToken) => ...)

แล้วเราจะเช็คได้ยังไงหล่ะ ว่า Token อันนี้เป็นของ User คนไหน ?

ใครที่ใช้ Node.js ก็ง่ายเลย เพราะสามารถลง firebase-admin มาใช้ได้ มี feature ให้เกือบครบ (ขาดแค่ list user, ณ วันที่ 25/12/2016)

สำหรับคนใช้ภาษาอื่น ลองมาดูกันว่าเราจะต้องทำยังไงบ้าง

Access Token ที่ Firebase สร้างมาให้ ถูก signed ด้วย วิธี RS256 ดังนั้นเราสามารถเอา Public Key มา verify ได้… แล้วจะเอา Public Key มาจากไหนหล่ะ…

https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com

ถ้าดูดี ๆ จะให้ว่า มันให้ Public Key มา 3–4 อัน!!! ถ้ารอสักพักแล้วกด refresh ใหม่ มันกลายเป็นอันใหม่!!!!

แสดงว่าเราก็ต้องโหลด public key มา แล้วเช็คว่า max-age มันเท่าไร (หมดอายุตอนไหน) พอหมดอายุแล้วก็ต้องไปดึงมาใหม่

เราก็จะได้หน้าตา function fetchKeys ออกมาประมาณนี้

var (
keysMutex = &sync.RWMutex{}
keys map[string]*rsa.PublicKey
)
func fetchKeys() error {
keysMutex.Lock()
defer keysMutex.Unlock()
resp, err := http.Get("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")
if err != nil {
return err
}
defer resp.Body.Close()
 keysExp, _ = time.Parse(time.RFC1123, resp.Header.Get("Expires"))
 m := map[string]string{}
if err = json.NewDecoder(resp.Body).Decode(&m); err != nil {
return err
}
ks := map[string]*rsa.PublicKey{}
for k, v := range m {
p, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(v))
if p != nil {
ks[k] = p
}
}
keys = ks
return nil
}

แล้วเราจะรู้ได้ยังไงว่า Token อันนี้จะต้องใช้ Key ไหนมา verify ?

ใน header ของ token จะมี field นึงที่ชื่อว่า kid (Key ID) อยู่ เราจะต้องเอาค่านี้ออกมาเพื่อเลือกว่าจะใช้ key ไหน

และตอนเลือกก็ต้องเช็คก่อนด้วยว่า keys ที่มีอยู่ มันหมดอายุหรือยัง

func selectKey(kid string) *rsa.PublicKey {
keysMutex.RLock()
if keysExp.IsZero() || keysExp.Before(time.Now()) || len(keys) == 0 {
keysMutex.RUnlock()
if err := fetchKeys(); err != nil {
return nil
}
keysMutex.RLock()
}
defer keysMutex.RUnlock()
return keys[kid]
}

พอได้ key ก็เอาไป validate กับ token ได้เลยครับ

แต่ยังไม่หมดนะครับ เราจะต้องเช็คด้วยว่า token นี้เป็นของ project ไหน เพราะ Firebase จะสร้าง token ด้วย private key เดียวกันสำหรับหลาย ๆ project

วิธีการเช็คว่า token อันนี้เป็นของ project เรารึป่าว ก็เช็ค Audience (aud)ใน payload ว่าตรงกับ project id ของเราไหม และเช็ค Issuer (iss) ว่า ตรงกับ https://securetoken.google.com/{projectId} ด้วยไหม

และเราสามารถดึง UserID ออกมาจาก Token ได้จาก field Subject ใน payload


แล้วจะดึง User data ออกมาจาก Firebase ยังไงหล่ะ ?

เนื่องจาก Firebase Authentication นั้น implement อยู่บน Google Identity Toolkit (git) เราจึงสามารถส่ง request ไปถามบน endpoint เดียวกับ git เลยได้

นอกจากนี้เรายังสามารถเข้าไปเปลี่ยน data ของ user, ลบ user, เปลี่ยนรหัสผ่าน หรือกระทั่ง disable user ก็ได้

ข้อเสียของ library ของ Node.js ตอนนี้คือยังไม่สามารถ list user ออกมาได้ แต่เราสามารถส่ง request ไปแทนได้นะ


สำหรับคนใช้ภาษา Go ผมได้เขียนเป็น library ง่าย ๆ ไว้แล้ว สามารถลองโหลดไปใช้ได้ที่ https://github.com/acoshift/go-firebase-admin

ใครที่สงสัยว่าต้องส่ง requet อะไรไปหา firebase เพื่อเอาข้อมูล user สามารเข้าไปดูใน code ได้เลยครับ


ทิ้งท้าย

ในเมื่อ Firebase Authentication มันพัฒนาต่อมาจาก Identity Toolkit แล้วทำไมไม่ใช้ Library ของ git หล่ะ ?… ผมลองแล้วครับ แต่มันใช้มะได้ เหมือน public key มันใช้คนละตัวกัน :3

Like what you read? Give acoshift a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.