มาทำ Token-based Authentication แบบง่าย ๆ กันเถอะ

ช่วงหลัง ๆ มานี้ Token-based Authentication ถูกนำมาใช้แทน Cookie-based กันบ้างแล้ว ข้อดีคือเราเขียน code ครั้งเดียวสามารถใช้ได้กับทั้ง Web App และ Mobile App และสามารถใช้กับ Microservice ได้ด้วย

แล้ว Token คืออะไรหล่ะ ?

ถ้าจะให้อธิบายเป็นภาษาบ้าน ๆ ผมก็จะบอกว่า

Token คือสิ่งที่ใช้แทน username และ password

แต่การที่จะได้ Token มา ก็ต้องเอา username/password ไปแลกมานะ

ใช้แทนยังไง ?

เช่น ผมมี Microservice ที่เก็บข้อมูลสินค้าอย่างเดียว ไม่มี database ของ user
ดังนั้น เราไม่สามารถส่ง username/password เพื่อไปขอข้อมูลกับ Microservice ตัวนี้ได้ เพราะมันไม่รู้ว่า username/password ที่ส่งมา มีตัวตนจริง ๆ หรือเปล่า…

Token มีหลายประเภทครับ แต่ในบทความนี้ผมขอพูดถึง JSON Web Token ละกัน


JSON Web Token (JWT) คืออะไร ?

ตามตัวเลยครับ JWT คือ Token ที่เก็บ payload เป็น JSON

JWT มีส่วนประกอบ 3 ส่วน คือ header, payload, และ signature

  1. Header — บอกว่า token นี้ เป็น JWT นะ แล้วก็บอกด้วยว่าตรง signature ใช้ algorithm อะไรเพื่อ verify
  2. Payload — เป็น JSON ที่เราเก็บไว้ใน token
  3. Signature — ใช้เพื่อ verify ว่า token นี้ ถูกสร้างโดนคนที่มี secret เท่านั้น เกิดจากการนำ header กับ payload มาเข้ารหัสด้วย secret
ข้อควรระวัง!!! — payload ไม่ได้ถูกเข้ารหัสนะ ถึงไม่มี secret ก็สามารถอ่านได้ ดังนั้นห้ามเก็บข้อมูลสำคัญไว้ใน payload เช่น รหัสผ่าน

สามารถอ่านรายละเอียดเพิ่มเติมเกี่ยวกับ JWT ได้ที่ https://jwt.io/


ปัญหาหลักของ Token

ก็คือถ้า Token ถูกสร้างขึ้นมาแล้ว และถูกคนอื่นขโมยไป เขาก็จะสามารถใช้ Token นั้นเข้ามาใช้งานในระบบแทนเจ้าของ Token ได้ เพราะ Token ไม่ได้ถูกเก็บไว้ใน database จึงไม่สามารถลบ Token นี้ออกจากระบบได้…

วิธีการการแก้ปัญหานี้คือ

เราจะต้องกำหนดเวลาหมดอายุของ Token

แล้วจะกำหนดเท่าไรหล่ะ ? — 1 ปี​ ?,​ 1 เดือน ?,​ 1 อาทิตย์​ ?,​ 1 วัน ?, 1 ชม. ? หรือ 1 นาที ?

ถ้าเรากำหนดเวลาเยอะไป ก็ไม่ต่างอะไรกับไม่กำหนดเวลา เพราะกว่า Token จะหมดอายุ ก็โดนเอาไปใช้ได้เยอะแล้ว

แต่ถ้าเรากำหนดเวลาสั้นไป เช่น 1 ชม. แสดงว่า User ก็จะต้อง Login ใหม่ทุก 1 ชม. เพื่อไปขอ Token อันใหม่… แบบนี้คงไม่ดีแน่ ๆ

แสดงว่าถ้าเราต้องการลบ Token ออกจากระบบ เราจะต้องเก็บ Token ไว้ใน Database

