Unit7 - [Backend] 인증 / 보안 + 12주차 회고

강성일·2023년 7월 2일
0
post-thumbnail

✅ TIL


Naver에 로그인하는 것은 본질적으로 “나는 Naver에게 내 정보를 줄 수 있을 정도로 믿고 있다.”와 같다.
내 아이디, 비밀번호뿐만 아니라 심지어는 나의 주소, 카드번호, 계좌의 비밀번호 등을 알려주기도 한다.

우리는 어떻게 이런 특정 웹사이트들을 신뢰할 수 있을까?
아니, 웹사이트들은 여러분에게 신뢰를 주기 위해 어떻게 정보를 안전하게 보호하고 있을까?

오늘은 HTTPS의 작동방식과 여러 인증방식을 중점으로 학습하게 된다.

로그인, 로그아웃과 같은 기능을 구현하게 된다.
그리고 큰 개념인 인증(authentication)에 대해서 알아보자.

클라이언트, 서버를 모두 다루면서, Full Stack 개발 환경에서의 전체적 흐름 및 작동을 직접 확인할 것이다.



🍪 Cookie


쿠키는 서버에서 클라이언트에 영속성 있는 데이터를 저장하는 방법이다.
서버는 클라이언트의 쿠키를 이용하여 데이터를 가져올 수 있다.

그러므로 쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만
의미하지 않고 클라이언트에서 서버로 쿠키를 다시 전송하는 것도 포함된다.

이런 쿠키에는 다음과 같은 특징이 있다.

서버가 클라이언트에 특정한 데이터를 저장할 수 있다.

앞서 언급한 것처럼 서버는 쿠키를 이용하여 데이터를 저장하고 이 데이터를 다시 불러와 사용할 수 있다.

하지만 데이터를 저장한 이후 아무 때나 데이터를 가져올 수는 없다.
데이터를 저장한 이후 특정 조건들이 만족되어야 다시 가져올 수 있기 때문이다.

이런 조건들은 아래 코드처럼 http 헤더를 사용해 쿠키 옵션으로 표현할 수 있다.

'Set-Cookie':[
            'cookie=yummy', 
            'Secure=Secure; Secure',
            'HttpOnly=HttpOnly; HttpOnly',
            'Path=Path; Path=/cookie',
            'Doamin=Domain; Domain=codestates.com'
        ]


쿠키 옵션 종류

1. Domain

  • 도메인이라는 것은 여러분들이 흔히 사용하는 www.google.com과 같은 서버에 접속할 수 있는 이름이다.
    쿠키 옵션에서 도메인은 포트 및 서브 도메인 정보, 세부 경로를 포함하지 않는다. 여기서 서브 도메인이란 www 같은 도메인 앞에 추가로 작성되는 부분을 말한다.
  • 따라서 요청해야 할 URL이 http://www.localhost.com:3000/users/login이라 하면 여기에서 Domain은 localhost.com이 된다.
  • 만약 쿠키 옵션에서 도메인 정보가 존재한다면 클라이언트에서는 쿠키의 도메인 옵션과 서버의 도메인이 일치해야만 쿠키를 전송할 수 있다. 이를 통해 naver.com에서 받은 쿠키를 google.com에 전송하는 일을 막을 수 있다.

2. Path

  • Path는 세부 경로로써 서버가 라우팅할 때 사용하는 경로를 의미한다.
    만약 요청해야 하는 URL이 http://www.localhost.com:3000/users/login인 경우라면 여기에서 Path, 즉 세부 경로는 /users/login이 됩니다. 이를 명시하지 않으면 기본적으로 /으로 설정되어 있다.
  • Path 옵션의 특징은 설정된 경로를 포함하는 하위 경로로 요청을 하더라도 쿠키를 서버에 전송할 수 있다. 즉 Path/users로 설정되어 있고, 요청하는 세부 경로가 /users/codestates인 경우라면 쿠키 전송이 가능하다.
  • 하지만 /posts/codestates로 전송되는 요청은 Path 옵션(/users)을 만족하지 못하기 때문에 서버로 쿠키를 전송할 수 없다.

3. MaxAge or Expires

  • 쿠키가 유효한 기간을 정하는 옵션이다. 만약 쿠키가 영원히 남아있다면 그만큼 탈취되기도 쉬워지기 때문에 이러한 유효기간을 설정하는 것이 보안 측면에서 중요하다.
  • MaxAge는 쿠키가 유효한 시간을 초 단위로 설정하는 옵션이다. 마치 쿠키에게 시한부 옵션을 주는 것과 비슷하다고 볼 수 있다.
  • ExpiresMaxAge와 비슷하지만 언제까지 쿠키가 유효한지 심판의 날을 지정할 수 있다. 이때 옵션의 값은 클라이언트의 시간을 기준으로 한다. 이후 지정된 시간, 날짜를 초과하게 되면 쿠키는 자동으로 파괴된다.
  • 쿠키는 위 옵션의 여부에 따라 세션 쿠키(Session Cookie)와 영속성 쿠키(Persistent Cookie)로 나눠진다.
    • 세션 쿠키: MaxAge 또는 Expires 옵션이 없는 쿠키로, 브라우저가 실행 중일 때 사용할 수 있는 임시 쿠키이다. 브라우저를 종료하면 해당 쿠키는 삭제된다.
    • 영속성 쿠키: 브라우저의 종료 여부와 상관없이 MaxAge 또는 Expires에 지정된 유효시간만큼 사용가능한 쿠키이다.

