새소식

반응형
Front-end

Access Token을 브라우저에 저장 시 고려해야 할 사항들

2024.11.27
  • -
반응형

Unsplash 이미지

 

 

1. 들어가며

프론트엔드 개발 시에 인증과 관련된 작업을 하면서 Best Practice가 없다보니 헤맸던 경험을 많이 했었습니다. 이는 특히 JWT 방식의 인증을 할 때 더욱 심해지는데요. access token을 클라이언트 쪽에서 관리해주게 되면서 여러가지 문제를 마주할 수 있습니다.

 

이에 대한 구현 방식은 정말 프로젝트마다 다르기 때문에 프로젝트 요구사항이나 보안 요구사항에 맞게 구현해 주어야 하는데, 적절한 방법을 골라 사용하기 위해서는 어떤 상황들이 발생할 수 있고 각 상황별 어떤 대처 방법이 있는지 알아야 합니다.

 

더 이상 웹 애플리케이션은 단순한 정적 사이트가 아니라 정적 컨텐츠와 동적 컨텐츠의 복잡한 구성을 이루고 있습니다. 일반적으로 웹 애플리케이션의 모든 로직은 브라우저 단에서 실행된다고 보면 됩니다.(SSR이 아닌 환경에서는)

 

애플리케이션이 서버에서 모든 콘텐츠를 가져오는 대신 브라우저에서 백엔드 API를 통해 가져오는 JavaScript를 실행하고 그에 따라 웹 애플리케이션 UI를 업데이트 하는 방식입니다.

 

데이터에 대한 접근을 보호하기 위해 대부분 OAuth2.0을 사용합니다. OAuth2.0을 사용하면 자바스크립트 애플리케이션에서 보내는 모든 API 요쳥마다 access token을 첨부해 주어야 합니다. 사용성을 위해 자바스크립트 애플리케이션은 일반적으로 access token을 온디맨드 방식으로 요청하지 않고 앱 내에 저장해 놓습니다. 

 

문제는 자바스크립트 내에서 이러한 access token을 어떻게 얻을까요? 그리고 그 토큰을 얻었다고 해도 애플리케이션이 필요할 때 요청에 추가해주어야 하는데 이 토큰을 어디에 저장을 해두어야 할까요?

 

이번 포스팅에서는 브라우저에서 사용할 수 있는 다양한 저장 방식 솔루션에 대해 알아보고 각 선택지에서 발생할 수 있는 여러 보안적 위험 요소에 대해서 알아볼 것입니다. 이러한 위험 요소를 알아본 뒤 OAuth 방식으로 보호되는 API와 통합해야 하는 자바스크립트 애플리케이션에 가장 적합한 브라우저 보안 옵션을 제공하는 패턴을 알아보겠습니다.

 

2. access token을 획득하는 경로

앱에서 access token을 저장하기 위해선 당연히 먼저 access token을 어디선가 얻어와야겠죠? 현재 가장 권장되는 방식 중에 하나는 code flow입니다.

 

code flow는 먼저 사용자로부터 authorization code, 즉 인증 코드를 습득하는 2단계짜리의 프로세스입니다.

 

그런 다음 앱은 back-channel 요청에서 authorization code를 access token으로 교환하는 과정을 거치게 됩니다. 이 요청을 토큰 요청(token request)이라고 하며, 그 예시는 다음과 같습니다.

 

const accessToken = await fetch(OAuthServerTokenEndpoint, {
  method: "POST",
  // authorization code와 PKCE를 포함한 토큰 요청
  // x-www-urlencoded로 인코딩된 데이터를 제출
  body: new URLSearchParams({
    client_id: "example-client",
    grant_type: "authorization_code",
    code: authorization_code,
    code_verifier: pkce_code_verifier,
  }),
})
  // 서버 응답 JSON 객체
  .then((response) => response.json())
  .then((tokenResponse) => {
    if (tokenResponse.accessToken) {
      return tokenResponse.accessToken;
    }
  }).catch(
    // 에러 처리
  )

 

 자바스크립트 코드를 포함하여 브라우저에서 로드한 리소스는 어느 누구나 확인해볼 수 있습니다. 그렇기 때문에 자바스크립트로 구현된 모든 OAtuh 클라이언트는 공개 클라이언트로 간주되며, secret한 데이터를 지키고 있을 수 없으므로 토큰 요청 중에 인증할 수 없습니다.

 