และเมื่อเก็บ Token ไว้ใน Database ก็จะใช้กับ Microservice ไม่ได้…

มาทำความรู้จักกับ Refresh Token

Token ที่พูดถึงข้างบนนั้นเราจะเรียกมันว่า Access Token ครับ
Access Token คือ Token ที่ใช้สำหรับเรียกขอ resource ต่าง ๆ เป็น Token ที่เราจะส่งให้ Microservice เพื่อขอ resource

ส่วน Refresh Token คือ Token ที่ใช้เพื่อขอ Access Token…

งงไหมครับ ?

สรุปคือ

User/password ใช้ขอ Refresh Token
Refresh Token ใช้ขอ Access Token
Access Token ใช้ขอ resources

สรุปอีกรอบ

  1. Refresh Token ไม่มีวันหมดอายุ (หรือหมดอายุนาน ๆ) เก็บใน Database
  2. Access Token มีอายุแปปเดียว เช่น 1 นาที

เรามาลองเขียน Microservice สำหรับ Authen กันดูครับ

เนื่องจากผมจะใช้ Datastore เก็บข้อมูล เราต้องไปสร้าง Project กันก่อน
ที่ https://console.cloud.google.com

สร้าง Project ที่ Google Cloud Console

เสร็จแล้วไปที่ IAM & Admin เพื่อสร้าง Service Account สำหรับ connect ไป Datastore ครับ

IAM and Admin

แล้วก็ไปที่ Service Account > Create Service Account ใส่ชื่อ account และ เปลี่ยน Role เป็น Project > Editor นะครับ อย่าลืมกด Furnish a new private key ด้วย

สร้าง Service Account

หลังจากสร้างเสร็จแล้วเราจะได้ไฟล์ JSON มา เก็บไว้ให้ดี อย่าให้หาย และ

อย่าเอา private key ขึ้น public repo

ขั้นตอนต่อไป เราจะทำการ Generate RSA256 เพื่อใช้ในการ Sign Token

# สำหรับ Mac/Linux
$ openssl genrsa -out key.rsa 1024
$ openssl rsa -in key.rsa -pubout > key.pub

เราจะได้ไฟล์มา 2 ไฟล์ คือ

  1. key.rsa เป็น private key สำหรับ Sign Token
  2. key.pub เป็น public key สำหรับให้ Verify Token

ดังนั้นเราจะให้แค่ไฟล์ key.pub กับ Microservice เพื่อใช้ในการ Verify Token แต่จะไม่ให้ key.rsa เพราะเราไม่ต้องการให้ Microservice สร้าง Token

อย่าลืม!!! อย่าเอา private key ขึ้น public repo

เสร็จแล้วก็สร้างไฟล์ตาม Project Structure นี้เลย หรือจะดูจาก Github ก็ได้นะ
https://github.com/acoshift/jwt-authen-golang-example

Project Structure

แน่นอนว่าอย่าลืมไปเพิ่มไฟล์ private ต่าง ๆ ใน .gitignore ด้วยนะ

# .gitignore
service-account.json
key.rsa
key.pub

Model

ใน Folder model เราจะสร้าง model ทั้งหมด 5 อย่าง คือ

  • base.go
package model
import (
"cloud.google.com/go/datastore"
)
// Base type provides datastore-based model
type Base struct {
key *datastore.Key
ID int64 `datastore:"-" json:"id"`
}
// Key return datastore key or nil
func (x *Base) Key() *datastore.Key {
return x.key
}
// SetKey sets key and id to new given key
func (x *Base) SetKey(key *datastore.Key) {
x.key = key
x.ID = key.ID
}

ทำหน้าที่เป็น Model ที่จะแปลง key ของ datastore ให้เป็น ID
ดังนั้น ทุก model ที่จะเซฟลง datastore ต้องมี Base ด้วย

  • token.go