4. Secure

  • 사용하는 프로토콜에 따른 쿠키의 전송 여부를 결정하는 옵션입니다. 만약 Secure 옵션이 true로 설정된 경우 HTTPS를 이용하는 경우에만 쿠키를 전송할 수 있다.
  • Secure 옵션이 없다면 프로토콜에 상관없이 http://www.codestates.com 또는 https://www.codestates.com에 모두 쿠키를 전송할 수 있다.
  • 단, 도메인이 localhost인 경우에는 HTTPS가 아니어도 쿠키 전송이 가능하다. 개발 단계에서는 localhost를 사용하는 경우가 많기 때문에 생긴 예외이다.

5. HttpOnly

  • 자바스크립트로 브라우저의 쿠키에 접근이 가능한지 여부를 결정한다. 만약 해당 옵션이 true로 설정된 경우, 자바스크립트로 쿠키에 접근이 불가하다.
  • 옵션을 명시하지 않는 경우에는 기본적으로 false로 지정된다. 만약 이 옵션이 false인 경우 document.cookie를 이용해 자바스크립트로 쿠키에 접근할 수 있으므로 쿠키가 탈취될 위험이 있다.

6. SameSite

  • Cross-Site 요청을 받은 경우, 요청에서 사용한 메서드(e.g. GET, POST, PUT, PATCH …)와 해당 옵션의 조합을 기준으로 서버의 쿠키 전송 여부를 결정하게 된다. 이때, Cross-Origin과 Cross-Site를 혼동하지 않도록 주의해야 한다.
    • Cross-Origin : 서버의 도메인, 프로토콜, 포트 중 하나라도 다른 경우 Cross-Origin으로 구분된다.
      • http://codestates.com vs https://codestates.com ⇒ 프로토콜이 다르므로 Cross-Origin이다.
      • https://codestates.com:443 vs https://codestates.com ⇒ https의 기본 포트는 443이다. 따라서 도메인, 프로토콜, 포트가 모두 같은 Same-Origin이다.
    • Cross-Site : eTLD+1이 다른 경우 Cross-Site로 구분된다.
      • eTLD+1이 무엇일까?
        • TLD(Top Level Domain, 최상위 도메인): .com, .org, .kr, .io 같이 도메인의 가장 마지막 부분을 의미한다.
        • eTLD(Effective Top-Level Domain, 유효 최상위 도메인): 사이트를 식별할 수 있을 만큼 세분화된 TLD를 말한다. .com, .org와 같은 TLD는 그 자체로도 eTLD로 판단하며, .kr, .io 같은 TLD는 하위 도메인을 하나 더 더한 .co.kr, .github.io등을 eTLD라고 판단한다.
        • eTLD+1: eTLD 바로 왼쪽의 하위 레벨 도메인을 합한 것을 말한다.
        • http://codestates.com vs https://codestates.com ⇒ 두 주소 모두 eTLD는 .com, eTLD+1은 codestates.com으로 같으므로 Same-Site이다.
        • https://code.github.io vs https://states.github.io ⇒ 두 주소 모두 eTLD가 github.io로 같지만, eTLD+1은 각각 code.github.io, states.github.io로 다르므로 Cross-Site이다.
  • SameSite 옵션에서 사용할 수 있는 속성은 다음과 같다.
    • Lax: Cross-Site 요청이라면 GET 메서드에 대해서만 쿠키를 전송할 수 있다.
    • Strict: 단어 그대로 가장 엄격한 옵션으로, Cross-Site가 아닌 Same-Site인 경우에만 쿠키를 전송할 수 있다.
    • None: Cross-Site에 대해 가장 관대한 옵션으로 항상 쿠키를 보내줄 수 있다. 다만 쿠키 옵션 중 Secure 옵션이 필요하다.

서버에서 이러한 옵션들을 지정한 다음 서버에서 클라이언트로 쿠키를
처음 전송하게 된다면 헤더에 Set-Cookie라는 프로퍼티로 쿠키를 담아 전송한다.

이후 클라이언트에서 서버에게 쿠키를 전송해야 한다면 클라이언트는
헤더에 Cookie라는 프로퍼티에 쿠키를 담아 서버에 쿠키를 전송하게 된다.



쿠키를 이용한 상태 유지


이러한 쿠키의 특성을 이용하여 서버는 클라이언트에 인증정보를 담은 쿠키를 전송하고,
클라이언트는 전달받은 쿠키를 서버에 요청과 함께 전송하여 Stateless한 인터넷 연결을 Stateful하게 유지할 수 있다.

