Published on

What is Authentication and Authorization?

Authors

What is Authentication and Authorization?

ในการพัฒนา API Service นั้น เราจำเป็นต้องมีการป้องกันการเข้าถึงทรัพยากรในเซิฟเวอร์ของเราด้วย ซึ่งจะมีการทำอยู่ 2 อย่าง คือ

  • Authentication คือ การยืนยันว่าตัวตนว่าเป็นใคร
  • Authorization คือ การตรวจสอบสิทธิ์การเข้าถึงทรัพยากรในเซิฟเวอร์ว่าสามารถเข้าใช้ได้ หรือไม่

Authentication

ในการยืนยันตัวตนว่าเป็นใครนั้นจะมีหลักการทำ คือ

  • Sign-up API คือ การลงทะเบียนผู้ใช้งาน หลักๆ ก็จะมี username กับ password ส่งมาให้ Autentication Service เพื่อสร้างผู้ใช้งานใหม่ขึ้นมา
  • Sign-in API คือ การให้ผู้ใช้งานยืนยันตัวตนก่อนที่จะเข้ามาใช้งาน โดยจะต้องส่ง username และ password มาตรวจสอบ ถ้าถูกต้องจะส่งอะไรบ้างอย่างไปให้ client เพื่อใช้ค่านี้แทน เพื่อไม่ต้อง login ทุกครั้ง โดยจะนิยมใช้ JSON Web Token (JWTs) โดยจะสร้าง token ขึ้นมา 2 ตัว คือ
    • Refresh Token: เป็น JWTs ที่มีอายุยาวๆ ใช้ในการสร้าง Access Token ใหม่
    • Access Token: เป็น JWTs ที่มีอายุสั้นๆ เช่น 5 นาที ใช้ส่งไปกับทุกการเรียกใช้ API Service อื่นๆ เพื่อเป็นตัวแทนว่าคือใคร โดย client จะต้องเก็บค่า token ทั้งสองนี้เอาไว้ และครั้งถัดไป client จะต้องส่ง access token ไปด้วยทุกครั้ง
  • Sign-out API คือ การส่ง Refresh Token มาให้ Authentication Service เพื่อลบออกจากฐานข้อมูล ก็จะทำให้ไม่สามารถใช้งาน Token นี้ได้อีกต่อไป
  • Refresh API คือ การส่ง Refresh Token มาให้ Authentication Service สร้าง Access Token ใหม่
  • Authentication Middleware เป็น middleware ที่เอาวางไว้ในทุก routes ที่ต้องการยืนยันตัวตนก่อนเข้าใช้งาน โดยตัวมันจะทำหน้าที่ ตรวจสอบ access token ที่ส่งเข้ามา ว่าถูกต้องหรือไม่
    • ถ้าถูกต้องจะดึงเอาข้อมูลผู้ใช้งานส่งต่อไปให้ handler ของ route นั้นๆ ต่อไป
    • ถ้าไม่ถูกต้องจะไม่อนุญาตให้เข้าใช้งาน route นั้นๆ
  • Profile API คือ การส่ง Access Token มาให้ Authentication Service เพื่อดึงเอาข้อมูลผู้ใช้ตอบกลับไป หรือใช้ในการแก้ไขข้อมูลส่วนตัวโดยผู้ใช้งานเอง

Sign-up API

ตัวอย่างโค้ดในบทความนี้จะใช้ Node.js (TypeScript)

การทำ sign-up api นั้น จะส่ง username และ password เข้ามา เพื่อบันทึกเก็บไว้ในฐานข้อมูล โดยเราจะต้องทำการ hash password ก่อนเก็บด้วยทุกครั้ง

signup = async (req: Request, res: Response) => {
  const { email, password } = req.body

  const hash = await passUtil.hash(password)

  await this.db.user.create({
    data: {
      email,
      password: hash,
    },
  })

  res.sendStatus(201)
}