package model
import "time"
// Token model
type Token struct {
Base
Token string `json:"-"`
UserID int64 `json:"-"`
CreatedAt time.Time `json:"createdAt"`
LastAccessAt time.Time `json:"lastAccessAt"`
}
// Stamp current time to token
func (x *Token) Stamp() {
x.LastAccessAt = time.Now()
if x.CreatedAt.IsZero() {
x.CreatedAt = x.LastAccessAt
}
}

ทำหน้าที่ไว้สำหรับเก็บ Refresh Token ถ้าดูดี ๆ จะเห็นว่า เราจะไม่ส่งค่า Token กลับไปให้ User เวลา user ต้องการดู Refresh Token ของตัวเอง จะส่งไปแค่ ID, CreatedAt, และ LastAccessAt ถ้า User ต้องการลบ Refresh Token อันไหน ให้ส่งมาแค่ ID อย่างเดียว

อย่าส่ง Token ที่เคยสร้างไปแล้วใน Database ให้ User
ถ้าจะลบ ก็ส่ง ID ที่อ้างอิง token นั้นใน database มาก็พอ
json:"-" หมายถึงตอน marshal data ไปเป็น JSON ไม่เอา field นี้
  • timestamp.go
package model
import "time"
// HasTimestamp base model
type HasTimestamp struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Stamp current time to model
func (x *HasTimestamp) Stamp() {
x.UpdatedAt = time.Now()
if x.CreatedAt.IsZero() {
x.CreatedAt = x.UpdatedAt
}
}

ทำหน้าที่เหมือน Base type แต่ดูแลเฉพาะ CreatedAt และ UpdatedAt ถ้า model ไหนมี 2 fields นี้ ให้ใช้ HasTimestamp

  • password.go
package model
import (
"golang.org/x/crypto/bcrypt"
)
const bcryptCost = 13
// HasPassword base type
type HasPassword struct {
Password string `datastore:",noindex" json:"-"`
}
// SetPassword hash password then set to model
func (x *HasPassword) SetPassword(password string) error {
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return err
}
x.Password = string(hashed)
return nil
}
// ComparePassword verify password and hashed
func (x *HasPassword) ComparePassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(x.Password), []byte(password))
return err == nil
}

HasPassword ทำหน้าที่ดูแลเกี่ยวกับรหัสผ่าน จะเห็นได้ว่า เราจะไม่ส่งรหัสผ่านไปให้ User และมี function SetPassword และ ComparePassword เพื่อ hash และ compare รหัสกับ hash

  • user.go
package model
// User model
type User struct {
Base
HasPassword
HasTimestamp
Username string `json:"username"`
}

API

ในส่วนของ API จะทำหน้าที่คุยกับ Database ทั้งหมด

  • client.go
package api
import (
"context"
"time"
 "cloud.google.com/go/datastore"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
)
// Config API type
type Config struct {
ServiceAccountJSON []byte
ProjectID string
}
var (
client *datastore.Client
)
// Init api
func Init(cfg Config) error {
gconf, err := google.JWTConfigFromJSON(cfg.ServiceAccountJSON, datastore.ScopeDatastore)
if err != nil {
return err
}
ctx := context.Background()
client, err = datastore.NewClient(ctx, cfg.ProjectID, option.WithTokenSource(gconf.TokenSource(ctx)))
return err
}
func getContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 30*time.Second)
}

ส่วนของ client.go จะทำหน้าที่สร้าง object datastore.Client และมี function getContext() ให้ไฟล์อื่น ๆ ใน api เรียกใช้

เราใช้ context.WithTimeout เพื่อป้องกันไม่ให้ service เราค้างเวลาที่ไม่สามารถ connect ไป datastore ได้ เนื่องจาก context.Background() ไม่มี timeout ถ้า connect ไม่ถูกตัดและไม่ได้ response จาก datastore, service เราจะรอไปเรื่อย ๆ แล้ว request นั้นก็จะค้าง

