[TIL] Day 25 : Authentication, Authorization

Q·2024년 5월 21일

TIL

목록 보기
26/59

Abstract

  • 인증: 사용자가 누구인지 확인하는 과정. (로그인)
  • 인가: 사용자가 특정 자원에 접근할 권한이 있는지 확인하는 과정. (권한 부여)

Authentication과 Authorization은 웹 애플리케이션 보안에서 중요한 개념으로, 각각의 역할이 다르다.

Authentication (인증; 認證)

사전적 의미: 인정하여 증명함.

쉽게 말해,

회사 빌딩 로비와 같이, 특정 건물(IP 주소)에 들어가기 위한 신분증 검사(로그인)를 하는 것이다.

서비스를 이용하려는 사용자가 누구인지를 확인하는 과정으로,
현재 로그인 하려는 사용자가 DB에 저장된 사용자 정보와 일치하는지 확인한다.

과정:

  1. 사용자가 로그인 폼에 사용자 이름과 비밀번호를 입력한다.

  2. 서버는 해당 자격 증명이 유효한지 데이터베이스에서 확인한다.

  3. 유효하다면, 서버는 사용자에게 세션 토큰이나 JWT(JSON Web Token)를 발급한다.

  4. 사용자는 이후 요청에 이 토큰을 포함시켜 자신의 신원을 증명한다.

예제:

// express, bcrypt, jwt, prisma를 이용한 예제

router.post('/sign-in', async (req, res, next) => {
  //1. email, passowrd를 body로 전달 받는다.
  const { email, password } = req.body;

  //2. 전달 받은 email에 해당하는 사용자가 있는지 확인한다.
  const user = await prisma.users.findFirst({ where: { email } });
  if (!user) {
    return res.status(401).json({ message: '등록되지 않는 이메일입니다.' });
  }

  //3. 전달 받은 passoword와 DB에 저장된 password를 bcrypt를 이용해 검증한다.
  const pwMatch = await bcrypt.compare(password, user.password);
  if (!pwMatch) {
    return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
  }

  //4. 로그인에 성공한다면, 사용자에게 JWT를 발급한다.
  const token = jwt.sign(
    { userId: user.userId },
    env('SECRET_KEY'), //외부에서 코드를 보더라도 알 수 없도록 dotenv에 추가
  );
  res.cookie('authorization', `Bearer ${token}`);
  return res.status(200).json({ message: '로그인 성공했습니다.' });
});

Authorization (인가; 認可)

사전적 의미: 인정하여 허가함

쉽게 말해,

회사 건물에서 특정 방에 들어가려면 별도의 권한을 요청하고 검사하는 것과 같다.

즉, 이미 인증된(로그인 한) 사용자가 특정 리소스에 접근하거나 특정 작업을 수행하려고 할 때, 그 권한이 있는지를 검증하는 과정이다.

대표적인 예로,

  • 게시글, 댓글을 작성할 때: 로그인 된 사용자인지 검증
  • 게시글, 댓글을 수정/삭제할 때: 해당 게시글, 댓글을 작성한 사용자 또는 관리자인지 검증

과정:

  1. 사용자가 요청을 보낼 때, 인증된 토큰을 포함시킨다.

  2. 서버는 토큰을 검증하여 사용자가 누구인지 확인한다.

  3. 서버는 사용자의 역할이나 권한을 확인하여 요청된 자원에 대한 접근 권한이 있는지 결정한다.

  4. 권한이 있으면 요청을 처리하고, 없으면 접근을 거부한다.

예제:

// Express.js에서의 예제

app.get('/admin', verifyToken, (req, res) => {
  if (req.user.role === 'admin') {
    res.send('Welcome, admin');
  } else {
    res.status(403).send('Access denied');
  }
});

function verifyToken(req, res, next) {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).send('Token is missing');
  }
  try {
    const user = verifyJWT(token);
    req.user = user;
    next();
  } catch (err) {
    res.status(401).send('Invalid token');
  }
}

JWT (Json Web Token)

서버와 클라이언트 사이에서 JSON 형태의 데이터를 안전하게 전송 및 검증할 수 있는 웹 표준 토큰이다.

형식:

header.payload.signiture

  • header : 어떤 방식으로 암호화 되었는지에 대한 정보가 담겨있다.

  • payload : 실제 전달하고자 하는 데이터가 iat라는 key와 함께 담겨 있다.

    • iat : issuedAt의 약어로, 토큰 발급 시간에 대한 정보를 담고 있다. (in seconds)
  • signiture:

장점:

  1. 누구든지 JWT 내부에 들어있는 정보를 decode 가능

JWT를 decode 하는 건 누구나 가능하다. (jsonwebtoken 라이브러리를 이용하거나, 공식 사이트에서도 가능하다.)
따라서 암호화가 되었다고 개인정보나 민감한 정보를 담아 보내면 안 된다.

Q) 그렇다면 누구나 decode 가능한데 왜 암호화할까?
A) 데이터를 읽는 것만 가능할 뿐이지, JWT가 암호화 되는 encoding 알고리즘을 알지 않는 한, 데이터를 위변조 할 수 없기 때문이다.

  1. 인증서버에서 발급되었는지 위변조 여부 verify 가능

쿠키나 세션과 달리, 어떤 서버에서 발급된 토큰인지 다양한 암호화 알고리즘을 사용해서 인증할 수 있어 데이터의 신뢰성을 보장한다.

  1. stateless한 데이터 관리

토큰 자체에 데이터가 저장되어 있어 서버에 데이터를 저장할 필요가 없다.
즉, stateless하게 데이터를 관리할 수 있다. (서버가 죽었다 살아나도 동일한 동작을 한다는 뜻)

쿠키나 토큰은 stateful한 데이터 관리 (서버가 죽었다 살아나면 쿠키의 증발 우려)

bcrypt

입력받은 데이터의 암호화 및 검증을 도와주는 모듈이다.
특정 알고리즘을 통해 hashing과 salting을 한다.

주민등록번호나 비밀번호와 같은 정보를 String 그대로 저장하는 것이 아니라
이렇게 bcrypt과 같은 모듈을 이용하여 암호화 해서 저장하게 되면,
DB가 해킹당하더라도 사용자 정보의 보안을 확보할 수 있게 된다.

왜냐하면, hashing은 일반적으로 단방향이기 때문이다. 즉, encode는 가능해도, decode는 어렵다. (일반적으로 암호화는 소수 두 개를 곱하는 건 쉬워도, 그 수를 소인수 분해하는 두 소수를 찾는 건 어렵다는 사실을 이용한다.)

Q) 그렇다면 DB에는 비밀번호가 encoded 된 상태로 저장이 되는데, 어떻게 비밀번호가 일치하는지 검증할 수 있을까?
A) 클라이언트가 전달한 비밀번호를 똑같은 알고리즘 혹은 모듈로 encoding 해서 DB에 저장된 encoded password와 일치하는지 확인하면 된다.

예제:

const password = 'HelloPassword'; //사용자의 비밀번호
const saltRounds = 10; //salting을 몇 번 반복할 지 설정
const hashedPassword = await bcrypt.hash(password, saltRounds);

const trueResult = await bcrypt.compare('HelloPassword' , hashedPassword);
const falseResult = await bcrypt.compare('helloworld' , hashedPassword);

console.log(trueResult); //true
console.log(falseResult); //false

0개의 댓글