Daily Front_Minhhk

Token,, sprint-auth-token 본문

Code개발일지

Token,, sprint-auth-token

Minhhk 2023. 1. 7. 21:21

JWT (JSON Web Token)

 

JWT 종류

JWT는 보통 다음과 같이 두 가지 종류의 토큰을 이용해 인증을 구현합니다.

  1. 액세스 토큰 (Access Token)
  2. 리프레시 토큰 (Refresh Token)

 

액세스 토큰은 보호된 정보들(유저의 이메일, 연락처, 사진 등)에 접근할 수 있는 권한부여에 사용합니다.

클라이언트가 처음 인증을 받게 될 때(로그인 시)

액세스 토큰, 리프레시 토큰 두가지를 다 받지만,

실제로 권한을 얻는 데 사용하는 토큰은 액세스 토큰입니다.

 

 

그럼 액세스 토큰만 있으면 되는 것 아닌가요?

맞습니다. 권한을 부여 받는데엔 액세스 토큰만 가지고 있으면 됩니다.

하지만 액세스 토큰을 만약 악의적인 유저가 얻어냈다면 어떻게 될까요? 이 악의적인 유저는 자신이 00유저인것 마냥 서버에 여러가지 요청을 보낼 수 있습니다.

(만약 돈과 관련된 문제라면 큰일이 날 수 있겠네요!) 그렇기 때문에 액세스 토큰에는 비교적 짧은 유효기간 을 주어 토큰을 탈취하더라도 오랫동안 사용할 수 없도록 하는것이 좋습니다.

액세스 토큰의 유효기간이 만료된다면 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다. 이때, 유저는 다시 로그인할 필요가 없습니다.

 

 

리프레시 토큰도 탈취 당한다면요?

유효기간이 긴 리프레시 토큰마저 악의적인 유저가 얻어낸다면 이는 큰 문제가 될 것입니다.

상당히 오랜 기간동안 액세스 토큰이 만료되면 이를 다시 발급 받아 유저에게 피해를 입힐 수 있기 때문이죠.

그렇기 때문에 유저의 편의보다 정보를 지키는 것이 더 중요한 웹사이트들은 리프레시 토큰을 사용하지 않는 곳이 많습니다.

 

 

 

JWT 구조

1. Header

어떤 종류의 토큰인지, 어떤 알고리즘으로 시그니처를 암호화할지 json으로 적혀있다. 이 제이슨객체를 base64방식으로 인코딩하면 헤더 완성!

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload

서버에서 활용할 수 있는 유저의 정보. 접근가능한 정보권한, 개인정보 등.
base64로 인코딩하면 페이로드 완성!

{
  "sub": "someInformation",
  "name": "phillip",
  "iat": 151623391
}

3. Signature

헤더와 페이로드를 서버의 비밀키와 헤더에서 지정한 알고리즘을 사용해 암호화한다.

HMAC SHA256 알고리즘을 사용할때 시그니쳐 생성방법

HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

 

 

 

 

 

토큰기반 인증 절차

1. 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다.

 

2. 아이디/비밀번호가 일치하는지 확인하고, 클라이언트에게 보낼 암호화된 토큰을 생성한다.

    ◦ access/refresh 토큰을 모두 생성한다.

        ▪ 토큰에 담길 정보(payload)는 유저를 식별할 정보, 권한이 부여된 카테고리(사진, 연락처, 기타 등등)이 될 수 있다.

        ▪ 두 종류의 토큰이 같은 정보를 담을 필요는 없다.

 

3. 서버가 토큰을 클라이언트에게 보내주면, 클라이언트는 토큰을 저장한다.

    ◦ 저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양하다.

 

4. 클라이언트가 HTTP 헤더(Authorization 헤더) 또는 쿠키에 토큰을 담아 보낸다. 쿠키에는 리프레시 토큰을 헤더 또는 바디에는 액세스 토큰을 담는 등 다양한 방법으로 구현할 수 있다.

    ◦ Authorization 헤더를 사용한다면 Bearer Authentication을 이용한다.

 