ระวังเวลาใช้ context.Background() เพราะไม่มี timeout
  • user.go
package api
import (
"jwt-authen-golang-example/model"
 "cloud.google.com/go/datastore"
"google.golang.org/api/iterator"
)
const kindUser = "User"
// FindUser from datastore
func FindUser(username, password string) (*model.User, error) {
ctx, cancel := getContext()
defer cancel()
 var user model.User
q := datastore.
NewQuery(kindUser).
Filter("Username =", username).
Limit(1)
key, err := client.Run(ctx, q).Next(&user)
if err == iterator.Done {
// Not found
return nil, nil
}
if err != nil {
return nil, err
}
user.SetKey(key)
if !user.ComparePassword(password) {
// wrong password return like user not found
return nil, nil
}
return &user, nil
}
// SaveUser to datastore
func SaveUser(user *model.User) error {
ctx, cancel := getContext()
defer cancel()
 var err error
user.Stamp()
key := user.Key()
if key == nil {
key = datastore.IncompleteKey(kindUser, nil)
}
key, err = client.Put(ctx, key, user)
if err != nil {
return err
}
user.SetKey(key)
return nil
}

user.go เป็นตัวกลางในการคุยกับ datastore
ถ้าดูดี ๆ จะเห็นว่า SaveUser ยังใช้ไม่ได้นะครับ เพราะถ้ามี User สมัครมาใหม่ มันยังไม่ได้เช็คว่า Username มีในระบบรึยัง ถ้าจะ implement ระบบสมัครสมาชิกจะต้องเช็คทั้ง Username และความยาวของ password ด้วยนะครับ

  • token.go
package api
import (
"jwt-authen-golang-example/model"
 "cloud.google.com/go/datastore"
"google.golang.org/api/iterator"
)
const kindToken = "Token"
// CreateToken save new token to database
func CreateToken(token string, userID int64) error {
ctx, cancel := getContext()
defer cancel()
 var err error
tk := &model.Token{
Token: token,
UserID: userID,
}
tk.Stamp()
key := datastore.IncompleteKey(kindToken, nil)
key, err = client.Put(ctx, key, tk)
if err != nil {
return err
}
tk.SetKey(key)
return nil
}
func getToken(token string) (*model.Token, error) {
ctx, cancel := getContext()
defer cancel()
 var tk model.Token
var err error
q := datastore.
NewQuery(kindToken).
Filter("Token =", token).
Limit(1)
key, err := client.Run(ctx, q).Next(&tk)
if err == iterator.Done {
// token not found
return nil, nil
}
if err != nil {
return nil, err
}
tk.SetKey(key)
return &tk, nil
}
// DeleteToken delete a token from datastore
func DeleteToken(token string) error {
tk, err := getToken(token)
if err != nil {
return err
}
ctx, cancel := getContext()
defer cancel()
return client.Delete(ctx, tk.Key())
}
// ValidateToken validate and update token last access timestamp
func ValidateToken(token string, userID int64) (bool, error) {
tk, err := getToken(token)
if err != nil {
return false, err
}
if tk == nil || tk.UserID != userID {
return false, nil
}
tk.Stamp()
go func(tk model.Token) {
ctx, cancel := getContext()
defer cancel()
client.Put(ctx, tk.Key(), &tk)
}(*tk)
return true, nil
}

เช่นกัน token.go ก็เป็นตัวกลางในการคุยกับ datastore สิ่งที่ยังขาดอีกคือคำสั่ง list token ตาม user id เพื่อให้ user สามารถเลือกลบ token ได้ (หรือจะแยกไปไว้ใน microservice อันอื่นก็ได้นะ)

ระวังเรื่องการใช้ go ด้วยนะครับ จริง ๆ ในตัวอย่างอาจจะทำแค่

go func() {
ctx, cancel := getContext()
defer cancel()
client.Put(ctx, tk.Key(), tk)
}()