하지만 기본적으로 쿠키는 오랜 시간 동안 유지될 수 있고, HttpOnly 옵션을 사용하지 않았다면
자바스크립트를 이용해서 쿠키에 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다.

이런 인증정보를 이용해 공격자가 유저인척 서버에 요청을 보낸다면
서버는 누가 요청을 보낸 건지 의심하지 않고 이를 인증된 유저의 요청으로 취급하게 된다.

이때 개인정보와 같은 민감한 정보를 공격자가 탈취한다면 2차 피해가 일어날 수 있다.



💾 Session


세션기반 인증 (Session-based Authentication)


로그인

로그인을 통해 사용자의 인증 정보를 저장하고, 인증된 사용자가 어떤 식으로 웹사이트를 이용하는지 간단하게 알아보자.

사용자가 웹사이트에서 아이디 및 비밀번호를 이용해서 로그인을 시도하면(그림에서 1번), 과연 어떤 일이 벌어질까?
사용자가 만일 정확한 아이디와 비밀번호를 입력했다면, 서버는 인증(Authentication)에 성공했다고 판단할 것이다.

그렇다면, 다음번에 인증을 필요로 하는 작업(e.g. 그림에서와 같이, 장바구니에 물품 추가)을 요청할 경우에 한번 더 로그인 과정을 거쳐야 할까?

아니다. 서버가 "해당 유저는 인증에 성공했음"을 알고 있다면, 유저가 매번 로그인할 필요가 없을 것이다.

인증에 따라 리소스의 접근 권한(Authorization)이 달라진다.

이때 서버와 클라이언트에 각각 필요한 것은 다음과 같다.

  • 서버: 사용자가 인증에 성공했음을 알고 있어야 한다.
  • 클라이언트: 인증 성공을 증명할 수단을 갖고 있어야 한다.

여기서 몇 가지 용어가 등장한다.

  • 사용자가 인증에 성공한 상태는 세션이라고 부른다.
    • 서버는 일종의 저장소에 세션을 저장한다. (그림에서 2번) 주로 in-memory(자바스크립트 객체를 생각하면 됩니다), 또는 세션 스토어(redis 등과 같은 트랜잭션이 빠른 DB)에 저장한다.
  • 세션이 만들어지면, 각 세션을 구분할 수 있는 세션 아이디도 만들어지는데(그림에서 3번), 보통 클라이언트에 세션 성공을 증명할 수단으로써 세션 아이디를 전달한다. (그림에서 4번)

이때 웹사이트에서 로그인을 유지하기 위한 수단으로 쿠키를 사용한다.
쿠키에는 서버에서 발급한 세션 아이디를 저장한다.

쿠키를 통해 유효한 세션 아이디가 서버에 전달되고, (그림에서 5번)
세션 스토어에 해당 세션이 존재한다면 (그림에서 6번)
서버는 해당 요청에 접근 가능하다고 판단한다. (그림에서 7,8번)

하지만 쿠키에 세션 아이디 정보가 없는 경우, 서버는 해당 요청이 인증되지 않았음을 알려준다.



로그아웃

그렇다면, 로그아웃은 어떻게 구현해야 할까?

세션 아이디가 담긴 쿠키는 클라이언트에 저장되어 있으며, 서버는 세션을 저장하고 있다.
그리고 서버는 그저 세션 아이디로만 인증 여부를 판단한다.

주의: 쿠키는 세션 아이디, 즉 인증 성공에 대한 증명을 갖고 있으므로, 탈취될 경우 서버는 해당 요청이 인증된 사용자의 요청이라고 판단한다. 이것이, 우리가 공공 PC에서 로그아웃해야 하는 이유이다.

그러므로 로그아웃은 다음 두 가지 작업을 해야 한다.

  • 서버: 세션 정보를 삭제해야 한다.
  • 클라이언트: 쿠키를 변경하거나 삭제해야 한다.

클라이언트에서 세션 정보를 없애기 위해서는 res.cookie로 쿠키의 값을 무효한 값으로 변경하거나,
res.clearCookie로 쿠키를 삭제해 버리면 된다.



express-session

Node.js에는 이런 세션을 대신 관리해 주는 express-session이라는 모듈이 존재한다.

아래 설명을 참고하여 모듈의 역할 및 사용법을 알아보자.

express-session은 세션을 위한 미들웨어로, express 서버에서 쉽게 세션을 위한 공간을 다룰 수 있도록 만들어준다.

const express = require('express');
const session = require('express-session');

const app = express();

app.use(
  session({
    secret: '@codestates',
    resave: false,
    saveUninitialized: true,
    cookie: {
      domain: 'localhost',
      path: '/',
      maxAge: 24 * 6 * 60 * 10000,
      sameSite: 'none',
      httpOnly: false,
      secure: true,
    },
  })
);


express-session를 사용해 위와 같이 세션의 옵션을 지정할 수 있다. 언뜻 보면 쿠키 옵션과 비슷해 보인다.

하지만 세션의 경우 secret 옵션의 비밀키를 이용해 암호화해 세션 id라는 것을 생성한다.
그리고 이것을 클라이언트에게 쿠키로 전송한다.