하지만 코드 교환용 증명키(PKCE)는 공용 클라이언트의 코드 흐름을 보호할 수 있는 수단을 제공합니다. 인증 코드와 관련된 위험을 완화하려면 항상 코드 흐름에 PKCE를 적용하는 방식을 고려해볼 수 있습니다.

 

3. 브라우저에 위협을 가하는 요소

Cross-Site Request Forgery(CSRF)

CSRF(크로스 사이트 요청 위조) 공격에서는 악의적인 공격자가 사용자를 속여 브라우저를 통해 의도치 않게 악성 요청을 수행하도록 합니다.

https://www.guardrails.io/blog/what-is-csrf-and-how-do-you-prevent-it/

 

예를 들어, 공격자는 사이트에서 조작된 이미지에 악성 링크를 삽입하여 브라우저가 우리 서비스의 API GET 요청을 실행하도록 유도하거나 악성 웹사이트에 양식을 추가하여 POST 요청을 실행하도록 유도할 수 있습니다.

 

어떤 경우든지 브라우저는 이러한 요청에 싱글사인온(SSO) 쿠키를 포함한 모든 쿠키를 자동으로 추가하게 됩니다.

  • 공격자는 일반적으로 악의적인 요청에 사용자의 인증된 세션을 사용하는 메커니즘이기 때문에 "session riding"이라고 하기도 합니다. 

 

그렇기 때문에 공격자는 사용자를 대신하여 몰래 요청을 수행하고 사용자가 호출할 수 있는 모든 엔드포인트를 호출할 수 있습니다. 하지만 공격자는 그 응답을 읽을 수는 없으므로 일반적으로 사용자의 비밀번호 업데이트와 같은 일회성 상태 변경 요청을 노립니다.

 

다들 SNS의 계정에 광고성 게시물이나 유해한 게시물이 올라간 경험을 해보신적 있을 것입니다. 해킹과 유사하지만 게시물만 올려져 있다는 점에서 조금 다를 수 있는데요. 이 경우, 해당 SNS에 로그인 되어 있는 상태에서 악성 링크를 잘못 클릭하여 "게시물 작성" 요청이 전송되어 결과적으로 해당 사용자 계정에 공격자가 작성한 광고나 악성 링크가 포함된 게시물이 올라갈 수 있게 되었던 것입니다.

 

 

Cross-Stie Scripting(XSS)

크로스 사이트 스크립팅(XSS)은 공격자가 신뢰할 수 있는 웹사이트에 악성 클라이언트 사이드 코드를 주입할 수 있도록 허용하는 취약점을 말합니다. 이러한 취약점은 웹 애플리케이션에서 사용자 입력을 통해 생성된 출력이 제대로 검증되지 않을 때 발생할 수 있습니다. 브라우저는 이러한 악성 코드를 신뢰할 수 있는 웹사이트의 일부로 간주하고 자동으로 실행합니다.

 

XSS 공격은 access token이나 refresh token을 훔치거나 CSRF 공격을 수행하는 데 사용될 수 있습니다. 하지만 이러한 공격은 토큰의 유효 기간 동안이나 취약점이 있는 브라우저 탭이 열려 있는 동안과 같이 제한된 시간 내에서만 실행될 수 있다는 시간적 제약이 있습니다.

 

XSS를 이용해 직접적으로 access token을 가져오는 것이 불가능한 경우에도, 공격자는 session riding 기법으로 XSS 취약점을 악용할 수 있습니다. 이를 통해 인증된 요청을 보호된 웹 엔드포인트로 보낼 수 있으며, 공격자는 사용자를 가장해 사용자가 접근할 수 있는 모든 백엔드 엔드포인트를 호출할 수 있는데, 이로 인해 심각한 피해를 초래할 수 있습니다.

 

4. 브라우저 저장 솔루션

애플리케이션이 access token을 받으면, API 요청에 사용하기 위해 이를 저장해야 합니다. 브라우저에서 데이터를 저장하는 방법에는 여러 가지가 있습니다.

 

