[Nest.js] 인증(Authentication)과 인가(Authorization)

HoonDong_K·2025년 5월 24일

NestJS

목록 보기
3/3
post-thumbnail

인증 Authentication

인증(Authentication): 사용자의 신원을 확인하는 프로세스 (로그인)

로그인 설계

flow_chart

  1. Client에서 로그인 API(api.auth.login()) 요청
  2. Server에서 Auth Service가 해당 요청을 받고 로그인 정보를 통해 유저가 존재하는지 확인
  3. 유저가 존재한다면 Jwt Service를 통해서 refresh token과 access token을 생성한다.
  4. 생성된 두 토큰을 Auth Controller로 전달하고 해당 컨트롤러에서 refresh token은 HTTPOnly, samesite=strict옵션으로 쿠키에 담아주며 access token은 response 해준다.
  5. Client는 응답받은 access token과 함께 authStore.login() 호출
  6. authStore에서는 access token을 저장하고 api.me.get() 요청을 통해 유저의 정보를 요청한다.
  7. Client에서 axios.interceptor를 통해 만약 store에 access token이 존재한다면 요청 헤더에 authorization으로 토큰을 함께 전달한다.
  8. Server에서 요청을 받기 전에 Guard를 먼저 거치게 되고, 인가가 필요한 요청에 대해 전달된 토큰이 유효한 지 확인한다.
  9. Jwt Service에서 헤더에 담긴 토큰을 검증하고 올바른 토큰이라면 Me service에게 토큰에 담긴 회원 식별자로 회원 정보를 조회한다.
  10. Me Controller는 회원 정보를 Client로 응답한다.
  11. 받아온 회원 정보를 store에 저장한다.

Token

Access Token: 최소한의 유저 정보만을 담은 JWT토큰
Refresh Token: 만료된 Access Token을 다시 발급시켜주기 위한 토큰

Q. 각 토큰들은 어디에 저장이 되어야 안전할까?

'안전'하다라는 것부터 정의를 해야할 것 같다.
토큰을 탈취당했을 때 어떤 문제점이 발생할 수 있을까?

  1. 개인정보 노출
  2. 토큰을 통한 악의적인 요청 가능

그럼 어떻게 토큰이 탈취될 수 있을까?
대표적인 두 가지의 웹사이트 공격이 있다.

  1. XSS(Cross Site Scripting)
    공격자가 웹사이트에 악성 코드를 삽입하는 공격. 유효성 검사 혹은 인코딩을 사용하지 않을 경우 공격을 허용하게 되고 이를 통해 쿠키, 세션 토큰과 같은 개인 정보를 탈취하거나 인위적인 HTML 스크립트를 덮어 쓸 수 있다.
  2. CSRF(Cross-Site Request Forgery)
    브라우저가 쿠키를 자동으로 전송하기 때문에, 사용자가 악의적인 링크를 클릭하면 본인의 인증 정보를 이용한 의도치 않은 요청이 서버로 전달될 수 있다. 로그인이 되어 있는 상태에서 악의적인 링크를 클릭하게 되면 해당 웹사이트에 공격자의 의도가 담긴 요청(비밀번호 변경 등,,)을 보내게 되고 사용자는 자신도 모르게 정보가 변경되거나 탈취될 수 있다.

그러면 토큰을 웹에 저장할 수 있는 공간은 어디어디 있을까?

  1. Cookie : 매 요청마다 함께 서버로 전송되며 옵션에 따라 보안 설정이 가능하다.
  2. Local Storage: 영구 보관이 가능하고(기간 설정 불가) 도메인마다 스토리지 생성
  3. Session Storage: 세션(브라우저, 탭)이 종료되면 데이터가 사라진다.
  4. Memory: 런타임 메모리에 저장되며 새로고침 시 초기화된다.

그러면 우리는 예상되는 공격으로부터 안전하게 보관할 수 있는 곳에 토큰을 저장하면 안전하다라고 말할 수 있을 것이다.