쿠키로 전송된 세션 id는 이에 종속되는 고유한 세션 객체를 가지며 이는 서버에 저장된다.
이때 세션 객체는 유저별로 독립적으로 생성된 객체이므로 유저별로 각각 다른 데이터를 저장할 수 있다.

따라서 클라이언트에 유저의 개인정보를 담지 않고도,
서버가 클라이언트의 세션 id를 이용해 유저의 인증여부를 판단할 수 있다.

세션 객체는 req.session으로 접근할 수 있으며 앞서 말했듯
이를 통해 세션에 임의의 데이터를 저장하거나 불러올 수 있다.



🪪 Token


Hashing


가장 많이 쓰이는 암호화 방식 중에 하나인 해싱을 소개한다.
복호화가 가능한 다른 암호화 방식들과 달리, 해싱은 암호화만 가능하다.

해싱은 해시 함수(Hash Function)를 사용하여 암호화를 진행하는데, 해시 함수는 다음과 같은 특징을 가진다.

  • 항상 같은 길이의 문자열을 리턴합니다.
  • 서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나옵니다.
  • 동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나옵니다.

아래 표는 대표적인 해시 함수중 하나인 SHA1에 특정 입력 값을 넣었을 때 어떤 결과가 리턴되는지 보여주는 예시이다.

비밀번호해시 함수(SHA1) 리턴 값
‘password’‘5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8’
‘Password’‘8BE3C943B1609FFFBFC51AAD666D0A04ADF83C9D’
‘kimcoding’‘61D17C8312E8BC24D126BE182BC674704F954C5A’


레인보우 테이블과 솔트(Salt)

그런데 항상 같은 결과값이 나온다는 특성을 이용해 해시 함수를 거치기
이전의 값을 알아낼 수 있도록 기록해 놓은 표인 레인보우 테이블이 존재한다.

레인보우 테이블에 기록된 값의 경우에는 유출이 되었을 때 해싱을 했더라도
해싱 이전의 값을 알아낼 수 있으므로 보안상 위협이 될 수 있다.

이때 활용할 수 있는 것이 솔트(Salt)이다.

솔트는 소금이라는 뜻으로, 말 그대로 소금을 치듯 해싱 이전 값에 임의의 값을 더해
데이터가 유출되더라도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법이다.

비밀번호 + 솔트해시 함수(SHA1) 리턴 값
‘password’ + ‘salt’‘C88E9C67041A74E0357BEFDFF93F87DDE0904214’
‘Password’ + ‘salt’‘38A8FDE622C0CF723934BA7138A72BEACCFC69D4’
‘kimcoding’ + ‘salt’‘8607976121653D418DDA5F6379EB0324CA8618E6’


솔트를 사용하게 되면 해싱 값이 유출되더라도,
솔트가 함께 유출된 것이 아니라면 암호화 이전의 값을 알아내는 것은 불가능에 가깝다.


해싱의 목적

그런데, 왜 복호화가 불가능한 암호화 방식을 사용하는 걸까?

바로 해싱의 목적은 데이터 그 자체를 사용하는 것이 아니라,
동일한 값의 데이터를 사용하고 있는지 여부만 확인하는 것이 목적이기 때문이다.

예시를 들어보겠다. 사이트 관리자는 사용자의 비밀번호를 알고 있을 필요가 없다.

오히려 사용자들의 비밀번호를 알고 있다면, 이를 얼마든지 악용할 수 있기 때문에 심각한 문제가 생길 수도 있다.

그래서 보통 비밀번호를 데이터베이스에 저장할 때, 복호화가 불가능하도록 해싱하여 저장하게 된다.
해싱은 복호화가 불가능하므로 사이트 관리자도 정확한 비밀번호를 알 수 없게 된다.

그럼 서버 측에서 비밀번호를 모르는 상태에서 어떻게 로그인 요청을 처리할 수 있는 걸까? 방법은 간단하다.
해싱한 값끼리 비교해서 일치하는지 확인하는 것이다. 꼭 정확한 값을 몰라도, 해싱한 값이 일치한다면 정확한 비밀번호를 입력했다는 뜻이 되기 때문에, 해싱 값으로만 로그인 요청을 처리하는 데에도 전혀 문제가 없다.

이처럼 해싱은 민감한 데이터를 다루어야 하는 상황에서 데이터 유출의 위험성은 줄이면서
데이터의 유효성을 검증하기 위해서 사용되는 단방향 암호화 방식이다.



Token


토큰 인증 방식

토큰 인증 방식은 최근 웹 애플리케이션에서 많이 사용되는 인증 방식 중 하나이다.
토큰을 사용하면 사용자의 인증 정보를 서버가 아닌 클라이언트 측에 저장할 수 있다.


토큰 인증 방식의 등장 배경

토큰 기반 인증은 기존의 세션 기반 인증이 가지고 있던 한계를 극복하고자 고안되었다.

세션 기반 인증은 서버에서 유저의 상태를 관리한다.