Sign-in API

จะส่ง username และ password เข้ามา เพื่อทำการตรวจสอบ ถ้าถูกต้องจะได้ข้อมูลของผู้ใช้งาน กับ Refresh Token และ Accress Token ตอบกลับไป

  • เริ่มจากเอา username ไปค้นหาจากฐานข้อมูล
signin = async (req: Request, res: Response) => {
  const { email, password } = req.body

  // 1. find user from email
  const user = await this.db.user.findFirst({ where: { email } })
  if (!user) {
    res.sendStatus(404)
    return
  }
}
  • เอา password ไปเทียบกับที่เข้ารหัสเก็บไว้
signin = async (req: Request, res: Response) => {
  const { email, password } = req.body

  // 1. find user from email
  // 2. verify password
  const ok = await passUtil.verify(user.password, password)
  if (!ok) {
    res.sendStatus(401)
    return
  }
}
  • สร้าง refresh token โดยมี payload เป็น tokenId โดยจะเก็บเอาไว้ในฐานข้อมูลพร้อมวันหมดอายุ เพื่อใช้สร้าง access token และการ sign out
signin = async (req: Request, res: Response) => {
  const { email, password } = req.body

  // 1. find user from email
  // 2. verify password
  // 3. create refresh token and save to db
  const tokenId = generateTokenId()
  const refresh = generateRefreshToken({ sub: tokenId })
  await this.db.token.create({
    data: {
      userId: user.id,
      token: tokenId,
      expiredAt: <Date>refresh.expiredAt,
    },
  })
}
  • สร้าง access token โดยในตัวอย่างนี้จะแนบข้อมูล user (id, role) ไปใน payload
signin = async (req: Request, res: Response) => {
  const { email, password } = req.body

  // 1. find user from email
  // 2. verify password
  // 3. create refresh token and save to db
  // 4. create access token with user info
  const access = generateAcceesToken({
    sub: user.id,
    role: user.role,
  })
}
  • เสร็จแล้วส่งข้อมูล userInfo, refresh token, access token ตอบกลับไป
signin = async (req: Request, res: Response) => {
  const { email, password } = req.body

  // 1. find user from email
  // 2. verify password
  // 3. create refresh token and save to db
  // 4. create access token with user info
  // 5. send AuthResponse
  const authResp: AuthResponse = {
    user: {
      id: user.id,
      email: user.email,
      role: user.role,
    },
    refresh: {
      token: refresh.token,
      expiredAt: <Date>refresh.expiredAt,
    },
    access: {
      token: access.token,
      expiredAt: <Date>access.expiredAt,
    },
  }
  res.json(authResp)
}

Sign-out API

จะส่ง refresh token เข้ามา เพื่อทำการลบออกจากฐานข้อมูล ซึ่งจะทำให้ไม่สามารถใช้ refresh token นี้ในการออก access token ใหม่ได้

signout = async (req: Request, res: Response) => {
  const { refreshToken } = req.body

  const decode = decodeToken(refreshToken)
  if (decode === null) {
    res.sendStatus(404)
    return
  }

  await this.db.token.delete({
    where: {
      token: <string>decode.sub,
    },
  })

  res.sendStatus(204)
}

Resfresh API

เป็นการส่ง refresh token มาตรวจสอบ ถ้ายังมีในฐานข้อมูล และยังไม่หมดอายุ ก็จะทำการออก access token ตัวใหม่ ตอบกลับไป

refresh = async (req: Request, res: Response) => {
  const { refreshToken } = req.body
  try {
    const decode = verifyRefreshToken(refreshToken)
    const token = await this.db.token.findFirst({
      where: {
        token: <string>decode.sub,
        expiredAt: { gt: new Date() },
      },
      include: {
        user: true,
      },
    })

    if (!token) {
      return res.sendStatus(401)
    }

    const access = generateAcceesToken({ sub: token.user?.id, role: token.user?.role })

    res.json(access)
  } catch (error) {
    res.sendStatus(401)
  }
}