Web Storage API나 IndexedDB 같은 전용 API를 사용해 토큰을 저장할 수도 있고, 단순히 메모리에 저장하거나 쿠키에 보관할 수도 있습니다. 일부 저장 방식은 데이터를 영구적으로 유지하지만, 다른 방식은 일정 시간이 지나거나 페이지가 닫히거나 새로고침되면 데이터가 삭제됩니다.

 

어떤 저장 방식은 탭 간 데이터를 공유할 수 있는 반면, 어떤 방식은 현재 탭에만 국한되는 것도 존재합니다. 하지만 여기서 설명하는 대부분의 방식은 origin(출처) 단위로 데이터를 저장하는 것과 관련되어 있습니다. 그래서 관련 논의를 위해 originsite의 개념을 이해하면 좋습니다.

 

Origin(출처)은 웹 리스스의 URL에서 스킴(https 등), 호스트명(도메인), 그리고 포트를 조합한 것을 의미합니다. 예를 들어, https://example.com/number/onehttps://example.com:80/path/two 는 스킴(https), 호스트명(example.com),  포트(기본 포트)가 동일하므로 동일한 Origin을 가졌다고 표현합니다. 이들의 Origin은 https://example.com 입니다.  반면에 https://example.com:8443 이나 https://this.example.com 은 포트 또는 호스트명이 다르기 때문에 서로 다른 Origin 으로 취급합니다.

 

반면, Site(사이트)는 Origin 보다는 더 큰 개념으로, 여러 리소스를 포함하는 웹 애플리케이션 전체를 나타냅니다. 간단히 말해, Site는 스킴과 도메인 이름으로 정의됩니다. 예를 들어, https://example.com 은 하나의 Site입니다. https://example.comhttps://this.example.com:8443 는 호스트명과 포트가 달라 서로 다른 Origin을 갖지만, 동일한 도메인(example.com)과 스킴(https)을 사용하므로 동일한 Site로 취급하는 것입니다.

 

Local Storage(로컬 스토리지)

로컬 스토리지는 JavaScript의 전역 객체인 localStorage를 사용해 Web Storage API를 통해 접근할 수 있습니다. 로컬 스토리지에 저장된 데이터는 브라우저 탭과 세션 간에 공유되며, 브라우저를 닫아도 삭제되지 않고 유지됩니다. 즉, 데이터에 유효 기간이 없으며, 사용자가 직접 삭제하지 않는 한 지속적으로 남아있습니다.

 

이러한 특성 때문에, 애플리케이션의 모든 탭에서 로컬 스토리지에 저장된 데이터에 접근할 수 있습니다. 이러한 편리함으로 인해 토큰을 로컬 스토리지에 저장하고 싶어하는 개발자가 많고 실제로 많은 프로젝트들에서도 로컬 스토리지를 access token 저장소로 활용하고 있습니다.

 

하지만 유수의 서비스들을 돌아다녀보면 local storage에 토큰을 저장하는 서비스는 많이 보이지 않습니다. 왜 그럴까요?

 

// access token 저장!
localStorage.setItem("token", accessToken);
// access token 불러오기!
let accessToken = localStorage.getItem("token");

 

해당 경우, 애플리케이션이 API를 호출할 때, 토큰을 로컬 스토리지에서 가져와 요청에 수동으로 추가할 것입니다. 그러나 로컬 스토리지는 JavaScript를 통해서 접근 가능하기 때문에, 이 방식은 크로스 사이트 스크립팅(XSS) 공격에 취약합니다.

 

만약 로컬 스토리지를 사용해 access token을 저장하고, 공격자가 애플리케이션 내에서 외부 자바스크립트 코드를 실행할 수 있다면, 공격자는 토큰을 탈취하여 직접 API를 호출할 수 있습니다. 더 나아가, XSS는 공격자가 애플리케이션의 로컬 스토리지 데이터를 조작하도록 허용하며, 이는 토큰을 강제로 변경시킬 수 있다는 것을 의미하기도 합니다.

 

로컬 스토리지에 저장된 데이터는 영구적으로 저장된다는 점을 유념해야 합니다. 즉, 로컬 스토리지에 저장된 모든 토큰은 사용자의 장치(노트북, 컴퓨터, 모바일 등)의 파일 시스템에 남아 있고, 브라우저를 닫더라고 다른 애플리케이션에서 해당 데이터에 접근할 수 있는 상황이 벌어질 수 있습니다.

 