그래서 개발자들은 서버의 부담을 줄이기 위해 서버가 사용자의 인증 상태를 저장하는 것이 아닌
클라이언트에 이를 저장하는 방법을 고민하게 되었고, 그 결과 토큰 기반 인증 방식이 등장하였다.

토큰은 유저의 인증 상태를 클라이언트에 저장할 수 있어서,
세션 인증 방식의 비교해 서버의 부하나 메모리 부족 문제를 줄일 수 있다.

토큰은 교통 승차권과 같이 무언가를 이용할 수 있는 권한이나 자격을 나타내는, 일종의 증표이다.
일상생활에서 흔히 접하는 지하철 승차권 혹은 사무실 출입 카드가 토큰에 해당한다.

웹 보안에서의 토큰은 인증과 권한 정보를 담고 있는 암호화된 문자열을 말한다.
이를 이용해 특정 애플리케이션에 대한 사용자의 접근 권한을 부여할 수 있다.


토큰 인증 방식의 흐름

  1. 사용자가 인증 정보를 담아 서버에 로그인 요청을 보낸다.

  2. 서버는 데이터베이스에 저장된 사용자의 인증 정보를 확인한다.

  3. 인증에 성공했다면 해당 사용자의 인증 및 권한 정보를 서버의 비밀 키와 함께 토큰으로 암호화한다.

  4. 생성된 토큰을 클라이언트로 전달한다.

    • HTTP 상에서 인증 토큰을 보내기 위해 사용하는 헤더인 Authorization 헤더를 사용하거나, 쿠키로 전달하는 등의 방법을 사용한다.
  5. 클라이언트는 전달받은 토큰을 저장한다.

    • 저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양하다.
  6. 클라이언트가 서버로 리소스를 요청할 때 토큰을 함께 전달한다.

    • 토큰을 보낼 때에도 Authorization 헤더를 사용하거나 쿠키로 전달할 수 있다.
  7. 서버는 전달받은 토큰을 서버의 비밀 키를 통해 검증한다. 이를 통해 토큰이 위조되었는지 혹은 토큰의 유효 기간이 지나지 않았는지 등을 확인할 수 있다.

  8. 토큰이 유효하다면 클라이언트의 요청에 대한 응답 데이터를 전송한다.


토큰 인증 방식의 장점

  • 무상태성 : 서버가 유저의 인증 상태를 관리하지 않는다. 서버는 비밀 키를 통해 클라이언트에서 보낸 토큰의 유효성만 검증하면 되기 때문에 무상태적인 아키텍처를 구축할 수 있다.
  • 확장성 : 다수의 서버가 공통된 세션 데이터를 가질 필요가 없다는 것도 토큰 기반 인증의 장점이다. 이를 통해 서버를 확장하기 더 용이하다.
  • 어디서나 토큰 생성 가능 : 토큰의 생성과 검증이 하나의 서버에서 이루어지지 않아도 되기 때문에 토큰 생성만을 담당하는 서버를 구축할 수 있다. 이를 잘 활용하면 여러 서비스 간의 공통된 인증 서버를 구현할 수 있다.
  • 권한 부여에 용이 : 토큰은 인증 상태, 접근 권한 등 다양한 정보를 담을 수 있기 때문에 사용자 권한 부여에 용이하다. 이를 활용해 어드민 권한 부여 및 정보에 접근할 수 있는 범위도 설정할 수 있다.


JWT (JSON Web Token)


토큰 기반 인증 구현 시 대표적으로 사용하는 기술로 JWT(JSON Web Token)가 있다.

JWT는 JSON 객체에 정보를 담고 이를 토큰으로 암호화하여 전송할 수 있는 기술이다.

클라이언트가 서버에 요청을 보낼 때, 인증정보를 암호화된 JWT 토큰으로 제공하고,
서버는 이 토큰을 검증하여 인증정보를 확인할 수 있다.


JWT의 구성

JWT는 다음 그림과 같이 .으로 나누어진 세 부분이 존재하며 각각을 Header, Payload, Signature라고 부른다.

1. Header

Header에는 마치 HTTP의 헤더처럼 해당 토큰 자체를 설명하는 데이터가 담겨 있다.
토큰의 종류, 그리고 시그니처를 만들 때 사용할 알고리즘을 JSON 형태로 작성한다.

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

이 JSON 객체를 base64 방식으로 인코딩하면 JWT의 첫 번째 부분인 Header가 완성된다.

  • 참고로, base64 방식은 원한다면 얼마든지 디코딩할 수 있는 인코딩 방식이다. 따라서 비밀번호와 같이 노출되어서는 안 되는 민감한 정보를 담지 않도록 해야 한다.

2. Payload

Payload는 HTTP의 페이로드와 마찬가지로 전달하려는 내용물을 담고 있는 부분이다.

어떤 정보에 접근 가능한지에 대한 권한, 유저의 이름과 같은 개인정보,
토큰의 발급 시간 및 만료 시간 등의 정보들을 JSON 형태로 담는다.

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

이 JSON 객체를 base64로 인코딩하면 JWT의 두 번째 부분인 Payload가 완성된다.

3. Signature

Signature는 토큰의 무결성을 확인할 수 있는 부분이다.

