Skip to main content

Implementing Authentication Logic

เนื่องจากเรามี 3 ส่วนที่ต้อง implement เดียวจะเรื่มจากชั้นที่อยู่นอกที่สุดก่อะครับ

ก่อนจะมา handle function ต่าง  ส่วนมากผมจะทำ function utils ต่าง  เพื่อมาช่วยในเราการ dev ก่อน และ 1 function สำคัญที่ขาดไม่ได้เลย ก็คือ bodyparser และเราจะไม่ไปลง lib ที่ไหนครับ เพราะผมลองบางตัวแล้วไม่ถูกใจเนื่องจากผมอยากจะให้ตัว body นั้นถูกยัดเข้าไปใน type ของเราเลยนันเอง งั้นเรามาเขียนเองกันเลยดีกว่าครับ

ส่วนคนที่ไม่ทราบว่า bodyparser คืออะไร เรามาอ่านส่วนนี้กันก่อนครับ

bodyparser คือ function ที่จะแปลง body ของ request ที่ clientClient เรียกมาออกมาเป็น format ต่าง  ไม่ว่าจะเป็น json หรืออื่น 

สร้างไฟล์ bodyparser.go ขี้นมาแล้วใส่โค้ดนี้ลงไปได้เลยครับ

package utils

func Parse[T any](r *http.Request, body T) error {
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return err
	}
	if err := json.Unmarshal([]byte(b), &body); err != nil {
		return err
	}
	return nil
}

function ที่เราจะทำนั้นผมทำให้เป็น generic เพื่อที่จะได้สามารถรับ type อะไรก็ได้ แล้วเอา value มาใส่ให้ลงไปใน poiner ของตัวแปล ซึ่งอย่างที่เห็นคือผมรับ request (สิ่งที่เก็บ value ของ body) ของ net/http และตัวแปลของเราซึ่งเป็น generic type

ต่อมาเราจะอ่าน body ให้ออกมาเป็น byte array หรือ ในที่นี้คือ json ที่ถูก encode อยู่ และนำมา unmarshal เข้า pointer ของตัวแปล และหากไม่มี error ก็ให้ return ค่า nil กลับไป เท่านี้เราก็จะได้ bodyparser ง่าย  มาแล้วครับ

SignUp (POST request)

Overall Spec

Screenshot 2565-11-07 at 22.20.05.png

หลังจากเห็นภาพรวม service เราไปแล้ว ก่อนจะทำ function POST ใด  ผมจะทำสร้าง struct ของ request ไว้ก่อนครับ เพื่อให้ง่ายต่อการรับ body เพราะสาเหตุผลที่เราสร้าง function bodyparser เพอให้เราสามารถสร้าง type ไว้รับ body ได้โดยง่ายนันเอง

สร้าง folder type และสร้างไฟล์ user.go ด้านใน

package types

type SignIn struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

การสร้าง struct เราควรจะใส่ชื่อ attribute เป็นรูปแบบ Capitalize เพราะ golangGolang จะมองว่า attribute นั้นเป็น private และไม่สามารถดึงมาใช้ได้นันเอง ส่วนด้านหลังเอาไว้ unmarshal value เข้าไปนั้นเอง ดังน้ันเราควรจะตั้งให้เหมือนกับ request ที่จะได้รับ

Handler

func (h userHandler) SignUp(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	var response []byte
	var body types.SignIn
	err := utils.Parse(r, &body)
	if err != nil {
		response_value := map[string]any{"success": false, "error": err.Error()}
		response, _ := json.Marshal(response_value)
		w.Write(response)
		return
	}

	token, base64, err := h.service.SignUp(body.Email, body.Password)
	if err != nil {
		response_value := map[string]any{"success": false, "error": err.Error()}
		response, _ := json.Marshal(response_value)
		w.Write(response)
		return
	}
	response, _ = json.Marshal(map[string]any{"success": true, "token": token, "image": base64})
	w.Write(response)
	return
}