따라서 로컬 스토리지를 사용할 경우, 엔드포인트 보안을 고려해야 하며, 악성 소프트웨어, 디바이스 도난, 디스크 탈취 등 브라우저 외부의 공격 벡터에 대비하고 보호해야 합니다.

 

이 경우 아래와 같은 권장 사항을 따를 수 있습니다.

  • 로컬 스토리지에 토큰과 같은 민감한 데이터를 저장하지 않도록 합니다.
  • 로컬 스토리지의 데이터를 신뢰하지 마세요.(특히 인증 및 인가와 관련된 데이터는 더더욱 주의)

 

Session Storage(세션 스토리지)

세션 스토리지는 Web Storage API에서 제공하는 또 다른 메커니즘의 저장 방식입니다.

 

로컬 스토리지와는 달리 sessionStorage 객체를 사용해 저장된 데이터는 탭이나 브라우저가 닫히면 삭제가 됩니다. 그리고 세션 스토리지에 저장된 데이터는 다른 탭에서 접근할 수 없습니다. 현재 탭과 동일한 출처(origin)의 자바스크립트 코드만 세션 스토리지를 통해 데이터를 읽고 쓸 수 있습니다.

 

세션 스토리지는 창이 닫히면 브라우저가 자동으로 모든 토큰을 제거하기 때문에, 토큰이 디바이스에 남아있지 않아 로컬 스토리지보다는 안전하다고 볼 수 있습니다. 또한 세션 스토리지는 탭 간의 공유되지 않는다는 특성으로, 공격자가 다른 탭(또는 창)에서 토큰을 읽을 수 없으며, 이는 XSS 공격의 영향에서 멀어질 수 있습니다.

 

하지만 실제로 세션 스토리지를 사용하여 토큰을 저장할 때 주된 보안 문제는 여전히 XSS입니다. 애플리케이션 자체가 XSS에 취약한 경우, 공격자는 아직 탭을 닫지 않은 사용자의 스토리지에서 토큰을 탈취하여 API 호출에 재사용할 수 있습니다. 따라서 세션 스토리지도 토큰과 같은 민감한 데이터를 저장하기에 완전히 적합하지는 않은 것입니다.

 

IndexedDB

IndexedDB는 Indexed Database API의 약자로, 브라우저에서 더 많은 데이터를 비동기적으로 저장할 수 있도록 설계된 API입니다. 그러나 토큰을 저장할 때는 이 API가 제공하는 기능과 용량까지는 필요하지 않은 경우가 대부분일 것입니다. 애플리케이션은 API 호출마다 토큰을 전송하므로, 토큰 크기를 최소화하는 것이 좋습니다.

 

지금까지 언급된 다른 클라이언트 측 저장 메커니즘과 마찬가지로, Indexed Database API를 통해 저장된 데이터 접근도 동일 출처 정책(same-site-origin policy)에 의해 제한됩니다. 동일한 출처의 리소스와 서비스 워커만 데이터에 접근할 수 있는 것이죠.

 

보안 관점에서 IndexedDB는 로컬 스토리지와 사실 비슷합니다.

  • 토큰이 파일 시스템을 통해 유출될 수 있습니다.
  • 토큰이 XSS 공격을 통해 유출될 수 있습니다.

따라서 IndexedDB에 access token이나 기타 민감한 데이터를 저장하지 않도록 해야 합니다.

 

그리고 IndexedDB는 이미지와 같이 애플리케이션이 오프라인에서 동작하는 데 필요한 데이터를 저장하는 데 더 적합합니다.

 

In Memory(메모리 상)

토큰을 저장하는 비교적 안전한 방법은 바로 메모리에 저장하는 것입니다. 다른 저장 방식에 비해 토큰이 애초에 파일 시스템에 저장되는 것이 아니기 때문에  디바이스 파일 시스템과 관련된 위험이 줄어듭니다.

 

소위 Best Practices라고 하는 방식은 메모리에 토큰을 저장할 때 클로저(closure)를 사용하는 것입니다. 예를 들어, API를 호출할 때 사용할 토큰을 별도의 메서드 안에 정의하여 메인 애플리케이션(메인 스레드)에 토큰을 노출하는 일이 없도록 하는 방식입니다.

 