Header와 Payload가 완성되었다면, Signature는 이를 서버의 비밀 키(암호화에 추가할 salt)와
Header에서 지정한 알고리즘을 사용하여 해싱한다.

예를 들어, 만약 HMAC SHA256 알고리즘을 사용한다면 Signature는 아래와 같은 방식으로 생성된다.

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

따라서 누군가 권한을 속이기 위해 토큰의 Payload를 변조하는 등의 시도를 하더라도 토큰을 발급할 때
사용한 Secret을 정확하게 알고 있지 못한다면 유효한 Signature를 만들어낼 수 없기 때문에
서버는 Signature를 검증하는 단계에서 올바르지 않은 토큰임을 알아낼 수 있다.


토큰 인증 방식의 한계

Signature을 사용해서 위조된 토큰을 알아낼 수는 있지만, 토큰 자체가 탈취된다면 토큰 인증 방식의 한계가 드러난다.

  • 무상태성 : 인증 상태를 관리하는 주체가 서버가 아니므로, 토큰이 탈취되어도 해당 토큰을 강제로 만료시킬 수 없다. 따라서 토큰이 만료될 때까지 사용자로 가장해 계속해서 요청을 보낼 수 있다.
  • 유효 기간 : 토큰이 탈취되는 상황을 대비해서 유효 기간을 짧게 설정하면, 사용자는 토큰이 만료될 때마다 다시 로그인을 진행해야 하기 때문에 좋지 않은 사용자 경험을 제공한다. 그렇다고 유효 기간을 길게 설정하면 토큰이 탈취될 경우 더 치명적으로 작용할 수 있다.
  • 토큰의 크기 : 토큰에 여러 정보를 담을 수 있는 만큼, 많은 데이터를 담으면 그만큼 암호화하는 과정도 길어지고 토큰의 크기도 커지기 때문에 네트워크 비용 문제가 생길 수 있다.

액세스 토큰(Access Token) & 리프레시 토큰(Refresh Token)

토큰 인증의 한계를 극복하기 위해 다양한 방법들이 고안되었지만,
이 중 대표적인 구현 방법은 액세스 토큰과 리프레시 토큰을 함께 사용하는 것이다.

  • Access Token : 액세스 토큰은 말 그대로 서버에 접근하기 위한 토큰으로 앞서 다룬 토큰과 비슷한 역할을 한다. 따라서 보안을 위해 보통 24시간 정도의 짧은 유효기간이 설정되어 있다.

  • Refresh Token : 리프레시 토큰은 서버 접근을 위한 토큰이 아닌 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 발급받기 위해 사용되는 토큰이다. 따라서 리프레시 토큰은 액세스 토큰보다 긴 유효기간을 설정한다.

이렇게 두 가지의 각기 다른 토큰을 사용하는 경우, 액세스 토큰이 만료되더라도 리프레시 토큰의
유효기간이 남아있다면 사용자는 다시 로그인을 할 필요 없이 지속해서 인증 상태를 유지할 수 있다.

물론 리프레시 토큰의 도입도 모든 문제를 해결해주진 않는다.

리프레시 토큰은 긴 유효 기간을 가지고 있어 해당 토큰마저 탈취된다면 토큰의 긴 유효 기간 동안
악의적인 유저가 계속해서 액세스 토큰을 생성하고 사용자의 정보를 해킹할 수도 있기 때문이다.

이를 대비하기 위해 리프레시 토큰을 세션처럼 서버에 저장하고 이에 대한 상태를 관리하기도 한다.

결국 서버에서 상태를 관리하지 않기 위해 고안된 토큰 인증도 보안성을 위해
일정 부분 서버에서 상태 관리를 담당하는 것처럼 결국 이 세상에 완벽한 보안 방법은 없다.

세션, 토큰 등 다양한 보안 방식 및 여러 구현 방법들은 절대 뚫리지 않는 궁극의 보안을 위해 만들어진 것이 아니다.

이러한 여러 방식들은 단순히 보안뿐만 아니라 보안과 사용자 경험 사이의 적절한 균형을 찾기 위해 만들어졌다.
따라서 개발자로서 내가 맡은 서비스에 어떤 인증 방식이 가장 적절한지 판단할 줄 아는 것이 가장 중요하다.



💼 OAuth


웹이나 앱에서 흔히 찾아볼 수 있는 소셜 로그인 인증 방식은 OAuth 2.0라는 기술을 바탕으로 구현된다.

전통적으로 직접 작성한 서버에서 인증을 처리해 주는 것과는 달리, OAuth는 인증을 중개해 주는 메커니즘이다.
보안된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공하는 프로세스를 단순화하는 프로토콜이다.

즉, 이미 사용자 정보를 가지고 있는 웹 서비스(Naver, Kakao, Google 등)에서
사용자의 인증을 대신해 주고, 접근 권한에 대한 토큰을 발급한 후, 이를 이용해 내 서버에서 인증이 가능해진다.


OAuth는 언제, 왜 쓸까?