Middleware Authentication

สำหรับการตรวจสอบว่าผู้ใช้งานได้ทำการ login มาแล้วรึยังนั้น เราจะต้องเขียนตรวจสอบในทุกๆ router controller ที่ทำการป้องกันเอาไว้ ดังนั้นเราสามารถนำโค้ดส่วนนี้ออกมาเป็น middleware แล้วเอาไปวางไปก่อนให้ route controller นั่นๆ ทำงานแทนได้ ซึ่งถ้าตรวจสอบผ่านจะทำการแนบ user object ไปกับ req object เพื่อให้ controller นำไปใช้งานต่อได้

export const authentication = (req: Request, res: Response, next: NextFunction) => {
  const authorization = req.header('Authorization')?.split(' ')
  // 'Access denied. No token provided.'
  if (!authorization) return res.sendStatus(401)
  // 'Access denied. Invalid token.'
  if (authorization[0] !== 'Bearer') return res.sendStatus(401)

  try {
    const decoded = verifyAccessToken(authorization[1])
    console.log(decoded)

    // eslint-disable-next-line prettier/prettier
    const { sub: id, role } = decoded as {
      sub: string
      role: string
    }
    req.user = { id, role }
    next()
  } catch (ex) {
    // 'Access denied. Invalid token.'
    res.sendStatus(401)
  }
}

Profile API

เป็นการส่ง access token เข้ามา เพื่อดึงเอาข้อมูลผู้ใช้งานกลับไป โดยจะใช้ร่วมกับ authentication middleware เพื่อยืนยันตัวตนก่อน

// auth api router
router.get('/profile', authentication, ctrl.profile)

// auth api controller
profile = async (req: Request, res: Response) => {
  const { id } = req.user
  const user = await this.db.user.findFirst({ where: { id: +id } })

  res.json({
    id: user?.id,
    email: user?.email,
    role: user?.role,
  })
}

Authorization

การตรวจสอบสิทธิ์การเข้าถึงทรัพยากรในเซิฟเวอร์ สามารถทำได้หลายวิธี โดยในบทความนี้ใช้วิธีตรวจสอบสิทธิแบบ Role base หรือก็คือการกำหนดว่าในแต่ละ routes นั้น อนุญาตให้ผู้ใช้งานที่มี role ตามที่กำหนดเท่านั้น ถึงจะเข้าใช้งานได้

ซึ่งทำได้โดยการเพิ่มโค้ดการตรวจสอบ role เข้าไปในทุกๆ route handler นั้นเอง เช่น การค้นหา users ต้องเป็นผู้ใช้งานสิทธิ admin เท่านั้น

list = async (req: Request, res: Response) => {
  if (req.user.role !== 'admin') {
    return res.sendStatus(403)
  }

  const users = await this.db.user.findMany()

  const serailizeUsers = users.map((user) => {
    return {
      id: user.id,
      email: user.email,
      role: user.role,
    }
  })

  res.json(serailizeUsers)
}

แต่เราสามารถเอาโค้ดส่วนนี้ออกมาเขียนไว้ใน middleware แทนได้ ดังนี้

export const authorization = (req: Request, res: Response, next: NextFunction) => {
  if (req.user.role !== 'admin') {
    return res.sendStatus(403)
  }
  next()
}

และเอา middleware ไปเรียกใช้หลังการทำ authentication

export const registerRoutes = ({ app, baseUrl, db }: RouterConfig) => {
  const ctrl = new UserController(db)

  const router = express.Router()

  router.post('/', authentication, authorization, ctrl.create)
  router.get('/', authentication, authorization, ctrl.list)

  app.use(baseUrl + '/users', router)
}

เพียงเท่านี้เราก็จะได้การทำ Authorization แบบ Role Base แล้ว

โค้ดทั้งหมดดูได้ที่นี่