아래의 예시는 자바스크립트로 메모리에서 토큰을 처리하는 방법을 보여줍니다.

function protectedCalls(tokenResponse) {
  const accessToken = tokenResponse.accessToken;
  return {
    // access token을 담아 API 호출
    getOrders: () => {
      const req = new Request("https://server.example/orders");
      req.headers.set("Authorization", accessToken);
  	 return fetch(req)
    }
  }
}
const apiClient = protectedCalls(tokenResponse);
// 보호된 API 호출
apiClient.getOrders();

 

그런데 한 가지 주의사항이 있습니다.

 

공격자가 토큰을 직접적으로 공격하지 못하기 때문에, 토큰을 이용하여 API를 바로 호출하는 것이 불가능하긴 합니다. 하지만 공격자가 토큰을 참조하는 apiClient(custom axios, fetch..)를 이용한다면 언제든 API를 호출하는 것이 가능해집니다.

 

이러한 공격은 브라우저 탭이 열려있는 시간과 인터페이스에서 제공되는 함수의 scope로 제한됩니다.

 

메모리에 토큰을 저장할 때의 단점

 

XSS 취약성과 관련된 보안 문제 외에도, 메모리에 토큰을 저장하면 페이지가 새로고침될 때 토큰이 사라지는 사용자 경험(UX)의 큰 단점이 있습니다. 애플리케이션은 새로운 토큰을 다시 받아와야 하며, 이는 사용자 인증 과정을 다시 요구할 수 있습니다. 안전한 설계는 사용자 경험도 고려해야 합니다.

 

서비스 워커를 활용한 개선된 설계

 

서비스 워커를 활용하면 메인 웹 페이지와 분리된 별도의 스레드에서 토큰 처리 기능을 실행하여 UX 문제를 완화시킬 수 있습니다. 서비스 워커는 애플리케이션, 브라우저, 네트워크 사이에서 프록시 역할을 해줍니다. 이를 통해 요청과 응답을 가로채 데이터를 캐싱하거나 오프라인 접근을 지원할 수 있으며, 토큰을 가져오거나 추가할 수도 있습니다.

 

추가적인 고려사항

 

자바스크립트 클로저나 서비스 워커를 사용해 토큰과 API 요청을 처리하는 경우, 공격자는 OAuth 플로우(예: callback 또는 silent flow)를 노림으로써 토큰을 탈취를 하려고 할 수 있습니다. 공격자는 서비스 워커를 등록 해제하거나 우회할 수 있으며, window.fetch 같은 메서드를 덮어 씌워서 실시간으로 토큰을 읽어내는 방법("read the token on the fly") 또한 사용할 수 있습니다.(prototype pollution)

 

따라서 자바스크립트 클로저와 서비스 워커는 편의성을 위한 도구로는 적합하지만, 보안상의 완벽한 해결책으로 간주해서는 안 됩니다.

 

Cookie (쿠키)

쿠키는 브라우저에 저장되는 데이터 중 하나입니다. 설계적으로 브라우저는 쿠키를 서버로의 모든 요청에 자동으로 추가되어 요청과 함께 보내지는 요소입니다. 따라서 애플리케이션은 쿠키를 사용할 때 굉장히 신경을 써서 보내 주어야 합니다.

 

설정들이 잘 설정되지 않으면 브라우저가 Cross Site 요청에 쿠키를 추가할 수 있으며, 이 경우가 바로 앞서 설명한 CSRF 공격에 취약한 상황인 것입니다.

 

쿠키는 보안 속성을 제어하는 여러 속성을 가지고 있습니다.

 

예를 들어, Samesite 속성은 CSRF 공격의 위험을 줄이는 데 도움이 됩니다. 쿠키에 SameSite 속성이 Strict로 설정되어 있으면, 쿠키가 생성된 사이트와 동일한 사이트에서 발생하고 동일한 사이트를 대상으로 하는 요청에만 쿠키를 추가합니다. 브라우저는 "링크"와 같이 다른 사이트에 포함된 요청에서는 쿠키를 더 이상 추가하지 않습니다.

 