ก่อนอื่นใน function นี้ผมจะตรวจ method ของ request นี้ก่อนและค่อย set content-type เป็น application/json ที่เราคุ้นหน้าุ้นตาเพื่อให้ content ออกมาเป็นรูปแบบที่เราต้องการจริง ๆ ต่อมา parse body โดยใช้ function ที่เราสร้างก่อนหน้านี้ และหากเกิด error ผมได้มีการ handle ไว้และส่ง response ที่เราต้องการกลับไป และ return ด้วย เพราะไม่งั้นจะเกิด error เนื่องจากเรา write ไปแล้ว แล้วต่ดันมีคำสั่ง write ใหม่

ลำดับต่อมาก็จะเป็นขั้นตอนการเรียก function ที่จะไปทำ business logic ของเรา และรับค่าที่ return ออกมา ซึ่งใน spec ของเราคือกาาจะส่ง token และรูปที่เป็น base64 กลับมานันเอง หลังจากทำ function สำเร็จและไม่มีข้อผิดพลาด เราก็จะปั้นตัว response ของเราและ write กล้บไปให้ user นันเอง

Service

เริ่มจา่อนอื่น การลง library 2 ตัวก่อน นั่นคือ TOTP, และ JWT ที่ซึ่งสำคัญกับการทำระบบ authentication ของเรานั้นคือ TOTP, และ JWT

$ go get github.com/pquerna/otp/totp

$ go get github.com/golang-jwt/jwt

func (s userService) SignUp(email string, password string) (*string, *string, error) {
	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      "GDSC KMUTT",
		AccountName: email,
	})
	if err != nil {
		return nil, nil, err
	}
	secret := key.Secret()
	hashedPwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return nil, nil, err
	}

	user, err := s.repository.CreateUser(email, string(hashedPwd), secret)
	if err != nil {
		return nil, nil, err
	}
	claims := jwt.MapClaims{
		"id":  user.Id,
		"exp": time.Now().Add(time.Hour * 72).Unix(),
	}

	// Create token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(config.C.JWT_SECRET))

	// Convert TOTP key into a PNG
	var buf bytes.Buffer
	img, err := key.Image(200, 200)
	if err != nil {
		return nil, nil, err
	}
	if err := png.Encode(&buf, img); err != nil {
		return nil, nil, err
	}
	base64string := "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
	return &tokenString, &base64string, nil
}

ส่วน service จะเยอะนิดนึงครับ เพราะเป็นส่วนที่รวม business logic ทั้งหมดที่จะประมวณผลข้อมูลที่เราได้มารับออกมาเป็นสิ่งที่เราต้องการ

เริ่มจากพระเอกของเรา TOTP ที่เราจะมาลอง implement กัน ซึ่งหลักการของมันไม่ได้มีอะไรซับซ้อนครับ ก็คือเราจะ generate key ของ user คนปัจจุบันมาก่อน และเราก็จะได้ object key ซึ่งมีข้อมูลสำคัญที่เราต้องเก็บ นันคือ secret ของ user นั้นเอง และ object key นี้สามารถดีงค่าอย่างอื่นออกมาได้ด้วย เช่น QRcode ที่เอาไว้สแกนโดยไม่ต้องใส่ secret โดยตรง

ดังนั้นใน function create user ผมเลยใส่ parameter secret เข้าไปด้วย เพื่อที่จะเอา secret ไปเก็บด้วยนันเอง คราวนี้หลังจากได้ key มา เราก็จะมาทำให้ password ของ user ปลอดภัยขึ้น ด้วยการ hashed นั้นเอง ซึ่งจะทำให้ password กลายเป็น text ที่เราอ่านไม่ออก เพื่อและี่จะให้คนที่ดูแล database ไม่สามารถดู password ของ user ได้นั้นเอง และหลังจากได้รับข้อมูลทั้งหมดที่จะนำไปเก็บใน database แล้วเราก็เรียก function create user ผ่าน "Port"Port ที่เราสร้างได้เลย