5. 서버는 토큰을 해독하여 “아 우리가 발급한 토큰이 맞네!” 라고 판단이 될 경우, 클라이언트의 요청을 처리한 후 응답을 보내준다

 

토큰 장점

1. 무상태성 & 확장성

서버에서 클라이언트 정보를 저장할 필요가 없이 토큰이 해독되는지만 판단하면 된다.

 

2. 안전함

암호화한 토큰을 사용하고 암호화 키를 노출할 필요가 없어 안전하다.

 

3. 어디서나 생성 가능

토큰을 확인하는 서버가 토큰을 만들지 않아도 된다.

 

4. 권한 부여에 용이

페이로드에 어떤 정보에 접근 가능한지 정할 수 있다.

 

 

토큰기반 인증의 단점

1. 용량이 크다


2. 내용물이 들어있기 때문에 랜덤한 토큰을 사용 할 때보다 용량이 크다


3. 요청을 할 때마다 토큰이 이동해서 데이터양이 증가한다

 

 

 

JWT(Json Web Token) 소개

본 포스팅은 크게 3가지 파트로 이루어집니다.

medium.com

 

 

 

 


 

sprint-auth-token

 

🍪 Server

 

server/controllers/users/login.js

const { USER_DATA } = require("../../db/data");
// JWT는 generateToken으로 생성할 수 있습니다. 먼저 tokenFunctions에 작성된 여러 메서드들의 역할을 파악하세요.
const { generateToken } = require("../helper/tokenFunctions");

module.exports = async (req, res) => {
  const { userId, password } = req.body.loginInfo;
  const { checkedKeepLogin } = req.body;
  // checkedKeepLogin이 false라면 Access Token만 보내야합니다.
  // checkedKeepLogin이 true라면 Access Token과 Refresh Token을 함께 보내야합니다.
  const userInfo = {
    ...USER_DATA.filter(
      (user) => user.userId === userId && user.password === password
    )[0],
  };

  /*
   * TODO: 로그인 로직을 구현하세요.
   *
   * userInfo에는 요청의 바디를 이용해 db에서 조회한 유저정보가 담겨있습니다. 콘솔에서 userInfo를 출력해보세요.
   * 유저의 정보가 출력된다면 해당 유저가 존재하는 것임으로 로그인 성공에 대한 응답을 전송해야합니다.
   * 만약 undefined가 출력된다면 해당하는 유저가 존재하지 않는 것임으로 로그인 실패에 대한 응답을 전송해야합니다.
   *
   * 로그인 성공 시에는 쿠키에 JWT를 담아 전송해야합니다.
   * 로그인 상태가 유지되어야 한다면 Access Token과 Refresh Token 모두 보내야합니다.
   * Access Token은 Session 쿠키로 Refresh Token은 Persistent Cookie로 보내야합니다.
   * Access Token의 쿠키 아이디는 access_jwt, Refresh Token의 쿠키 아이디는 refresh_jwt로 작성하세요.
   *
   * 로그인 상태가 유지되길 원하지 않는다면 Access Token만 보내야합니다.
   *
   * 클라이언트에게 바로 응답을 보내지않고 서버의 /useinfo로 리다이렉트해야 합니다.
   * express의 res.redirect 메서드를 참고하여 서버의 /userinfo로 리다이렉트 될 수 있도록 구현하세요.
   */
  if (!userInfo.id) {
    res.status(401).send("Not Authorized");
  } else {
    // 로그인 성공 시에는 쿠키에 JWT를 담아 전송해야합니다.
    const {accessToken, refreshToken} = await generateToken(userInfo, checkedKeepLogin)
  
    const cookieOptions = {
      domain: "localhost",
      path: "/",
      httpOnly: true,
      sameSite: "none",
      secure: true,
    };
    
    res.cookie('access_jwt',accessToken, cookieOptions)
    // 로그인 상태가 유지되어야 한다면 Access Token과 Refresh Token 모두 보내야합니다.
    if(checkedKeepLogin) {
      cookieOptions.maxAge = 1000 * 60 * 60 * 24 * 7
      res.cookie('refresh_jwt', refreshToken, cookieOptions)
    }
    res.redirect('/userinfo')
  }
};