쿠키 역시 자바스크립트를 통해 설정하고 읽을 수 있습니다. 그러나 자바스크립트로 쿠키를 읽을 수 있는 것이 가능하면 애플리케이션이 CSRF 외에도 XSS 공격에까지 취약해집니다. 따라서 쿠키를 설정할 때는 백엔드 구성 요소를 통해 쿠키를 설정하고, 이 때 HttpOnly 옵션을 설정하는 것이 매우 권장됩니다. 이 HttpOnly 옵션은 브라우저가 쿠키를 자바스크립트를 통해 접근할 수 없도록 보호하여 XSS 공격을 통해 데이터 유출 위험을 줄여줍니다.

 

쿠키가 중간자 공격(Man-in-the-Middle Attack)을 통해 유출되어 세션 탈취로 이어지는 것을 방지하려면, 쿠키는 암호화된 연결(HTTPS)을 통해서만 전송되어야 합니다. 브라우저에 HTTPS 요청에서만 쿠키를 전송하도록 지시하려면, 쿠키에 Secure 속성을 설정해야 합니다.

Set-Cookie:token=myvalue;SameSite=Strict;Secure;HttpOnly

 

브라우저에서 사용하는 다른 영구 저장 방식과 마찬가지로, 쿠키는 브라우저를 닫아도 파일 시스템에 남아 있을 수 있습니다. (예: 쿠키에 만료 시간이 설정되지 않았거나, 브라우저가 세션 복구 기능의 일부로 세션 쿠키를 유지할 수 있음).

 

그리고 파일 시스템에서 토큰이 유출되는 위험을 줄이기 위해, 쿠키에는 암호화된 토큰만 저장해야 합니다. 따라서 백엔드 쪽에서 Set-Cookie 헤더를 통해 암호화된 토큰만 반환하도록 해야 합니다.

 

Threat Matrix (위험 요소 표)

다음 표는 브라우저에서 사용하는 저장 솔루션의 위협 평가를 요약한 것으로, 주요 위협 벡터는 빨간색으로 표시되어 있습니다. 주황색 위협은 웹 기술만으로는 완전히 방지할 수 없으며 추가적인 대책이 필요합니다. 초록색 위협은 적절한 설정을 사용하면 성공적으로 제거할 수 있는 위협을 나타냅니다.

 

공격자가 토큰을 훔치는 데 성공하면, 그 토큰이 유효한 동안 사용자나 애플리케이션과는 독립적으로 access token을 사용할 수 있습니다.

 

만약 공격자가 refresh token을 탈취한다면, 공격을 상당히 오래 지속할 수 있으며 피해 규모를 더욱 키울 수 있습니다. 이는 access token을 갱신할 수 있기 때문입니다. 심지어 해커는 공격을 JavaScript 애플리케이션에서 사용하는 API뿐만 아니라 다른 API로도 확장할 수 있습니다. 예를 들어, 해커는 액세스 토큰을 재사용(리플레이)하고, 다른 API의 취약점을 악용할 수도 있습니다.

 

훔친 access token은 심각한 피해를 초래할 수 있으며, XSS는 여전히 웹 애플리케이션의 주요 보안 문제로 남아 있습니다. 따라서 클라이언트 코드에서 접근 가능한 위치에 액세스 토큰을 저장하는 것을 피해야 합니다. 그렇기에 access token을 쿠키에 저장하는 것이 권장됩니다. 

 

적절한 속성으로 설정된 경우, 브라우저는 쿠키를 통해 access token이 유출될 일이 없습니다. 이 경우 XSS 공격은 동일한 사이트에서의 세션 라이딩(session-riding) 공격과 유사하게 간주될 수 있습니다.

 

 

쿠키를 사용한 OAuth 시맨틱

쿠키는 아직까지도 토큰을 전달하고 API 자격 증명 역할을 수행하는 데 가장 적합한 방법입니다. 왜냐하면, 공격자가 XSS 취약점을 악용하는 데 성공하더라도, 제대로 설정된 쿠키에서는 access token을 탈취할 수 있는 방법이 없기 때문입니다.

 

하지만, 이를 가능하게 하려면 쿠키가 올바르게 설정되어야 합니다.

 

첫째, 쿠키에 HttpOnly 속성을 설정하여 JavaScript를 통해 접근할 수 없도록 해야 합니다. 이를 통해 XSS 공격의 위험을 줄일 수 있습니다. 또 다른 중요한 속성은 Secure 플래그로, 이 플래그를 설정하면 쿠키가 HTTPS 연결을 통해서만 전송되도록 보장하여 중간자 공격(man-in-the-middle attacks)을 방지할 수 있습니다.

 