ก็ได้ เพราะตัวแปร tk ไม่มีที่อื่นใช้แล้ว

Service

มาดูส่วนสำคัญกันบ้างครับ

  • auth.go

ลองดูทีละส่วนนะครับ

package service
import (
"crypto/rsa"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
 jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
 "io/ioutil"
"jwt-authen-golang-example/api"
"jwt-authen-golang-example/model"
)

ส่วนนี้ไม่มีอะไรครับ แค่ import เฉย ๆ

// Auth service
func Auth(g *echo.Group) {
g.Post("", authTokenHandler)
g.Post("/register", authRegisterHandler)
}

ส่วนนี้ทำหน้าที่ register service เข้าไปใน router ครับ

var (
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
)
func init() {
key, err := ioutil.ReadFile("key.rsa")
if err != nil {
log.Fatal(err)
}
privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
log.Fatal(err)
}
 key, err = ioutil.ReadFile("key.pub")
if err != nil {
log.Fatal(err)
}
publicKey, err = jwt.ParseRSAPublicKeyFromPEM(key)
if err != nil {
log.Fatal(err)
}
}

ส่วนนี้ทำหน้าที่อ่าน private key และ public key ครับ จริง ๆ วิธีนี้ไม่ควรทำนะครับ ควรจะทำแบบ API มากกว่าคือ ทำเป็น Config แล้วส่งค่ามาจาก main.go

มาดูส่วนของ Token กันบ้างครับ

func getTokenFromHeader(c echo.Context) string {
token := c.Request().Header().Get(echo.HeaderAuthorization)
token = strings.TrimSpace(token)
if token == "" || len(token) < 8 || strings.ToLower(token[:7]) != "bearer " {
return ""
}
token = strings.TrimSpace(token[7:])
return token
}

จริง ๆ คำสั่งนี้ใน service ที่เราทำไม่ได้ใช้นะครับ คำสั่งนี้เป็นคำสั่ง สำหรับ service อื่น ดึง token ออกมาจาก header หน้าตาของ header จะเป็น
Authorization: Bearer ACCESS_TOKEN
ดังนั้ง เราจึงต้องเช็คด้วยว่า Token เป็นประเภท Bearer และตัดออก

type tokenClaim struct {
ID int64 `json:"id"`
Type tokenType `json:"type"`
jwt.StandardClaims
}
type tokenType int
// Token Type
const (
_ = iota
TokenTypeRefreshToken tokenType = iota
TokenTypeAccessToken
)
const accessTokenDuration = time.Duration(time.Minute * 5)
// Token Errors
var (
ErrInvalidToken = errors.New("token: invalid token")
)

ส่วนนี้เป็นการประกาศว่า Claim ของ Token ของเราหน้าตาเป็นยังไง เราสามารถเพิ่ม field อื่น ๆ เข้าไปได้ เช่น