몇 년 전만 하더라도 특정 웹 앱의 서비스를 이용하기 위해선 해당 웹 앱에 회원가입을 하는 것이 우선이었다.

하지만 소셜 로그인이 보편화된 현재는 대부분의 사람들이 네이버 또는 카카오에
이미 가입된 계정을 이용해 빠르게 서비스에 가입하는 것을 택하고 있다.

뿐만 아니라 서비스를 구현하는 개발자도 신규 회원가입이나 회원 관리를
신경 쓰지 않아도 되기 때문에 사용자와 기업 모두 소셜 로그인을 선호하고 있는 추세이다.

유저 입장에서 생각해보면, 우리는 웹상에서 굉장히 많은 서비스를 이용하고 있고
각각의 서비스들을 이용하기 위해서는 회원가입 절차가 필요한 경우가 대부분이다.

각각의 서비스별로 ID와 Password를 다 기억하는 것은 매우 귀찮은 일이다.

하지만 OAuth를 활용한다면 자주 사용하고 중요한 서비스들(예를 들어 google, github, facebook)의
ID와 Password만 기억해 놓고 해당 서비스들을 통해서 외부 서비스로 소셜 로그인을 할 수 있다.

뿐만 아니라 OAuth는 보안상의 이점도 있다.

검증되지 않은 App에서 OAuth를 사용하여 로그인한다면, 유저의 민감한 정보가 직접 App에 노출될
일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 더 안전하게 사용할 수 있다.


OAuth 작동 메커니즘


OAuth의 주체

Resource Owner

  • OAuth 인증을 통해 소셜 로그인을 하고 싶어 하는 사용자를 Resource Owner라고 부른다.
  • Resource는 사용자의 이름, 전화번호 등의 정보를 뜻한다. 이러한 정보의 주인이 바로 사용자이기 때문에 Resource Owner라고 한다.

Resource Server & Authorization Server

  • 사용자가 소셜 로그인을 하기 위해서 사용하는, 이미 사용 중인 서비스(Naver, Kakao, Google 등)의 서버 중 사용자의 정보를 저장하고 있는 서버를 특정해서 Resource Server라고 부른다.
  • 이미 사용 중인 서비스의 서버 중 인증을 담당하는 서버를 특정해서 Authorization Server라고 부른다.

Application

  • 사용자가 소셜 로그인을 활용해 이용하고자 하는 새로운 서비스는 환경에 따라서 조금씩 다르게 불린다. 여기서는 Application이라고 부르도록 하겠다.
  • 경우에 따라서 Applicaiton을 Client와 Server로 세분화해서 지칭하기도 한다.

OAuth 인증 방식의 종류와 흐름

OAuth 인증 방식에는 여러 가지가 있지만, 그중 Implicit Grant Type, Authorization Code Grant Type,
그리고 Refresh Token Grant Type, 이렇게 세 가지에 대해서 알아보겠다.

  • Grant Type : Authorization Server에서 Access Token을 받아오는 방식

1. Implicit Grant Type

  1. 사용자가 Application에 접속한다.
  2. Application에서 Authorization Server로 인증 요청을 보낸다.
  3. Authorizaiton Server는 유효한 인증 요청인지 확인한 후 액세스 토큰을 발급한다.
  4. Authorization Server에서 Application으로 액세스 토큰을 전달한다.
  5. Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청한다.
  6. Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인한다.
  7. 유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달한다.

이렇게 인증을 중개받아 새로운 서비스를 이용할 수 있게 되었다.
하지만 소셜 로그인에서 Implicit Grant Type은 잘 사용하지 않는다.

기존 서비스에 로그인만 되어있다면 새로운 서비스에 바로 액세스 토큰을 내어주기 때문에 보안성이 떨어지기 때문이다.
그래서 보통은 여기에 인증 단계를 한 단계 추가한 인증 방식인 Authorization Code Grant Type을 주로 사용하게 된다.


2. Authorization Code Grant Type

  1. 사용자가 Application에 접속한다.
  2. Application에서 Authorization Server로 인증 요청을 보낸다.
  3. Authorizaiton Server는 유효한 인증 요청인지 확인한 후 Authorization Code를 발급한다.
  4. Authorization Server에서 Application으로 Authorization Code를 전달한다.
  5. Application이 Authorization Code로 발급받은 Authorization Code를 전달한다.
  6. Authorizaiton Server는 유효한 Authorization Code인지 확인한 후 액세스 토큰을 발급한다.
  7. Authorization Server에서 Application으로 액세스 토큰을 전달한다.
  8. Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청한다.
  9. Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인한다.
  10. 유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달한다.

Implicit Grant Type과 비교해 보면, Authorization Code를 사용한 인증 단계가 추가로 있기 때문에 비교적 더 안전하다.

또한, 원한다면 아래와 같이 토큰을 Application의 Client에 노출시키지 않고,
Server에서만 관리하도록 만들 수도 있기 때문에 소셜 로그인을 구현하는 방식의 선택지가 늘어나게 된다.