🍪 쿠키 속성

Cookie에는 옵션을 통해 여러 보안 설정을 할 수 있다.
그 중에는 XSS와 CSRF를 막을 수 있는 보안 설정도 존재한다.

  1. Secure: https 통신에서만 쿠키 접근 가능
  2. HTTPOnly: Javascript의 document.cookie API를 통해 쿠키 접근 불가
  3. SameSite: 방문 중인 사이트와 다른 사이트의 리소스를 요청하는 것을 Cross Site 요청이라고 하며 이에 대한 응답으로 설정된 쿠키를 Third Party Cookie라고 부른다. 해당 속성은 세 가지 옵션이 제공된다.
    1. Strict: 타 사이트에서 시작된 모든 요청에 쿠키를 포함하지 않음
    2. Lax: 일부 안전한 방법(GET허용, POST불허)으로 타 사이트에서 접근 시 쿠키 전송 허용
    3. None: 제약 없음 (Secure 속성이 켜져야 한다.)
  4. Expires/Max-Age: Expires는 날짜와 시간으로, Max-Age는 초 시간으로 쿠키의 만료 기간을 설정할 수 있다.
  5. Domain: 쿠키가 적용되는 도메인을 설정하고 해당 도메인과 서브 도메인들의 요청에 쿠키가 포함될 수 있다.
  6. Path: 경로를 설정하면 해당 경로 이하의 요청에 대해서만 쿠키가 포함된다. /articles > /articles/a/b/c 포함

SameSite에 대해 더 자세히 보자면,
a.com과 그 하위 도메인인 api.a.com은 서브 도메인 관계이기에 같은 사이트(Same Site)로 판단한다.
완전히 다른 사이트에 대해 쿠키 전송 제약을 주는 옵션이다.

예시) 

[b.com]이라는 사이트에 로그인을 하여 각각 쿠키를 얻게 되었다.
- 쿠키A: SameSite=Strict
- 쿠키B: SameSite=Lax
- 쿠키C: SameSite=None; Secure


1. [a.com]에서 [b.com] 링크를 클릭 (안전한 요청)
	- 쿠키 A: 전송 X
    - 쿠키 B: 전송 O
    - 쿠키 C: 전송 O
    
2. [a.com]에서 [b.com]의 이미지 요청(<img src="https://b.com/image.png">)
	- 쿠키 A: 전송 X
    - 쿠키 B: 전송 X
    - 쿠키 C: 전송 O

그렇다면 쿠키를 통해서 어떻게 XSS와 CSRF를 막을 수 있을까?

HTTPOnly는 자바스크립트의 코드를 통해 쿠키에 접근, document.cookie API를 사용할 수 없기 때문에 악성 스크립트를 포함하여 공격하는 XSS를 방지할 수 있다.

SameSite=Strict는 동일한 사이트에서만 들어오는 요청에 대해 쿠키를 전송하기 때문에 외부 사이트에서 인증된 토큰을 통해 요청을 보내오는 CSRF 공격을 방지할 수 있다.

Lax설정 또한 안전한 네비게이션(GET 요청)에 대해서만 쿠키를 보내고 POST나 AJAX, iframe과 같은 요청에는 쿠키를 안보내기 때문에 어느 정도 CSRF 내성이 있긴 하지만 Strict가 제일 안전하다.

그렇다면 Access Token과 Refresh Token을 Cookie에 저장하는 것이 안전한 방법인가?

Cookie 보안 속성을 통해 XSS, CSRF 공격들을 예방하여 토큰을 웹에 저장시킬 수 있다는 장점이 있다.

하지만 보안을 강화시키게 되면 개발자 또한 접근이 어려워지는 경우가 있다.

예를 들어 HTTPOnly는 자바스크립트 코드를 통해 쿠키에 접근을 막는 속성이다. 즉, 클라이언트는 해당 쿠키를 다를 수 없으며, 요청/응답을 통해서만 쿠키를 전달할 수 있다.