isAdmin bool `json:"isAdmin"

แต่อย่าใส่เยอะมากนะครับ เพราะ Token เราจะบวม
จะเห็นว่า เราสร้าง TokenType ไว้ 2 ประเภทนะครับ คือ RefreshToken และ AccessToken
ส่วนเวลาหมดอายุของ AccessToken คือ 5 นาที

func validateToken(token string) (*tokenClaim, error) {
tok, err := jwt.ParseWithClaims(token, &tokenClaim{}, func(token *jwt.Token) (interface{}, error) {
// Check is token use correct signing method
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// return secret for this signing method
return publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := tok.Claims.(*tokenClaim); ok && tok.Valid {
return claims, nil
}
return nil, ErrInvalidToken
}

ส่วนนี้เป็นส่วนที่สำคัญมาก ๆ อีกส่วนครับ คือส่วนที่เราจะเอา Claims ออกมาจาก Token ดูดี ๆ จะเห็นว่า เราจะเช็ค Signed method ด้วย ถ้าไม่ตรงกับวิธีที่เราใช้ Sign แสดงว่าคนอื่นเป็นคน Sign เลยจึง Reject token นั้นทิ้ง

ระวังเรื่อง Signed Method ด้วยนะ อ่านเพิ่มเติมที่
https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
func verifyAccessTokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := getTokenFromHeader(c)
claims, err := validateToken(token)
if err != nil || claims.Type != TokenTypeAccessToken {
return c.String(http.StatusUnauthorized, "Unauthorized")
}
// set user id to context
c.Set("userID", claims.ID)
return next(c)
}
}

ส่วนนี้เป็นส่วนที่ไม่ได้ใช้ใน service นี้นะครับ เป็น middlware ใช้เพื่อเช็คว่า Access Token ที่ส่งมากับ Header นั้นถูกไหม ถ้าถูกก็จะเอา UserID ออกมาใส่ใน context ถ้าไม่ถูกก็จะตอบกลับไปว่า Unauthorized

func generateToken(id int64, expiresIn time.Duration, tokenType tokenType) (string, error) {
expiresAt := int64(0) // not expires
now := time.Now()
if expiresIn > 0 {
expiresAt = now.Add(expiresIn).Unix()
}
 token := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaim{
id,
tokenType,
jwt.StandardClaims{
IssuedAt: now.Unix(),
ExpiresAt: expiresAt,
},
})
return token.SignedString(privateKey)
}

ส่วนนี้เป็นส่วนสร้าง Token ครับ สร้างได้ทั้ง RefreshToken และ AccessToken เลย

func generateRefreshToken(id int64) (string, error) {
token, err := generateToken(id, 0, TokenTypeRefreshToken)
if err != nil {
return "", err
}
if err = api.CreateToken(token, id); err != nil {
return "", err
}
return token, nil
}
func generateAccessToken(id int64, expiresIn time.Duration) (string, error) {
token, err := generateToken(id, expiresIn, TokenTypeAccessToken)
if err != nil {
return "", err
}
return token, nil
}

หลังจากสร้าง RefreshToken แล้ว อย่าลืมเก็บลง database ด้วยนะ

type authRequest struct {
GrantType grantType `json:"grant_type"`
Username string `json:"username"`
Password string `json:"password"`
RefreshToken string `json:"refresh_token"`
}
type authResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"` // unit: seconds
RefreshToken string `json:"refresh_token,omitempty"`
UID int64 `json:"uid"`
}
type grantType string
// Grant Type
const (
grantTypePassword = "password"
grantTypeRefreshToken = "refresh_token"
)
func authTokenHandler(c echo.Context) error {
var body authRequest
if err := c.Bind(&body); err != nil {
return c.String(http.StatusBadRequest, "Bad Request")
}
if body.GrantType == grantTypePassword {
// handle password grant type => return refresh token
user, err := api.FindUser(body.Username, body.Password)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
if user == nil {
// user or password wrong = unauthorized
return c.String(http.StatusUnauthorized, "Unauthorized")
}
refreshToken, err := generateRefreshToken(user.ID)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
accessToken, err := generateAccessToken(user.ID, accessTokenDuration)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
return c.JSON(http.StatusOK, authResponse{
accessToken,
"bearer",
int64(accessTokenDuration.Seconds()),
refreshToken,
user.ID,
})
}
if body.GrantType == grantTypeRefreshToken {
// handle refresh token grant type => return access token
  // get user id from context
claims, err := validateToken(body.RefreshToken)
if err != nil {
return c.String(http.StatusUnauthorized, "Unauthorized")
}
// verify refresh token in database
if ok, err := api.ValidateToken(body.RefreshToken, claims.ID); !ok {
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
return c.String(http.StatusUnauthorized, "Unauthorized")
}
  accessToken, err := generateAccessToken(claims.ID, accessTokenDuration)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
return c.JSON(http.StatusOK, authResponse{
accessToken,
"bearer",
int64(accessTokenDuration.Seconds()),
"",
claims.ID,
})
}
 return c.String(http.StatusUnauthorized, "Unauthorized")
}