둘째, 수명이 짧은 액세스 토큰을 발급해야 합니다. 몇 분 동안만 유효한 토큰을 사용하는 것이 좋습니다. 최악의 경우에도 짧은 수명을 가진 토큰은 제한된 시간 동안만 악용될 수 있습니다. 일반적으로 15분의 유효 기간이 적합하다고 여겨집니다. 쿠키와 토큰의 만료 시간을 비슷하게 설정하는 것이 좋습니다.

 

셋째, 토큰을 민감한 데이터로 간주해야 합니다. 쿠키에는 암호화된 토큰만 저장해야 합니다. 만약 공격자가 암호화된 토큰을 탈취하더라도, 데이터를 해독할 수 없고 다른 API로 재사용(리플레이)할 수도 없습니다. 다른 API는 해당 토큰을 복호화할 수 없기 때문입니다. 암호화된 토큰은 탈취된 토큰의 영향을 줄이는 데 도움을 줍니다.

 

넷째, API 자격 증명을 언제 보낼지 제한해야 합니다. 쿠키는 API 자격 증명이 필요한 리소스에만 전송되도록 해야 합니다. 이를 위해 브라우저가 실제로 액세스 토큰이 필요한 API 호출에만 쿠키를 추가하도록 설정해야 합니다. 이를 위해 SameSite=Strict, API 엔드포인트의 도메인을 지정하는 도메인 속성, 경로(path) 등의 적절한 설정이 필요합니다.

 

마지막으로, refresh token을 사용할 때는 이를 별도의 쿠키에 저장해야 합니다. refresh token은 모든 API 요청과 함께 전송될 필요가 없기 때문에, 이를 방지하기 위해 설정을 확인해야 합니다. 리프레시 토큰은 만료된 액세스 토큰을 갱신할 때만 추가되어야 합니다. 따라서 리프레시 토큰이 저장된 쿠키는 액세스 토큰이 저장된 쿠키와 약간 다른 설정을 가져야 합니다.

 

 

Token Handler Pattern(토큰 핸들러 패턴)

토큰 핸들러 패턴은 JavaScript 클라이언트에서 OAuth의 모범 사례 원칙을 통합한 디자인 패턴입니다. 이 패턴은 OAuth 2.0 for Browser-Based Apps에서 설명된 백엔드 포 프론트엔드(BFF) 접근 방식을 따릅니다. 패턴은 암호화된 토큰과 필요한 속성을 가진 쿠키를 발급할 수 있는 백엔드 구성 요소를 도입합니다.

 

백엔드 구성 요소의 역할:

 

1. OAuth 클라이언트로 인증 서버와 상호작용하여 사용자 인증을 시작하고 토큰을 가져옵니다.

2. JavaScript 애플리케이션을 위한 토큰을 관리하여 토큰이 접근할 수 없도록 유지합니다.

3. 모든 API 요청을 프록시 및 가로채기하여 적절한 액세스 토큰을 첨부합니다.

 

토큰 핸들러 패턴은 브라우저에서 실행되는 애플리케이션을 위해 OAuth를 추상화하는 BFF를 정의합니다. 즉, 토큰 핸들러 패턴은 JavaScript 애플리케이션이 사용자 인증을 수행하고 API에 안전하게 인증된 호출을 할 수 있도록 하는 API를 제공합니다. 이를 위해 패턴은 쿠키를 사용하여 액세스 토큰을 저장하고 전송합니다.

 

토큰 핸들러는 두 가지 주요 구성 요소로 이루어져 있습니다:

 

1. OAuth 에이전트: 인증 서버에서 토큰을 가져오기 위해 OAuth 플로우를 처리합니다.

2. OAuth 프록시: 모든 API 요청을 가로채고 쿠키를 토큰으로 변환합니다.

 

 

OAuth 에이전트가 토큰을 가져온 후, 다음 속성을 가진 쿠키를 발급합니다:

  • SameSite=Strict
  • HttpOnly
  • Secure
  • API 경로(Path)

 

백엔드 구성 요소의 특성

 

