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 ที่ client เรียกมาออกมาเป็น 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)

ก่อนจะทำ 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 เพราะ golang จะมองว่า 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 ตัวก่อนซึ่งสำคัญกับการทำระบบ 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 นั้นเอง และสามารถดีงค่าอย่างอื่นออกมาได้ด้วย เช่น รูป QRcode

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

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

//in progress...