ส่วนนี้เป็นส่วนที่จัดการเกี่ยวกับการ issue Token ให้กับ User โดยที่จะแบ่งออกจาก type ของ request

json:",omitempty" ใช้เพื่อให้ตอน Marshal JSON ไม่ใส่ field นี้ ถ้า field นี้ empty

และส่วนสมัครสมาชิก เอาไว้ทดสอบ authen service ของเรา

type registerRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func authRegisterHandler(c echo.Context) error {
var body registerRequest
var err error
if err = c.Bind(&body); err != nil {
return c.String(http.StatusBadRequest, "Bad Request")
}
var user model.User
user.Username = body.Username
user.SetPassword(body.Password)
 err = api.SaveUser(&user)
if err != nil {
log.Println(err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
 return c.String(http.StatusCreated, "Created")
}

ไฟล์หลัก main.go

package main
import (
"net/http"
"time"
 "jwt-authen-golang-example/api"
"jwt-authen-golang-example/service"
"log"
 "io/ioutil"
 "github.com/labstack/echo"
"github.com/labstack/echo/engine"
"github.com/labstack/echo/engine/standard"
"github.com/labstack/echo/middleware"
)
const projectID = "jwt-authen-example"
func main() {
serviceAccount, err := ioutil.ReadFile("service-account.json")
if err != nil {
log.Fatal(err)
}
err = api.Init(api.Config{
ServiceAccountJSON: serviceAccount,
ProjectID: projectID,
})
if err != nil {
log.Fatal(err)
}
 e := echo.New()
e.Use(
middleware.Recover(),
middleware.Secure(),
middleware.Logger(),
middleware.Gzip(),
middleware.BodyLimit("2M"),
middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{
"http://localhost:8080",
},
AllowHeaders: []string{
echo.HeaderOrigin,
echo.HeaderContentLength,
echo.HeaderAcceptEncoding,
echo.HeaderContentType,
echo.HeaderAuthorization,
},
AllowMethods: []string{
echo.GET,
echo.POST,
},
MaxAge: 3600,
}),
)
 // Health check
e.Get("/_ah/health", func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
})
 // Register services
service.Auth(e.Group("/auth"))
 e.Run(standard.WithConfig(engine.Config{
Address: ":9000",
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}))
}

ไฟล์นี้ไม่มีอะไรครับ แค่สร้าง router ขึ้นมา ถ้าจะ deploy อย่าลืมปรับพวก allow origin ด้วยนะครับ


เหนื่อยมากครับ ลองมายิง request เล่นกันดีกว่า

ก่อนอื่นเลย เราก็ต้องสมัครกันก่อน

สมัคร User เข้าสู่ระบบ

แล้วก็ลองส่งไปขอ Refresh Token กัน

ส่งไปขอ Request Token

จะเห็นว่า เราจะได้ทั้ง Refresh Token และ Access Token มาด้วย หรือจะส่งมาแค่ Refresh Token อย่างเดียวก่อนก็ได้ แล้วค่อยให้ User ส่งมาอีกรอบขอ Access Token
เช่น ระบบที่ Access Token มี Scope อย่างชัดเจน ว่า Access Token นี้ใช้กับ service ไหนได้บ้าง ฯลฯ

คราวนี้เมื่อ Access Token หมดอายุ เราก็ต้องส่ง Refresh Token ไปขอ Access Token อันใหม่

ส่งไปขอ Access Token

เห็นไหมครับว่า วิธีทำระบบ Token-Based Authentication นั้นไม่ยากเลย
(o_O!!!)



อ้อ แล้วก็อย่าลืมระวังเรื่อง XSS กันด้วยนะครับ xD


Edit: 2016/11/11 Google Datastore API มี breaking change นะครับ แก้ code ในบทความแล้วครับ

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.