server/controllers/users/logout.js

module.exports = (req, res) => {
  /*
   * TODO: 로그아웃 로직을 구현하세요. 로그아웃 요청은 쿠키에 저장된 토큰을 삭제하는 과정을 포함해야 합니다.
   *
   * cookie-parser의 clearCookie('쿠키의 키') 메서드로 해당 키를 가진 쿠키를 삭제할 수 있습니다.
   * 만약 res.clearCookie('user') 코드가 실행된다면 `user=....` 쿠키가 삭제됩니다.
   * Refresh Token을 발급받았던 유저라면 Refresh Token 또한 삭제되어야 합니다.
   *
   * 로그아웃 성공에 대한 상태 코드는 205가 되어야합니다.
   */
  const { access_jwt, refresh_jwt } = req.cookies;
  const cookieOptions = {
    domain: "localhost",
    path: "/",
    httpOnly: true,
    sameSite: "none",
    secure: true,
  };
  res.clearCookie("access_jwt", cookieOptions);
  if (refresh_jwt) {
    res.clearCookie("refresh_jwt", cookieOptions);
  }
  res.status(205).send("logout!");
};

server/controllers/users/userInfo.js

const { USER_DATA } = require("../../db/data");
// JWT는 verifyToken으로 검증할 수 있습니다. 먼저 tokenFunctions에 작성된 여러 메서드들의 역할을 파악하세요.
const { verifyToken, generateToken } = require("../helper/tokenFunctions");

module.exports = async (req, res) => {
  /*
   * TODO: 토큰 검증 여부에 따라 유저 정보를 전달하는 로직을 구현하세요.
   *
   * Access Token에 대한 검증이 성공하면 복호화된 payload를 이용하여 USER_DATA에서 해당하는 유저를 조회할 수 있습니다.
   * Access Token이 만료되었다면 Refresh Token을 검증해 Access Token을 재발급하여야 합니다.
   * Access Token과 Refresh Token 모두 만료되었다면 상태 코드 401을 보내야합니다.
   */
  const { access_jwt, refresh_jwt } = req.cookies;

  const accessPayload = await verifyToken("access", access_jwt);
  if (accessPayload) {
    const userInfo = {
      ...USER_DATA.filter((user) => user.id === accessPayload.id)[0],
    };
    if (!userInfo.id) {
      res.status(401).send("Not Authorized");
    }
    delete userInfo.password;
    res.send(userInfo);
  } else if (refresh_jwt) {
    const refreshPayload = await verifyToken("refresh", refresh_jwt);
    if (!refreshPayload) {
      res.status(401).send("Not Authorized");
    }
    const userInfo = {
      ...USER_DATA.filter((user) => user.id === accessPayload.id)[0],
    };                                                                                                                 f
    if (!userInfo.id) {
      res.status(401).send("Not Authorized");
    }

    const { accessToken } = await generateToken(userInfo);
    const cookieOptions = {
      domain: "localhost",
      path: "/",
      httpOnly: true,
      sameSite: "none",
      secure: true,
    };
    res.cookie("access_jwt", accessToken, cookieOptions);
    res.redirect("/userinfo");
  } else {
    res.status(401).send("Not Authorized");
  }
};

'Code개발일지' 카테고리의 다른 글

Section3 회고  (0) 2023.01.11
OAuth,, sprint-auth-OAuth 참조  (0) 2023.01.10
[session] sprint-auth-session  (0) 2023.01.07
Session  (0) 2023.01.07
[cookie] sprint-auth-cookie  (0) 2023.01.07