하지만 Access Token은 요청 헤더에 담아 서버로 전달하여 인가를 받기위해 만들어진 토큰이지만 HTTPOnly 옵션 때문에 코드 상으로 쿠키에서 추출할 수 없어, 요청 헤더에 직접 담을 수 없다는 문제가 발생한다.

//plugins/axios.ts

const accessToken = document.cookie.get(...) // HTTPOnly 속성으로 쿠키 추출 ❌
                                        
if (accessToken) {
	config.headers.Authorization = `Bearer ${token}`;
}

그렇기 때문에 Refresh Token은 쿠키에 저장하고 Access Token은 다른 방식으로 저장할 필요가 있다.

바로 메모리에 저장하는 방식이다.

메모리를 저장해주는 방식은 여러가지가 있겠지만 현재 Vue를 통해 제작하고 있기에 이에 친숙한 Pinia를 사용하여 전역으로 Store에 토큰을 저장해주는 방식을 채택하였다.

그렇다면 메모리는 CSRF와 XSS 공격으로부터 안전한가?

  • CSRF: 자동으로 요청에 쿠키가 담겨지는 것을 이용한 공격이기 때문에 메모리에 저장되고 헤더에 토큰을 담아서 보내는 방식은 CSRF 공격에 안전하다.
  • XSS: 악의적인 JS 코드 삽입을 통한 공격이기 때문에 브라우저에서 실행된 악성 자바스크립트가 전역 상태를 통해 접근 가능하다

메모리에 저장하는 방식은 XSS 공격을 당할 수도 있다는 단점이 있기에 이를 보안할 방법들을 설정해주는 것이 좋다.

  1. 짧은 Access Token 주기
    • 탈취되더라도 만료된 토큰의 경우에는 사용이 불가하다
  2. CSP(Content Security Policy) 설정
    - 브라우저가 신뢰된 출처의 스크립트만 실행하도록 제한하므로, 악의적인 스크립트 삽입(XSS)을 예방
    - <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';" />

인가 Authorization

인가(Authorization): 특정 작업을 수행할 수 있는 권한을 가지고 있는지 검사

설계

권한이 필요한 API를 요청하는 것에 대한 설계는 인증 설계 단계에서 api.me.get() 요청을 보내는 것과 동일하다.

하지만 토큰이 만료되었을 경우를 포함한 다른 예시로 다이어그램을 제작하였다.

  1. api.todo.get(_id)를 통해 해당 유저의 todo 데이터를 요청한다.
  2. axios.interceptor를 통해 요청의 헤더에 store에 저장된 access token을 추가한다.
  3. 서버에서는 guard를 통해 헤더에 담긴 access token을 검사하고 유효할 경우 요청을 수락한다.
  4. todo Service에서 해당 유저의 식별자로 저장된 todo 리스트를 DB에서 가져오고
  5. todo Controller는 해당 리스트를 응답한다.

만약 3번에서 access token이 만료되거나 유효하지 않았을 경우, 401 UnauthorizedException를 발생시킨다.

  1. axios.interceptor에서 응답 에러의 코드가 401일 경우, store에 저장된 auth.refreshAccessToken()을 호출한다.
  2. store에서는 서버에 api.auth.refresh() API를 요청한다.
  3. 서버 auth controller에서 쿠키에 담긴 refresh token을 auth service로 전달한다.
  4. auth service에서는 jwt service를 통해 refresh token을 검증하고 유효할 경우, 해당 토큰에 담긴 유저 식별자로 user service에서 유저 정보를 가져온 후 accsss token을 생성한다.
  5. store에서 응답받은 access token을 저장하고 유저 정보를 요청한다.

만약 4번 refreh token이 만료되거나 유효하지 않을 경우, 로그아웃을 시키고 유저를 로그인 페이지로 이동시킨다.

profile
도움이 될 수 있는 개발자

0개의 댓글