※ Back-end내용에 대한 이해가 매우 부족할 수 있습니다. 가볍게 읽어주세요~
오늘의 공부거리들
1. Phaser.js 를 위한 준비단계 > 포스팅 #1 #2
2. 노마드코더 Typescript challenge day-2 > 포스팅
3. Urclass 해싱 & 토큰 인증방식 내용 > 현재 포스팅
오늘의 학습내용
- 해싱
- 토큰
- 토큰인증방식 실습
#1 해싱
사실... 2000자 넘게 공들여 쓴 해싱관련 글이 날아가버린 일이 발생했었기에 제대로 쓸 수 있을지 모르겠지만,,, 극심한 멘붕을 딛고 최대한 복구해보겠다
(1) 해싱의 개념
해싱이란, 컴퓨터 과학에서 데이터를 빠르게 검색하거나 비교하기 위해 사용되는 기술이다. 사용자가 입력한 임의의 길이의 데이터를 미리 정해둔 고정된 크기(길이)의 문자로 변환하는 프로세스를 말한다. 이 때, 출력되는 출력데이터를 해시 다이제스트(hash digest)또는 체크섬(checksum)라고 한다. 입력 데이터는 이번에 암호파트여서 비밀번호를 해싱하는 경우를 학습하는데 암호 또는 메시지 심지어 파일도 입력될 수 있다.
(2) 해시함수
해싱은 해시함수를 이용하여 이루어지는데, 해시함수는 입력 데이터를 가지고 고정 크기의 해시 값을 생성하는 수학적 알고리즘으로써 다음과 같은 속성을 가져야 한다.
1) Deterministic : 동일한 입력 데이터를 주면 항상 같은 해시 값이 생성되어야 함.
2) One-way(단방향성) : 해시 값으로 원본 입력 데이터를 얻는 것이 계산적으로 불가능해야 함.
3) Uniform(균일함) : 해시 공간에 고르게 분포된 해시 값을 생성해야 한다. 해시 함수로는 MD5, SHA-256(이번에 간단하게 실습해 본 함수) ... 등 여러가지가 있는데 일부 함수는 균일함이 부족하여 안전하지 않은 것으로 간주된다. 이게 무슨말인지 모르겠어서 ChatGPT에게 물어본 결과 답변은 이러하였다. 내맘대로 번역을 주의해서 원문을 읽으시길 추천합니다.
It means that the hash function should produce hash values that are evenly distributed across the range of possible hash values. In other words, if we imagine a hash function that outputs values between 0 and 999, a uniform hash function would produce approximately the same number of hash values for each possible output value between 0 and 999.
앞서 해시공간이라는 것은 출력될 수 있는 값의 범위를 말한다. 예를들어 해시함수를 통해 출력되는 값이 0부터 999라고 할 때 무작위 값을 넣었을 때 결과값이 한 쪽에 치우치지 않고 고르게 출력될 수 있도록 만들어야 한다는 것이다.
Why is uniformity important? Well, in many applications of hashing, we want to use the hash value to assign data to different buckets or locations. For example, in a hash table data structure, we might use the hash value of a key to determine which bucket in the table the key's associated value should be stored in. If the hash function is not uniform, some buckets may end up with more data than others, which can cause performance issues or other problems.
해시 응용 프로그램을 활용할 때, 해시 값을 저장하려고 할 수 있는데, 그 과정에서 특정 값 부근에 데이터가 몰릴 경우 테이블을 저장할 공간이 어떤 공간은 비어있고 어떤 공간은 할당된 저장공간보다 초과되어 저장이 안되는 문제가 발생할 수 있다.
For example, imagine a hash function that always outputs the same hash value for all input data. If we use this hash function in a hash table, all keys will be assigned to the same bucket, which defeats the purpose of using a hash table in the first place! On the other hand, if a hash function is perfectly uniform, each possible hash value will be assigned roughly the same number of keys, which is ideal.
극단적으로, 모든 입력 값에 대해 동일한 해시를 생성하는 함수를 생각해보면 모든 인풋에 대해 동일한 값을 해시로 저장한다면 애초에 해시 테이블을 사용할 이유가 없어진다. 가장 이상적인 것은 입력 값의 수와 동일한 수의 키가 할당되는 것인데 해시 함수가 완벽하게 균일한 상태일 때 가능하다.
Achieving perfect uniformity is difficult, if not impossible, in practice. However, good hash functions strive to be as uniform as possible, while also satisfying other important properties such as being deterministic and one-way. There are many techniques for designing and evaluating hash functions, including statistical tests and cryptographic analysis. By carefully selecting and testing hash functions, we can ensure that they are suitably uniform for their intended applications.
(3) 해싱의 특징
1) 단방향성 : 해시 함수에서 설명하였듯이 해시 값으로부터 원본 값을 알아내는 것은 불가능에 가깝다. 암호와 관련하여서는 이를 활용하여 비밀번호(해시)를 저장한다. 인증이 필요한 시스템에서는 비밀번호 자체가 아니라 비밀번호를 해싱한 값을 저장해둔다. 사용자가 로그인을 시도할 때 시스템은 입력한 암호를 해시하고 저장된 해시 값과 비교한다. 해시가 일치하는 경우 시스템은 실제 암호를 저장하거나 노출하지 않고 사용자를 인증할 수 있다. 송수신 중에 해시 값을 탈취당하더라도 비밀번호 자체를 추적해내기 힘들기 때문에 단방향성이 가지는 보안상 이점이 여기에 있다. 심지어, 서버가 해킹당해서 해시 값이 대량으로 탈취되어도 비밀번호를 알기 어려울 것이다. 이에 따라 원본 입력을 직접 저장하거나 노출할 필요 없이 빠르고 안전한 데이터 조회, 비교 및 검증을 수행할 수 있는 장점이 있다.
(4) 레인보우 테이블
그런데 오히려 이런 단방향성을 역이용해서 해킹을 시도하려는 노력이 있었으니, 레인보우 테이블이다. 레인보우 테이블에는 브루트 포스(brute force)라는 기술이 사용되는데 이 브루트 라는 단어는 다소 무식한 단어이다. BFS(완전탐색)에도 사용되는 단어로 일일히 하나하나 수작업을 통해 결과물을 만드는 과정인 것이다. 그런데, 레인보우 테이블을 찾아보니 단순히 password : hash digest 이런 식으로 1대1 대응표를 만드는 것을 넘어서 더욱 압축하는 처리를 하는 과정을 거친 것임을 알 수 있었다.(암호학에 조예가 깊어지고 싶은 분이 아니라면 링크를 보는 것을 개인적으로는 별로 추천하지는 않는다... 소제목에 있는 링크는 매우 읽을만하다,, 괜찮다)
대략적인 내용은 이렇다.
패스워드와 그에 대응되는 해시 값을 거의 하드코딩을 통해 1대1 표로 만든 것을 look-up table이라고 한다. 이 look-up table을 reduce 함수를 통해서 재가공하여 압축하는 것이 진정한 의미의 레인보우 테이블이다.
그런데 사실 이 룩업테이블이란 것도 해시의 길이를 보면 알겠지만 비밀번호가 8자리만 되어도 가능한 경우의수를 제곱으로 곱하게 되면 메모리 용량 때문에 사실상 보유하고 있기가 힘들다. 그래서 고안된 기술이 레인보우 테이블이다.
레인보우 테이블은 해시 값을 plaintext(평문이라고 하겠다.)로 축소하는 리듀스 함수(R())를 사용한다. 잠깐만, 그런데 이 리듀스함수는 원본 값을 알아내는 것은 아니고 해시 값을 편리하게 저장하기 위한 방법이라고 보면 된다. 단순한 예로 위의 그림에서 Stephen의 해쉬 값이(솔트가 없는 기준으로) 39e717cd3f5... 이렇게 진행 될 때 6개의 앞에 배치된 숫자만 취하는 것을 예로 들 수 있다. R(39e717cd3f5...) -> 397173 이런 식으로 결과값을 얻을 수 있는 것이다.
물론 이 방법도 일일히 비밀번호를 입력하고 그에 대한 해쉬 값을 얻는 과정이 수반되는 것은 동일하다. 다만, 저장용량에 있어서 수백만개의 해쉬값을 리듀스 함수를 6번정도 거치면 단 한 단어의 텍스트로 압축이 된다고 하니,,,(진짜...?) 효율성은 매우 좋다는 것을 알 수 있다.
(5) 솔팅
위와 같이 메모리 문제도 해결해버린 레인보우 테이블의 등장으로 해시 값의 유출도 위험해질 수 있는 상황이 발생하자 그에 대한 대응으로 솔팅이라는 것을 하게되었다. 솔팅이란 음식에 소금으로 조미하여 전혀 다른 맛을 내듯이 비밀번호와 임의의 조미료(텍스트)를 조합하여 한 번 꼬여있는 해쉬값을 생성하는 것이다. 이 때 조미료가 어떤 것이 들어간지 알기가 사실상 불가능하기 때문에 같은 패스워드에서도 다른 솔트값이 들어가면 다른 해시가 나오기 때문에 설령 솔트값을 한 두번 탈취 당해서 룩업테이블을 만들려고해도 다음에 도착하는 해시 값에 어떤 솔팅이 되어있는지 모른다면 룩업테이블의 자료로 활용할 수가 없다. 직접 만들려고해도 패스워드 경우의 수 하나 당 솔팅된 모든 경우의수를 곱한 만큼 더 복잡해지기 때문에 보안상 이점이 매우 커진다.
(6) 간단한 해싱실습
링크에서 해싱에 대한 실습을 해 볼 수 있다. 아무런 암호나 입력해보면 함수에 따라 해싱을 해주는 것을 알 수 있다.
긴 해쉬 문자로 변환된 것을 확인할 수 있다. 여기에 솔트를 첨가해서 다시 생성해보면,
이렇게 또 완전 다른 해시 값이 주어지는 것을 알 수 있다. 첨부하지는 않겠지만 솔팅값을 바꿀 때마다 같은 패스워드에서도 전혀 다른 해시 값이 나오는 것을 확인할 수 있었다.
(7) 솔팅을 해도 위험하다?
https://twitter.com/billatnapier/status/1156127760584232960
트위터에서 즐기는 Prof B Buchanan OBE
“Be worried about the power of the Cloud to crack hashed passwords ... the threat is not rainbow tables anymore, but Hashcat rules. You say that you "salt" you are passwords, but the salt is their with the hashed password.”
twitter.com
이러한 의견의 트윗도 있었는데, 원래 이 글에 대해 주절주절 썼었는데 내가 암호학 전공도 아니고 이 의견에 대한 토론이 이루어지는데도 잘 이해하지 못하겠어서 링크만 남긴다. 요지는 클라우드 서비스가 등장함에따라 상대적으로 저렴한 비용에 서버를 제공받아 해커들이 효율적인(?) 해킹을 할 수 있어서 솔팅을 해봤자 엄청난 속도로 해독해서 암호화폐정도가 아니고서야 해킹에 취약할 수 있다라는 내용이다.
#2 (드디어) 토큰 인증방식
토큰 인증방식은 정말 중요하게 사용되는 것 같아서 잘 정리하고 싶지만 일단 정리해두고 내용을 차차 업그레이드 하도록 하겠다. 왜냐하면 오늘 다루는 JWT뿐만 아니라 OAuth 개념에서도 확장되고 사용되는 분야가 다양해서 얕게 지금 한 번 정리하는 것으로 완전하게 정리할 수 없을 것 같기 때문이다.
(1) 등장배경 + 토큰 인증방식의 장점
토큰 인증방식은 기존의 자격증명 인증 방식의 여러 문제점을 해결하기 위해 발전되었다. 기존의 인증방식의 문제점과 그에 비해 토큰 인증방식이 어떤 장점을 가지는지 연결해서 알아보도록 하자.
1) 사용자 이름 및 암호 인증과 같은 전통적인 자격 증명 기반 방법에는 여러 가지 보안 취약점이 있다. 암호는 공격자가 추측하거나 도용하거나 가로챌 수 있으며, 사용자는 여러 계정에 걸쳐 암호를 재사용하는 경우가 많아 광범위한 보안 침해가 발생할 수 있다.
토큰 인증은 암호 대신 추측할 수 없는 고유한 토큰을 사용하여 보다 안전한 대안을 제공한다. 토큰은 암호화되고 서명되어 변조를 방지할 수 있다. 또한, 일정주기로 비밀번호 변경을 웹사이트에서 권장하고는 있으나 사용자가 강제로 변경하도록 하는 곳은 거의 없다. 그리고 변경하더라도 비밀번호가 그대로 노출되는 인증방식인 경우 탈취위험은 여전히 계속된다. 토큰은 이에 비해 제한된 기간 동안 발행되어 만료 후에는 더 이상 사용할 수 없도록 할 수 있다. 이와 같은 방법으로 손상된 토큰이 리소스에 무단으로 액세스하는 데 사용될 위험이 줄어든다.
2) 자격 증명 기반 방법은 특히 다른 리소스에 액세스하기 위해 자격 증명을 반복적으로 입력해야 하는 경우에 사용자에게 번거롭고 시간이 많이 소요되도록 한다.
토큰으로 권한을 부여한다면 사용자가 자격 증명을 반복적으로 입력할 필요 없이 여러 리소스에 액세스하는 데 사용할 수 있는 하나의 토큰을 얻게되어 인증 및 권한 부여 프로세스를 간소화한다. 이 장점은 서버마다 로그인 정보를 저장해야하는 세션인증방식의 단점과도 연결되는데 토큰 인증방식을 통해 다양한 서버 다양한 서비스에 공통으로 인증이 가능하도록 할 수 있어서 상대적으로 확장성이 있고 매우 편리하게 사용할 수 있다.
3) 토큰 인증은 유저의 인증상태를 클라이언트 쪽에 저장하여 세션기반 인증 방식에 비해 서버의 과부하나 메모리 부족 문제를 줄일 수 있다.
(2) 토큰 인증방식의 흐름
수업내용을 그대로 정리할 수도 있지만 이번에도 Chat GPT 사마의 도움을 받아 작성해보도록 하겠다..!! 역시 내맘대로 번역한다.
Q. can you make the flow of token authorization?
A. Sure~~
1. The user or application sends a request to the system for access to a resource.
사용자 또는 어플리케이션(이하 사용자)이 시스템에 엑세스 요청을 보낸다.
2. The system responds with a challenge, such as a request for a username and password, or a redirect to a login page.
시스템은 사용자 이름과 비밀번호같은 문제와 함께 로그인페이지로 리디렉션한다.
3. The user or application provides the requested credentials or completes the login process.
사용자는 요청된 자격 증명을 제공하거나 로그인 프로세스를 완료한다.
4. The system verifies the credentials and generates a token, which includes information about the user or application and the permissions they have been granted.
시스템은 확인한 뒤 토큰을 생성하는데 여기에는 사용자에게 부여된 권한에 대한 정보가 포함된다.
5. The token is returned to the user or application, which stores it for future use.
토큰은 저장될 수 있는 곳으로 반환되어나중에도 사용할 수 있게된다.
6. When the user or application requests access to a resource, it includes the token in the request.
그 후에 사용자가 엑세스 요청을 할 경우 토큰을 제시한다.
7. The system verifies the token, checking its validity, expiration, and the permissions associated with it.
시스템은 토큰을 검증하는데, 토큰의 유효성, 기한 등 필요한 부분을 확인한다.
8. If the token is valid and the user or application has the necessary permissions, access to the resource is granted.
토큰이 유효한 경우 사용자에게 리소스에 대한 접근이 허용된다.
9. If the token is invalid or has expired, or the user or application does not have the necessary permissions, access is denied and the user or application is prompted to obtain a new token.
토큰이 유효하지않거나 기한이 만료된 경우 사용자에게 새 토큰을 가져오라고 메세지를 준다.
10. The system may periodically refresh or revoke the token as needed, based on its configuration or user preferences.
시스템은 사용자의 설정에 따라 토큰을 주기적으로 갱신하거나 취소할 수 있다.
This flow can be further refined and customized based on the specific requirements of the system and the use case. For example, some systems may use additional authentication factors or verification steps, or may implement different token types or mechanisms for different types of resources or users.
이러한 흐름은 케바케로 인증방식이 추가되거나 사용자 정의화 될 수 있다. 예를 들어서 사용자마다 다른 권한을 부여하기 위한 다른 토큰 유형을 제공하는 등 차별화 된 시스템을 구현할 수도 있다.
오늘 배운 내용과 거의 비슷한데 사용자가 직접 이 과정에 참여한다기 보다는 토큰을 저장하고 사용자가 로그인을 원할 시 토큰을 이용해 엑세스 요청을 직접하는 것은 어플리케이션에서 대부분 일어나는 것이 합리적이며 더 맞는 내용인 것을 첨가하면 되겠다.
(3)JWT(JSON Web Token)
1) JWT의 간단 정의
JSON 객체에 정보를 담고 이를 토큰으로 암호화하여 전송할 수 있는 기술. 요즘 상용화된 이유 중 하나는 국제 인터넷 표준화 기구(Internet Engineering Task Force, IETF)에서 표준화한 RFC(RFC 7519)라는 웹 표준을 따르기 때문에 안정성이 높고 공인된 기술이기 때문이다.
2) JWT의 구성
JWT는 .(dot)으로 구분된 3가지 부분으로 구성되는데 각각을 살펴보자.
A)Header
헤더에는 HMAC-SHA256의 경우 "HS256" 또는 RSA-SHA256의 경우 "RS256"과 같이 토큰을 서명하는 데 사용되는 알고리즘에 대한 정보가 포함되어 있다. alg 의 값이 암호화한 알고리즘이 들어있다.
{
"alg": "HS256",
"typ": "JWT"
}
B)Payload
페이로드에는 전송되는 실제 데이터가 포함된다. 인증 중인 사용자 또는 리소스에 대한 요청을 여러 개 포함할 수 있다. 일반적인 요청내용은 "iss"(발행인), "sub"(대상자), "exp"(만료 시간), "aude"(청중)가 있다.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
C)Signature
signature는 JWT의 발신인이 요청하는 당사자인지 확인하고 메시지가 변조되지 않았는지 확인하는 데 사용된다. 헤더에 지정된 알고리즘을 사용하여 헤더와 페이로드를 비밀키(secret key)와 결합하여 생성된다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
secret key는 JWT의 발신자와 수신자에게만 알려져 있다.
JWT를 생성할 때, 보내는 주체가 시그니쳐 알고리즘을 사용하여 비밀 키와 함께 토큰의 헤더와 페이로드를 기반으로 시그니쳐를 생성한다. 수신자가 JWT를 수신하면 동일한 시그니쳐 알고리즘과 공유 secret key를 사용하여 시그니쳐를 다시 생성하고 JWT의 서명과 비교할 수 있다. 서명이 일치하는 경우 JWT는 유효한 것으로 간주되며 변조되지 않은 것이다.
시크릿 키가 추측하기 어렵고 무작위로 생성되어야한다 뭐 이런 것은 어찌보면 당연한 말이라 이하 생략한다.
시크릿 키가 중요한 이유는 페이로드 등을 변조해서 요청사항을 수정하려는 시도가 있어도 시크릿키를 모르면 동일한 시그니쳐를 생성할 수가 없어서 변조된 데이터인지 판별하는데 사용된다.
(4) 토큰인증방식의 한계와 리프레시 토큰
토큰이 비교적 안정적이긴 하지만 엑세스 토큰이든 리프레쉬 토큰이든 탈취당하는 경우에는 해킹으로부터 자유로울 수가 없다. 그에 대한 대비책으로 HttpOnly Cookie로 발급하거나 세션처럼 서버에 저장하고 이에 대한 상태를 관리하는 방법도 사용한다.
완벽한 방법은 없고 모든 것을 고려하여 균형적인 방법을 쓰자!
#3 토큰인증방식 실습
이번에도 클라이언트 부분과 서버부분이 나누어져 있는데 서버 측면에서 어떻게 토큰을 발급하고 관리하는지 그 부분에 집중해서 실습해보는 시간이었다.
각 파일들이 어떻게 연결되어서 어떤 정보를 주고받아 인증이 되는지를 중점적으로 살펴보자. 일단 큰 파일구조를 살펴보면 가장 상위 폴더가 내 github 폴더 경로에 token이란 이름으로 상위 폴더가 있고 그 밑에 client 폴더와 server 폴더가 존재하는 것으로 크게 나눌 수 있다. client 폴더에는 client를 구성하는 폴더와 파일들이 들어있고, sever에는 오늘 우리가 작업할 sever쪽 파일들이 들어있다.
서버 쪽의 파일 구조를 다시 살펴보면 내가 코드를 잘 짰는지 테스트하는 test코드가 있는 __test__파일이 있고, controllers 폴더와 index.js 파일이 있다.
index.js에서는 login.js,logout.js 그리고 userInfo.js까지 세 파일의 코드를 모듈객체로 exporting하는 것이다. module.exports로 되어있어서 exports default로 된 코드들만 보다가 과제를 하던중 잠깐 뇌정지가 왔었는데 이 글을 보면서 세션이 끝나고 나서 나중에 다시 정리할 수 있었다.
(1) controllers/login.js (POST /users/login)
구현 목표
- request로 받은 userId, password와 일치하는 유저가 DB에 존재하는지 확인합니다.
- 일치하는 유저가 없을 경우:
- 로그인 요청을 거절합니다.
- 일치하는 유저가 있을 경우:
- 필요한 데이터를 담은 두 종류의 JWT(access, refresh)를 생성합니다.
- request로 받은 checkedKeepLogin을 통해 로그인 상태 유지 여부를 확인합니다.
- Access Token은 세션 쿠키, Refresh Token은 영속성 쿠키로 보내야합니다.
- 클라이언트가 로그인 상태 유지를 원한다면 Access Token와 Refresh Token 모두 쿠키로 설정합니다.
- 클라이언트가 로그인 상태 유지를 원하지 않는다면 Access Token만 쿠키로 설정합니다.
- 클라이언트에 바로 응답을 보내지 않고 서버의 /userinfo로 리다이렉트합니다.
가장 먼저 구현해 볼 부분은 다음 코드로 db로부터 받아온 user 데이터를 기준으로 입력된 user ID와 password에 따라 일치하는 ID와 password 조합이 들어오면 Access token을 응답으로 보내고, 로그인 유지 기능이 체크되어있는지 여부에 따라 refresh token의 발급여부도 결정한다.
const { USER_DATA } = require('../../db/data');
// db로 부터 user data를 받아온다.
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
users에서 post 요청과 함께 전달된 reqest 객체에서 userId, password와 checkedKeepLogin을 구조분해할당하여 변수에 저장해준다.
여기서 먼저 userInfo라는 변수에 filter를 이용하여 방금 생성한 변수 userID, password와 USER_DATA를 비교하여 해당되는 조합이 있는지 찾아서 할당한다. 해당되는 조합이 없다면 undefined가 할당될 것이다.
const userInfo = {
...USER_DATA.filter(
(user) => user.userId === userId && user.password === password
)[0],
};
그래서 userInfo가 undefined인지 아닌지가 일치하는 유저가 있는지 여부이고 그에 따라 로그인 요청을 거부하거나 로그인 성공과 함께 토큰을 발급해준다. 성공한 경우에는 바로 클라이언트에 성공했다고 응답을 보내지는 않고 서버측 userInfo로 리다이렉트해서 토큰 검증과정을 거치고 그에 따라 최종적으로 로그인 성공/실패를 판정하여 클라이언트 쪽에 응답을 준다.
일치하는 유저가 있는지 여부에 따라 다음과 같이 코드가 작성된다.
const cookieOptions = {
domain: 'localhost',
path: '/',
sameSite: 'none',
secure: true,
expires: new Date(Date.now() + 24 * 3600 * 1000 * 7),
httpOnly: true,
};
// 쿠키의 경로, 주소 등 쿠키에 옵션을 설정하는 인자가 있는데, 쿠키설정 함수에서 inline으로 적지 않고 전달할 객체를 별도로 생성함.
if (!userInfo.id) {
res.status(401).send('Not Authorized');
} else {
const token = generateToken(userInfo, checkedKeepLogin);
if (checkedKeepLogin) {
res.cookie('refresh_jwt', token.refreshToken, cookieOptions);
}
delete cookieOptions.expires;
res.cookie('access_jwt', token.accessToken, cookieOptions);
}
res.redirect('/userinfo');
// userinfo로 redirect
(2)controllers/userInfo.js (GET /users/userinfo)
구현목표
- 쿠키에 Access Token이 있다면 verifyToken 메서드를 이용해 이를 검증합니다.
- 검증되었다면 토큰에 담긴 정보를 이용해 유저 정보를 db에서 조회한 후 응답으로 전달합니다.
- Access Token이 검증되지 않았다면 verifyToken 메서드를 이용해 Refresh Token을 검증합니다.
- 검증되었다면 토큰에 담긴 정보를 이용해 사용자 정보를 db에서 조회합니다.
- Access Token을 다시 발급해 쿠키에 담고 유저 정보와 함께 응답으로 전달합니다.
- 검증되지 않았다면 요청을 거절합니다.
- 검증되었다면 토큰에 담긴 정보를 이용해 사용자 정보를 db에서 조회합니다.
- Access Token, Refresh Token 모두 없다면 요청을 거절합니다.
userinfo로 리다이렉트 된 상황에서 현재 쿠키객체에는 access token은 발급이 무조건 되었을 것이고 refresh token이 각각 존재할 수도 있고 아닐 수도 있다. 이 토큰들도 다음과 같이 할당해준다.
const { access_jwt, refresh_jwt } = req.cookies;
access 토큰을 검증해주는 코드를 다음과 같이 작성하였다.
if (access_jwt) {
const accessToken_verification = verifyToken('access', access_jwt);
if (accessToken_verification) {
const userInfo = {
...USER_DATA.find((user) => user.id === accessToken_verification.id),
};
delete userInfo.password;
return res.send(userInfo);
}
}
그런데, 이 코드 전체를 감싸는 access 토큰이 존재하는지 여부를 검증하는 로직은 필요가 없는 부분이라고 생각하였다. 그래서 없애고 테스트를 돌렸는데 작동이 안되어서 왜 그런지 테스트 코드를 살펴보았다. 분명 저번에... 안됐던 것 같은데 이번에는 없애고 해도 작동된다. 뭔가 엣지케이스 때문에 원래 코드에서의 검증이 필요한 것인가 생각해보았는데 일단은 redirect 시점에 토큰이 있는지 검증이 필요가 없는 것 같아 코드 두 줄을 줄였다.
//if (access_jwt) {
const accessToken_verification = verifyToken('access', access_jwt);
if (accessToken_verification) {
const userInfo = {
...USER_DATA.find((user) => user.id === accessToken_verification.id),
};
delete userInfo.password;
return res.send(userInfo);
}
//}
그리고 refresh token과 관련하여 refresh 토큰이 있는 경우와 없는 경우의 로직을 또 완성해준다.
if (refresh_jwt) {
const refreshTokenPayload = verifyToken('refresh', refresh_jwt);
if (!refreshTokenPayload) return res.status(401).send('Not Authorized');
const userInfo = {
...USER_DATA.find((user) => user.id === refreshTokenPayload.id),
};
const cookiesOption = {
domain: 'localhost',
path: '/',
httpOnly: true,
sameSite: 'none',
secure: true,
};
res.cookie('access_jwt', generateToken(userInfo, false), cookiesOption);
}
//맨밑에는 둘 다 없는 경우에 401에러를 리턴한다.
return res.status(401).send('Not Authorized');
아 그리고 여기를 감싸는 if문을 없애도 되는지에 대해서 실험했었는데 이 코드의 가장 바깥쪽 if문은 없애면 안된다. 그렇게되면 access토큰이 없고 refresh 토큰만 있을 때 access토큰을 재발급하는 로직이 작동이 안되는데 왜 그런지 정말 의문이다. access token과 refresh token이 둘 다 없으면 세번째 줄에서 이미 401응답을 줄 것이다. access token이 있고 refresh token이 있어도 이미 access 토큰을 확인해서 이전 코드블럭의 코드에 따라 userinfo를 리턴하여 상황이 종료된다. access 토큰은 없는데 refresh 토큰만 있는 경우에 4번째 줄에 있는 코드가 실행될 것이고 그에따라 return은 없지만 res.cookie가 실행돼서 void 함수여도 괜찮지않을까 생각했는데 return res.cookie 로 해결하려고 해도 되지않고... 오히려 return res.cookie는 잘못된 사용법이다. (음... 근데 위에서는 return res.send 잘만 사용하는데 왜 return res.cookie는 또 안되는 것일까? 이 문제는 조금 더 해결해보고 보충하겠다.)
이렇게 분기에 따라 로그인 성공/실패를 판정하여 클라이언트로 응답을 보내고 clearcookie를 활용하여 로그아웃까지 구현해보았는데 이하 생략하도록 하겠다.
코드를 다시 정리해보면서 느끼는 점은 레퍼런스도 보면서 찬찬히 살펴보는데 꽤 시간이 많이들고 제대로 이해하고 사용하기가 정말 쉽지않음을 점점 느낀다. 완벽할 수는 없지만 적어도 눈에 보이는 의문점들은 해결하면서 계속 공부해나가도록 하겠다.
'study > TIL' 카테고리의 다른 글
[TIL No.27] 번들링, 웹펙 (0) | 2023.03.20 |
---|---|
[TIL No.26] Ouath 인증방식 (0) | 2023.03.09 |
[TIL No.24] 네트워크 심화 (0) | 2023.03.06 |
[TIL No.23] SEO, 웹 접근성 (0) | 2023.03.03 |
[TIL No.22] 웹 표준, Sementic HTML (0) | 2023.03.02 |