그런데, 사용자가 새로운 서비스를 이용하다가 액세스 토큰이 만료되었을 때,
매번 이 과정을 거쳐서 액세스 토큰을 다시 발급받아야 한다면 사용자 편의성에 있어서는 좋지 않다.

그렇기 때문에 액세스 토큰을 발급해 줄 때 리프레시 토큰을 같이 발급해주기도 한다.

이때, 리프레시 토큰을 사용해서 액세스 토큰을 받아오는 인증 방식을 Refresh Token Grant Type이라고 한다.


3. Refresh Token Grant Type

Refresh Token Grant Type은 간단하다.

Authorization Server로 리프레시 토큰을 보내주면,
Authorization Server는 리프레시 토큰을 검증한 다음 액세스 토큰을 다시 발급해 주게 된다.

Application은 다시 발급받은 액세스 토큰을 사용해서 Resource Server에서 사용자의 정보를 받아오게 된다.



OAuth의 장점

  • 쉽고 안전하게 새로운 서비스 : 사용자는 OAuth를 통해 특정 사이트에 아이디, 비밀번호, 이름, 전화번호 등의 정보를 일일이 입력하지 않아도 클릭 몇 번 만으로 손쉽게 가입할 수 있어 편리하다.
    정보를 해당 서비스에 직접 노출하는 것이 아니기 때문에 직접 가입하는 것보다 더 안전하다.
    Application의 입장에서도 회원의 정보를 직접 가지고 있음으로 인해서 발생할 수 있는 회원 정보 유출의 위험성에서 부담을 덜 수 있다.
  • 권한 영역을 설정 가능 : OAuth 인증을 허가한다고 해서 새로운 서비스가 사용 중이던 서비스의 모든 정보에 접근이 가능한 것은 아니다. 사용자는 원하는 정보에만 접근을 허락할 수 있어 보다 더 안전하다.
    OAuth 설정 페이지에서는 Application에서 필요한 정보를 선택할 수 있다. 사용자는 이 중 원하는 정보만 선택적으로 제공할 수 있다.


💡 Error Note.


💡 또 다른 인증 방법 SSO ?

  • SSO는 "Single Sign-On"의 약어로, 한 번의 로그인으로 여러 개의 서비스나 애플리케이션에 접근할 수 있는 인증 및 인가 메커니즘이다. SSO를 사용하면 사용자는 여러 서비스에 대한 별도의 인증 과정을 거치지 않고도 자동으로 인증된다.

  • 일반적으로 SSO 시나리오에서는 다음과 같은 주체들이 관련된다:

  1. Identity Provider (IDP):

    • SSO를 제공하는 시스템 또는 서비스이다.
    • 사용자의 인증 정보를 저장하고 관리한다.
    • 주로 표준 프로토콜인 OAuth, OpenID Connect, SAML 등을 사용하여 인증 및 인가를 처리한다.
    • 대표적인 IDP로는 Google, Facebook, Okta, Azure AD 등이 있다.
  2. Service Provider (SP):

    • SSO를 사용하는 다양한 서비스 또는 애플리케이션이다.
    • 사용자가 액세스하고자 하는 서비스를 제공한다.
    • IDP와 통신하여 사용자의 인증 및 인가 정보를 검증한다.
  • SSO의 작동 원리:
  1. 사용자가 Service Provider (SP)에 접속한다.
  2. SP는 사용자를 인증하기 위해 IDP로 리디렉션한다.
  3. 사용자는 IDP에 로그인 정보를 제공하여 인증을 수행한다.
  4. IDP는 사용자의 인증 정보를 확인하고, 인가된 서비스를 결정한다.
  5. IDP는 인증된 사용자에게 인증 토큰을 발급한다.
  6. 사용자는 발급된 인증 토큰을 SP로 전달한다.
  7. SP는 인증 토큰을 IDP에 검증 요청한다.
  8. IDP는 토큰의 유효성을 확인하고, 인증 및 인가 정보를 SP에 전달한다.
  9. SP는 사용자를 인증하고, 해당 서비스에 접근을 허용한다.
  • SSO의 주요 장점:
  1. 편의성: 사용자는 여러 개의 서비스에 대해 하나의 로그인으로 접근할 수 있으므로 편리하다.
  2. 보안: 사용자의 인증 정보는 IDP에서 중앙 집중식으로 관리되고 보호된다.
  3. 효율성: 여러 개의 로그인 프로세스를 거치지 않아도 되므로 시간과 노력을 절약할 수 있다.
  4. 중앙 집중 관리: 사용자 계정과 권한은 IDP에서 관리되므로, 중앙 집중식으로 사용자를 관리할 수 있다.
  5. 개방성: 표준 프로토콜을 사용하므로 다양한 IDP와 SP 간의 통합이 가능하다.
  • SSO는 여러 서비스 및 애플리케이션 간의 사용자 인증 및 인가를 단순화하고 보안성을 향상시키는 강력한 메커니즘이다. 많은 기업 및 웹 서비스에서 SSO를 구현하여 사용자 경험을 향상시키고 보안을 강화하고 있다.
profile
아이디어가 넘치는 프론트엔드를 꿈꿉니다 🔥

0개의 댓글