หลังจากที่ได้ user ที่ return กลับมาเราก็นำมาสร้าง token JWT ได้เลย โดยกำหนดส่งที่จะอยู่ใน token (ควรมี exp ไว้ตลอด เพราะเป็นื่อบ่งบอกอายุการใช้งานของ token) ในที่นี้เราใส่แค่ id ของ user ไปก่อน หลังจากได้ token ก็นำไปเข้า function signedstring ซึ่งจุดนี้เราจำเป็นต้องใช้ secret ของเราที่เราได้ set ไปแล้วใน config ดังนั้นเราก็เลยจึงเรียกได้เลย เช่นเดียวกับตอนใส่ connection string ของ database

ต่อมาเพื่อที่จะได้รูปมาเป็น format base64 เราไม่สามาถทำได้ทันทีเนื่องจาก library TOTP ไม่ได้ทำ method มาให้ เราจึงต้องมาแปลงจาก img ที่เป็น type Image.image เอาเอและซึ่งวิธีคือกานำ img เข้าไปใน function encode โดยที่จะเอา data เข้าไปเก็บใน bytes.buffer ทีนี้เราก็ค่ละนำ img ที่เป็น byte.buffer ไปแปลงเป็น base64 โดยใช้ function base64 encode ได้เลย (ที่จำเป็นต้อง concat string เข้าไปด้านหน้าก่อนเพราะจำเป็นต้องบอกก่อนว่า type ของ data ข้อมูลเป็น image นันเอง) เท่านี้เราก็ return ค่า token กับ base64 กลับไปได้เลย

Repository

func (u userRepositoryDB) CreateUser(email string, password string, secret string) (*User, error) {
	insert, err := u.db.Exec("INSERT INTO users (email, password, secret) VALUES (?, ?, ?)", email, password, secret)
	if err != nil {
		return nil, err
	}
	userId, err := insert.LastInsertId()
	var user = User{
		Id:       userId,
		Email:    email,
		Password: password,
		Secret:   secret,
	}

	return &user, nil
}

ส่วนนี้จะทำหน้าที่แค่อย่างเดียว คือการ insert data ข้อมูลเข้า database ดังนั้นจุดที่สำคัญคือการเขียน query สำหรับ insert นันเอง และเนื่องจากใน spec เราเขียนไว้ว่าจะเอา user data ไปใช้ด้วย ดังนั้นเราอย่าลืมปั่นสร้าง model user ตามที่สร้างไว้ใน repositoryRepository แล้วส่ง return กลับไปด้วย

เท่านี้การทำ serviceService สำหรับ signup หรือ register ก็เสร็จสมบูรณ์แล้ว แต่ยังขาดอีก service ที่สำคัญสำหรับการทำระบบ authentication ซึ่งมันก็คือระบบ signin

อกจากนีนเอง และเรามี surprise เซอร์ไพรส์ครับ เพราะ GDSC session ในครั้งนี้จะให้ผู้ชมได้ลองทำเองด้วย และจะยังมีของรางวัลให้สำหรับผู้ที่ implement ได้ถูกต้อง 5 คนแรกด้วย แต่ขอสงวนสิทธิ์ให้กับคนที่เข้า session onsite เท่านั้นนะครับ แน่นอนว่าเรามี documentation ให้ด้วยพร้อมทั้งสามารถถาม mentor เมนเทอร์ใน session ครั้งนี้ได้เต็มที่เลยครับ ขอให้โชคดีใน session หลังบ่ายนี้ที่จะเป็นการทำให้ project นี้น่าสนใจขึ้นไปอีก และถือเป็นการ meetup สำหรับผู้ที่สนใจการเขียน code โค้ดเพื่อพัฒนา community coder ในมหาลัยนี้ไปด้วยครับ

TOTP: https://github.com/pquerna/otp

JWT: https://github.com/golang-jwt/jwt

bcrypt: https://pkg.go.dev/golang.org/x/crypto/bcrypt