토큰 핸들러가 백엔드 구성 요소이기 때문에, OAuth 에이전트는 인증 서버에 대해 인증할 수 있는 비공개 클라이언트(confidential client)입니다(일반 JavaScript 클라이언트는 공개 클라이언트입니다). 이는 토큰을 얻기 위해 OAuth 에이전트가 인증을 해야 한다는 것을 의미합니다. 결과적으로, 공격자가 새 토큰을 성공적으로 얻으려면 클라이언트 자격 증명을 확보해야 하는 것이죠.

 

클라이언트 자격 증명 없이 JavaScript에서 Silent Flow를 실행하는 것은 실패합니다.

 

배포 요건

 

토큰 핸들러 패턴이 제대로 작동하려면, JavaScript 애플리케이션과 토큰 핸들러 구성 요소가 동일한 사이트에 배포되어야 합니다(즉, 동일한 도메인에서 실행되어야 합니다). 그렇지 않으면, 쿠키의 SameSite 제한 때문에 브라우저가 API 요청에 토큰 쿠키를 추가하지 않습니다.

 

데이터 가져오기

 

JavaScript 애플리케이션은 OAuth 프록시를 통해 API를 호출하면 됩니다.

// http://www.example.com/app.js
// Call to OAuth Proxy
const response = await fetch("https://api.example.com/orders", {
	// 브라우저에게 cross-origin 요청에 쿠키를 보내라고 지시
	credentials: "include"
});

브라우저는 요청에 자동으로 쿠키를 추가합니다. 위 예시에서 브라우저는 크로스 오리진 요청에도 쿠키를 포함합니다. 그러나 SameSite=Strict 속성 덕분에, 브라우저는 동일한 사이트(동일한 도메인)에서 발생한 크로스 오리진 요청에만 쿠키를 추가합니다.

 

OAuth 프록시는 쿠키를 복호화하고, 토큰을 업스트림 API에 추가합니다. 쿠키 속성은 브라우저가 HTTPS 요청에서만 쿠키를 추가하도록 보장하여 전송 중 보안을 유지합니다. 또한, 토큰이 암호화되어 있으므로 저장된 상태에서도 안전합니다. 이후, 토큰은 API에 안전하게 접근하기 위해 사용됩니다.

 

 

5. 결론

모던한 API 요청은 OAuth와 액세스 토큰을 사용하는 것이 가장 안전합니다.

 

하지만 자바스크립트 애플리케이션은 보안에 취약하다는 치명적인 단점이 있습니다. 이 글을 읽어보셨다면 알겠지만 브라우저에 토큰을 저장할 수 있는 안전한 솔루션이 그렇게 많이 없습니다. 사용 가능한 모든 솔루션은 대부분 XSS에 취약합니다.

 

따라서 모든 애플리케이션 보안의 최우선 순위는 XSS 취약점을 방지하는 것이어야 합니다.

 

토큰 핸들러 패턴은 JavaScript에서 사용할 수 없는 쿠키에 암호화된 토큰을 저장하여 XSS 위험을 완화합니다. 이 패턴은 웹에서의 문제점과 API의 문제점을 분리하고 웹 아키텍처를 손상시키지 않으면서도 잘 정립된 웹 기술을 통해 자바스크립트 애플리케이션을 강화하기 위한 여러가지 가이드를 제공하고 있습니다.

 

토큰 핸들러 패턴에 대한 자세한 설명을 확인하고 다양한 예제를 살펴보면 더 도움이 될 것 같습니다.

 

 

 

Token Handler Design Overview

A design overview of the key behavior when using the token handler pattern

curity.io

 

 

The Token Handler Pattern for Single Page Applications | Curity

Learn how to secure an SPA using an API-driven Backend for Frontend, for the best all-round architecture

curity.io

 

 

 

Curity I/O

Curity I/O is an identity company that specializes in OAuth and OpenID Connect - Curity I/O

github.com

 

 

반응형

'Front-end' 카테고리의 다른 글

BFF란 무엇인가  (0) 2024.11.25
React에도 디자인 패턴이 있다고?  (3) 2024.11.24
Headless란 무엇인가?  (1) 2024.11.23
[TypeScript] enum vs. const  (0) 2024.11.22
axios vs. fetch  (0) 2024.11.21
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.