- Published on
API Service with Go: Access Token & Refresh Token
- Authors
- Name
- Somprasong Damyos
- @somprasongd
Access Token & Refresh Token
จากบทความเรื่องการ Authentication with JWT ตัว token ที่สร้างขึ้นมาสำหรับใช้ยืนยันตัวตนนั้นจะเห็นว่ามันสามารถใช้งานได้ตลอดกาล ซึ่งมันไม่ปลอดภัยถ้าเกิด token นั้นหลุดออกไป ทำให้ใครก็ตามที่มี token สามารถเข้าใช้งานระบบเราได้ ดังนั้นต้องแก้ใขโดยทำ 2 สิ่ง คือ
- ทำให้ token มีเวลาหมดอายุ
- ต้อง revoke token นั้นออกไปได้
ซึ่งสามารถทำได้โดยการใช้ Access Token และ Refresh Token
โดยจะเอาโค้ดจากที่นี่ มาแก้ไข
Access Token
Access Token คือ token ที่แนบไปกับทุก request เพื่อใช้ยืนยันตัวตน และต้องมีอายุสั้นๆ เช่น 5 - 60 นาที ดังนั้นเราจะเอา jwt เดิมที่สร้างขึ้นมาในขั้นตอนการ Login มาเพิ่มเวลาหมดอายุเข้าไป มีขั้นตอน ดังนี้
- แก้ไขฟังก์ชัน
GenerateToken
ให้กำหนดเวลาหมดอายุได้
func GenerateToken(uid string, payload map[string]any, secretKey string, expires time.Duration) (string, time.Time, error) {
claims := jwt.MapClaims{}
claims["sub"] = uid
claims["iat"] = jwt.NewNumericDate(time.Now())
expiresAt := time.Now().Add(expires)
claims["exp"] = jwt.NewNumericDate(expiresAt)
for k, v := range payload {
claims[k] = v
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
//encoded string
encodedToken, err := token.SignedString([]byte(secretKey))
return encodedToken, expiresAt, err
}
- ในขั้นตอนการ Login ให้กำหนดเวลาหมดอายุเข้าไป โดย access token นั่นจะต้องมีอายุสั้นๆ เช่น 5 นาที
func (s authService) Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error) {
// validate form
// ค้นหาจาก email
// ตรวจสอบรหัสผ่าน ตรงกันหรือไม่
tokenId, _ := uuid.NewV4()
payload := map[string]any{
"user_id": user.ID.String(),
"email": user.Email,
"role": user.Role.String(),
}
// สร้าง access token
accessToken, expiresAt, err := util.GenerateToken(tokenId.String(), payload, s.config.Token.AccessSecretKey, s.config.Token.AccessExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateToken
}
// ตอบกลับไปพร้อมข้อมูล user
}
- ส่งเวลาหมดอายุกลับไปให้ client ด้วย
type AuthResponse struct {
User UserInfo `json:"user"`
- Token string `json:"token"`
+ AccessToken string `json:"access_token"`
+ AccessTokenExpiresAt time.Time `json:"access_token_expires"`
}
func (s authService) Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error) {
// validate form
// ค้นหาจาก email
// ตรวจสอบรหัสผ่าน ตรงกันหรือไม่
// สร้าง jwt token
// ตอบกลับไปพร้อมข้อมูล user
serialized := dto.AuthResponse{
User: dto.UserInfo{
ID: user.ID.String(),
Email: user.Email,
Role: user.Role.String(),
},
AccessToken: accessToken,
AccessTokenExpiresAt: expiresAt,
}
return &serialized, nil
}
- ทดสอบ Login
curl -XPOST \
-H "Content-type: application/json" \
-d '{
"email": "user@mail.com",
"password": "user"
}' \
'http://localhost:8080/api/v1/auth/login'
// Output
{
"status": 200,
"data": {
"auth": {
"user": {
"id": "6909a198-831d-4eb3-911c-5275f013fe97",
"email": "user@mail.com",
"role": "user"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAbWFpbC5jb20iLCJleHAiOjE2NTk2ODI1NDEsImlhdCI6MTY1OTY4MjI0MSwicm9sZSI6InVzZXIiLCJzdWIiOiI5YjMzYmMyMS1lYzY1LTQyZjgtOGIyZS1iZmU2NzlkZjJhODYiLCJ1c2VyX2lkIjoiNjkwOWExOTgtODMxZC00ZWIzLTkxMWMtNTI3NWYwMTNmZTk3In0.7LDxCSfB9j0KwFFNJkpsDSLAGEfsSZzxEqL39jvZCnA",
"access_token_expires": "2022-08-05T13:55:41.952302+07:00"
}
},
"requestId": "d7f4c6b3-0d4a-4b26-98ec-6d86e2c2a225"
}
Refresh Token
เมื่อ Access Token หมดอายุ เราจะไม่สามารถใช้งานต่อได้ ถ้าต้องไป Login ใหม่ทุกครั้งที่หมดอายุคงไม่ดีแน่ จึงเลยมีการสร้าง Refresh Token ขึ้นมา เพื่อใช้ในการสร้าง Access Token ใหม่แทนการ Login ดังนั้นจะต้องมีการเก็บ token id เอาไว้อ้างอิงว่าเป็นผู้ใช้งานคนไหนด้วย
การเก็บ tokenId
การเก็บ tokenId นั้นเราจะเก็บลงฐานข้อมูลเลยก็ได้ โดยทำการเชื่อม tokenId
เข้ากับ userId
เอาไว้ เพื่อใช้ในการดึงข้อมูลผู้ใช้งานก็ได้แบบนี้
CREATE TABLE tokens (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
tokenId uuid NOT NULL,
userId uuid NOT NULL,
created_at timestamptz NOT NULL default current_timestamp,
expires_at timestamptz NOT NULL,
CONSTRAINT tokens_pkey PRIMARY KEY (id),
CONSTRAINT tokens_userId_fkey FOREIGN KEY (userId) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE UNIQUE INDEX tokens_token_key ON tokens(tokenId);
แต่การติดต่อกับฐานข้อมูลบ่อยๆ มันช้า ในบทความนี้เลยจะเลือกบันทึกไว้ใน Redis แทน เพื่อต้องเข้าถึงข้อมูลบ่อยๆ และสามารถกำหนดเวลาหมดอายุได้ด้วย ซึ่งข้อมูลนั้นก็จะถูกลบออกไปเอง
สร้าง Refresh Token
- เริ่มจากสร้าง TokenRepository เอาไว้สำหรับ บันทึก อ่าน และลบข้อมูล token ใน Redis
type TokenRepository interface {
SetToken(tokenId string, data map[string]any, duration time.Duration) error
GetToken(tokenId string) (string, error)
DeleteToken(tokenId string) (int64, error)
}
package repository
import (
"fmt"
"goapi/pkg/module/auth/core/ports"
"goapi/pkg/util/cache"
"time"
)
const (
prefix = "TOKEN::"// กำหนด prefix ของ key สำหรับ token
)
type tokenRepositiry struct {
cache *cache.RedisClient
}
func NewTokenRepository(cache *cache.RedisClient) ports.TokenRepository {
return &tokenRepositiry{
cache: cache,
}
}
func (r tokenRepositiry) SetToken(tokenId string, data map[string]any, duration time.Duration) error {
return r.cache.Set(fmt.Sprintf("%s%s", prefix, tokenId), data, duration)
}
func (r tokenRepositiry) GetToken(tokenId string) (string, error) {
return r.cache.Get(fmt.Sprintf("%s%s", prefix, tokenId))
}
func (r tokenRepositiry) DeleteToken(tokenId string) (int64, error) {
return r.cache.Delete(fmt.Sprintf("%s%s", prefix, tokenId))
}
- แก้ไข Login Service ให้เพิ่มการสร้าง Refresh Token และบันทึกเก็บไว้ใน Redis ไว้ก่อนการสร้าง Access Token ซึ่ง Refresh Token จะมีแค่ข้อมูล
tokenId
เท่านั้น และให้มีอายุ 30 วัน
func (s authService) Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error) {
// validate form
// ค้นหาจาก email
// ตรวจสอบรหัสผ่าน ตรงกันหรือไม่
tokenId, _ := uuid.NewV4()
payload := map[string]any{
"user_id": user.ID.String(),
"email": user.Email,
"role": user.Role.String(),
}
// สร้าง refresh token
refreshToken, refreshExpiresAt, err := util.GenerateToken(tokenId.String(), nil, s.config.Token.RefreshSecretKey, s.config.Token.RefreshExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateRefreshToken
}
// บันทึก user ลง redis
err = s.tokenRepo.SetToken(tokenId.String(), payload, s.config.Token.RefreshExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateRefreshToken
}
// สร้าง access token
// ตอบกลับไปพร้อมข้อมูล user
}
- และแก้ให้ส่งค่า Refresh Token กลับไปด้วย
func (s authService) Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error) {
// validate form
// ค้นหาจาก email
// ตรวจสอบรหัสผ่าน ตรงกันหรือไม่
// สร้าง refresh token
// บันทึก user ลง redis
// สร้าง access token
// ตอบกลับไปพร้อมข้อมูล user
serialized := dto.AuthResponse{
User: &dto.UserInfo{
ID: user.ID.String(),
Email: user.Email,
Role: user.Role.String(),
},
AccessToken: accessToken,
AccessTokenExpiresAt: expiresAt,
+ RefreshToken: refreshToken,
+ RefreshTokenExpiresAt: refreshExpiresAt,
}
return &serialized, nil
}
- ทดสอบ Login
curl -XPOST \
-H "Content-type: application/json" \
-d '{
"email": "user@mail.com",
"password": "user"
}' \
'http://localhost:8080/api/v1/auth/login'
// Output
{
"status": 200,
"data": {
"auth": {
"user": {
"id": "a5cebe10-74d9-42e0-b4b6-236c3805f6ba",
"email": "user@mail.com",
"role": "user"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAbWFpbC5jb20iLCJleHAiOjE2NTk2OTI5NjAsImlhdCI6MTY1OTY5MjY2MCwicm9sZSI6InVzZXIiLCJzdWIiOiJkM2EyMDYxZC1mMDMyLTQ5YTEtYTBiNS1kMDZlNTU0Y2YzYjQiLCJ1c2VyX2lkIjoiYTVjZWJlMTAtNzRkOS00MmUwLWI0YjYtMjM2YzM4MDVmNmJhIn0.FwAVSlIKLdAkXmt2Ty6RsfxnKKb5Pb69_TpVdc2kCk4",
"access_token_expires": "2022-08-05T16:49:20.7117156+07:00",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIyODQ2NjAsImlhdCI6MTY1OTY5MjY2MCwic3ViIjoiZDNhMjA2MWQtZjAzMi00OWExLWEwYjUtZDA2ZTU1NGNmM2I0In0.nVm_N85fShKDYuijuJCrr8e-YoNHN0PXWLIMkVem_kI",
"refresh_token_expires": "2022-09-04T16:44:20.7097125+07:00"
}
},
"requestId": "24c8a26d-d321-4bbc-95f8-8b1a41014d93"
}
สร้าง API สำหรับสร้าง Access Token ใหม่
เราจะสร้าง API POST /api/v1/auth/refresh
ขึ้น ไว้สำหรับสร้าง access token ใหม่ จาก refresh token ที่ส่งมา
- เริ่มจากสร้าง RefreshToken Service
type AuthService interface {
Register(form dto.RegisterForm, reqId string) error
Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error)
Profile(email string, reqId string) (*dto.UserInfo, error)
UpdateProfile(email string, form dto.UpdateProfileForm, reqId string) (*dto.UserInfo, error)
+ RefreshToken(form dto.RefreshForm, reqId string) (*dto.AuthResponse, error)
}
- การทำงาน คือ จะรับ refresh token มาตรวจสอบ และนำไปดึงข้อมูลผู้ใช้งานใน redis ออกมา สร้าง access token และสุดท้ายก็ทำการ rotate refresh token นี้ด้วย เพื่อป้องกันการใช้งานซ้ำ โดยจะลบออกจาก Redis
func (s authService) RefreshToken(form dto.RefreshForm, reqId string) (*dto.AuthResponse, error) {
// validate form
err := common.ValidateDto(form)
if err != nil {
return nil, common.NewInvalidError(err.Error())
}
// ตรวจสอบ refresh token ว่ายัง valid หรือไม่
cliams, err := util.ValidateToken(form.Token, s.config.Token.RefreshSecretKey)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrInvalidRefreshToken
}
// เอา token id ไปหาใน redis
tokenId := cliams["sub"].(string)
encodedUser, err := s.tokenRepo.GetToken(tokenId)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrInvalidRefreshToken
}
// ถ้าอ่านค่าได้เป็นค่าว่าง แสดงว่าหมดอายุแล้ว
if encodedUser == "" {
return nil, ErrInvalidRefreshToken
}
// อ่านค่า user ออกมาไว้ใน payload
payload := map[string]any{}
err = json.Unmarshal([]byte(encodedUser), &payload)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrUnmarshalPayload
}
// สร้าง tokenId ใหม่
newTkId, _ := uuid.NewV4()
// สร้าง refresh token ใหม่
refreshToken, refreshExpiresAt, err := util.GenerateToken(newTkId.String(), nil, s.config.Token.RefreshSecretKey, s.config.Token.RefreshExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateRefreshToken
}
// บันทึก user ลง redis
err = s.tokenRepo.SetToken(newTkId.String(), payload, s.config.Token.RefreshExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateRefreshToken
}
// ลบ tokenId เดิม ป้องกันใช้ซ้ำ
s.tokenRepo.DeleteToken(tokenId)
// สร้าง access token ใหม่
accessToken, accessExpiresAt, err := util.GenerateToken(newTkId.String(), payload, s.config.Token.AccessSecretKey, s.config.Token.AccessExpires)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return nil, ErrGenerateAccessToken
}
// ส่ง token ใหม่กลับไป
serialized := dto.AuthResponse{
AccessToken: accessToken,
AccessTokenExpiresAt: accessExpiresAt,
RefreshToken: refreshToken,
RefreshTokenExpiresAt: refreshExpiresAt,
}
return &serialized, nil
}
- สร้าง Handler สำหรับ Refresh Token
type AuthHandler interface {
Register(common.HContext) error
Login(c common.HContext) error
Profile(c common.HContext) error
UpdateProfile(c common.HContext) error
RefreshToken(c common.HContext) error
}
// @Summary Refresh Token
// @Description Generate new access and refresh token from refresh token
// @Tags Auth
// @Accept json
// @Produce json
// @Param user body swagger.RefreshForm true "Refresh Token Data"
// @Failure 401 {object} swagdto.Error401
// @Failure 422 {object} swagdto.Error422{error=swagger.ErrLoginSampleData}
// @Failure 500 {object} swagdto.Error500
// @Success 200 {object} swagdto.Response{data=swagger.AuthSampleData}
// @Router /auth/login [post]
func (h authHandler) RefreshToken(c common.HContext) error {
// แปลง JSON เป็น struct
form := new(dto.RefreshForm)
if err := c.BodyParser(form); err != nil {
return common.ResponseError(c, common.ErrBodyParser)
}
// ส่งต่อไปให้ service ทำงาน
auth, err := h.serv.RefreshToken(*form, c.RequestId())
if err != nil {
// error จะถูกจัดการมาจาก service แล้ว
return common.ResponseError(c, err)
}
return common.ResponseOk(c, "auth", auth)
}
- สร้าง Route ใหม่
func SetupRoutes(cfg RouteConfig) {
h := handler.NewAuthHandler(cfg.AuthService)
auth := cfg.Router.Group(cfg.BaseURL + "/auth")
auth.Post("/register", util.WrapFiberHandler(h.Register))
auth.Post("/login", util.WrapFiberHandler(h.Login))
auth.Get("/profile", util.WrapFiberHandler(h.Profile))
auth.Patch("/profile", util.WrapFiberHandler(h.UpdateProfile))
+ auth.Post("/refresh", util.WrapFiberHandler(h.RefreshToken))
}
- อัพเดท public route policy
p, (admin)|(user), /api/v1/auth/profile, GET
p, admin, /api/v1/users, (GET)|(POST)
p, admin, /api/v1/users/*, (GET)|(PATCH)|(DELETE)
p2, /api/v1/auth/register, POST
p2, /api/v1/auth/login, POST
+p2, /api/v1/auth/refresh, POST
- ทดสอบ Refresh Token
curl -XPOST \
-H "Content-type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIyODY5NzQsImlhdCI6MTY1OTY5NDk3NCwic3ViIjoiYjQ0MDAxZDgtMjY0NC00YzkwLTlhZGQtMTM1ZDBlNzJkMWU4In0.pDPUk7sOxQhs2m165cBiZhhCYM_AapFhPX2hRD3ZmIU"
}' \
'http://localhost:8080/api/v1/auth/refresh'
// Output
{
"status": 200,
"data": {
"auth": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAbWFpbC5jb20iLCJleHAiOjE2NTk2OTU0NzMsImlhdCI6MTY1OTY5NTE3Mywicm9sZSI6InVzZXIiLCJzdWIiOiI4MDQxMmIyYy1lYTE5LTQ5ODgtYjYwNC1jOTBiY2I2NWUwY2QiLCJ1c2VyX2lkIjoiYTVjZWJlMTAtNzRkOS00MmUwLWI0YjYtMjM2YzM4MDVmNmJhIn0.0WAlZgBCxQVvDm-O2MLzyiGncmZ9jthqUM3I8UGnZfY",
"access_token_expires": "2022-08-05T17:31:13.4711998+07:00",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIyODcxNzMsImlhdCI6MTY1OTY5NTE3Mywic3ViIjoiODA0MTJiMmMtZWExOS00OTg4LWI2MDQtYzkwYmNiNjVlMGNkIn0.nSOwKTrhl0AwPMaHXqKPguzUUPd9PubMEHE1gZc1kds",
"refresh_token_expires": "2022-09-04T17:26:13.4693386+07:00"
}
},
"requestId": "c9709cd6-c5de-475f-b302-6cc117469981"
}
สร้าง API สำหรับสร้าง Revoke Token
เพิ่มให้สามารถทำการ Logout ออกจากระบบได้ โดยส่ง refresh token แล้วทำการลบออกจาก Redis ซึ่งจะทำให้ในขั้นตอนการ Refresh Token จะหาข้อมูลไม่เจอ ทำให้ไม่สามารถออก token ใหม่ได้
- เริ่มจากสร้าง RefreshToken Service
type AuthService interface {
Register(form dto.RegisterForm, reqId string) error
Login(form dto.LoginForm, reqId string) (*dto.AuthResponse, error)
Profile(email string, reqId string) (*dto.UserInfo, error)
UpdateProfile(email string, form dto.UpdateProfileForm, reqId string) (*dto.UserInfo, error)
RefreshToken(form dto.RefreshForm, reqId string) (*dto.AuthResponse, error)
+ RevokeToken(form dto.RefreshForm, reqId string) error
}
- การทำงาน คือ จะรับ refresh token มาและเอา tokenId ไปลบออกจาก Redis ออกมา
func (s authService) RevokeToken(form dto.RefreshForm, reqId string) error {
// validate form
err := common.ValidateDto(form)
if err != nil {
return common.NewInvalidError(err.Error())
}
// ตรวจสอบ refresh token ว่ายัง valid หรือไม่
cliams, err := util.ValidateToken(form.Token, s.config.Token.RefreshSecretKey)
if err != nil {
logger.ErrorWithReqId(err.Error(), reqId)
return ErrInvalidRefreshToken
}
// เอา token id ไปหาใน redis
tokenId := cliams["sub"].(string)
s.tokenRepo.DeleteToken(tokenId)
return nil
}
- สร้าง Handler สำหรับ Refresh Token
type AuthHandler interface {
Register(common.HContext) error
Login(c common.HContext) error
Profile(c common.HContext) error
UpdateProfile(c common.HContext) error
RefreshToken(c common.HContext) error
RevokeToken(c common.HContext) error
}
// @Summary Revoke Token
// @Description Remove token id in redis
// @Tags Auth
// @Accept json
// @Produce json
// @Param user body swagger.RefreshForm true "Refresh Token Data"
// @Failure 401 {object} swagdto.Error401
// @Failure 422 {object} swagdto.Error422{error=swagger.ErrLoginSampleData}
// @Failure 500 {object} swagdto.Error500
// @Success 204
// @Router /auth/revoke [post]
func (h authHandler) RevokeToken(c common.HContext) error {
// แปลง JSON เป็น struct
form := new(dto.RefreshForm)
if err := c.BodyParser(form); err != nil {
return common.ResponseError(c, common.ErrBodyParser)
}
// ส่งต่อไปให้ service ทำงาน
err := h.serv.RevokeToken(*form, c.RequestId())
if err != nil {
// error จะถูกจัดการมาจาก service แล้ว
return common.ResponseError(c, err)
}
return common.ResponseNoContent(c)
}
- สร้าง Route ใหม่
func SetupRoutes(cfg RouteConfig) {
h := handler.NewAuthHandler(cfg.AuthService)
auth := cfg.Router.Group(cfg.BaseURL + "/auth")
auth.Post("/register", util.WrapFiberHandler(h.Register))
auth.Post("/login", util.WrapFiberHandler(h.Login))
auth.Get("/profile", util.WrapFiberHandler(h.Profile))
auth.Patch("/profile", util.WrapFiberHandler(h.UpdateProfile))
auth.Post("/refresh", util.WrapFiberHandler(h.RefreshToken))
+ auth.Post("/revoke", util.WrapFiberHandler(h.RevokeToken))
}
- อัพเดท public route policy
p, (admin)|(user), /api/v1/auth/profile, GET
p, admin, /api/v1/users, (GET)|(POST)
p, admin, /api/v1/users/*, (GET)|(PATCH)|(DELETE)
p2, /api/v1/auth/register, POST
p2, /api/v1/auth/login, POST
p2, /api/v1/auth/refresh, POST
+p2, /api/v1/auth/revoke, POST
- ทดสอบ Revoke Token
curl -XPOST \
-H "Content-type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjIyODgxNTcsImlhdCI6MTY1OTY5NjE1Nywic3ViIjoiOWU1MmZkOTItZGMwZC00NmNjLTlhYjctYzdjYzcwYjliNDMwIn0.kifRJhUl73FjfjP8o5jqu6h6c9zk_U6ZguOPRSjGKXs"
}' \
'http://localhost:8080/api/v1/auth/revoke'
// Output
HTTP/1.1 204 No Content
สรุป
Access Token คือ token ที่แนบไปกับทุก request เพื่อใช้ยืนยันตัวตน และต้องมีอายุสั้นๆ เช่น 5 - 60 นาที
Refresh Token คือ token ที่เอาไว้ใช้สร้าง Access Token ใหม่ แทนการ Login ซึ่งมีอายุยาวๆ เช่น 7 - 30 วัน
สามารถดูโค้ดได้ที่นี่