토큰 방식의 약점은 한 번 발급한 토큰을 만료 전에 즉시 무효로 만들기 어렵다는 데 있었다. 이 약점은 토큰의 수명을 짧게 잡는 것으로 크게 완화되지만, 수명이 짧으면 사용자가 자주 다시 로그인해야 하는 불편이 뒤따른다. 안전을 위해 수명을 줄이면 편의가 나빠지고, 편의를 위해 수명을 늘리면 위험이 커지는 이 긴장을 푸는 구조가 두 종류의 토큰을 나누어 쓰는 방식이다.


이번 편은 왜 두 개의 토큰이 필요한지, 짧게 쓰고 버리는 토큰과 조용히 다시 발급받는 토큰이 각각 어떤 역할을 하는지, 그 갱신 수단을 어떻게 회전시키고 재사용을 어떻게 감지하는지, 그리고 이 토큰들을 어디에 어떻게 보관해야 하는지를 다룬다. 수명과 무효화의 딜레마를 실제로 다루는 설계이므로, 앞 편의 원리가 여기서 구체적인 구조로 이어진다.

두 개의 토큰이 필요한 이유

하나의 토큰으로 수명 문제를 풀려 하면 딜레마에 갇힌다. 그 토큰이 요청마다 실려 다니는 이상 새어 나갈 위험이 늘 있는데, 수명을 길게 두면 새어 나갔을 때 오래 악용되고, 짧게 두면 사용자가 자주 로그인해야 한다. 한 토큰에 자주 쓰인다는 성질과 오래 유효하다는 성질을 동시에 지우는 것이 애초에 무리다.


두 토큰 구조는 이 두 성질을 서로 다른 토큰에 나누어 맡긴다. 실제 요청에 실려 자주 오가는 토큰은 수명을 짧게 두어 새어 나가도 곧 만료되게 하고, 그 짧은 토큰을 다시 발급받는 데만 쓰이는 토큰은 좀처럼 오가지 않으니 수명을 길게 두어 로그인 유지를 담당하게 한다. 자주 노출되는 것과 오래 사는 것을 갈라놓는 것이다.


이렇게 나누면 노출의 빈도와 수명이 역으로 맞물린다. 자주 오가는 토큰은 짧게 살고, 드물게 오가는 토큰은 길게 산다. 위험이 큰 쪽의 노출 창을 줄이고, 노출이 적은 쪽에 긴 수명을 몰아준다. 두 성질의 충돌을 하나의 값에서 두 값으로 옮겨 해소하는 것이 이 구조의 핵심이다.


이 구조에서 사용자는 대개 불편을 느끼지 않는다. 짧은 토큰이 만료되어도 긴 토큰으로 새 토큰을 조용히 받아 오므로, 로그인 화면을 다시 보는 일 없이 방문이 이어진다. 안전을 위해 수명을 줄이면서도 편의를 지키는 절충이 두 토큰의 분업으로 이루어진다.

짧게 쓰고 버리는 토큰

실제 요청에 실려 신원을 증명하는 토큰은 수명을 짧게 잡는다. 이 토큰은 자원에 접근할 때마다 함께 보내지므로 노출의 기회가 잦다. 그래서 새어 나가더라도 곧 만료되어 쓸모를 잃도록, 유효 기간을 분 단위나 그에 준하는 짧은 수준으로 둔다. 짧은 수명이 이 토큰의 안전을 떠받치는 핵심 성질이다.


수명이 짧으므로 이 토큰은 무효화의 부담에서 비교적 자유롭다. 개별 무효화 장치를 두지 않아도, 새어 나간 토큰이 통하는 시간이 짧아 피해가 제한되기 때문이다. 무상태의 이점을 그대로 누리면서도 노출 위험을 수명으로 억누르는 것이다. 이 토큰에는 서버가 굳이 상태를 두지 않아도 되는 이유가 여기에 있다.


다만 짧은 수명이 검증 소홀의 핑계가 되어서는 안 된다. 이 토큰도 서명과 만료, 발급자와 대상을 모두 확인해야 하며, 검증 기준은 서버가 쥐고 있어야 한다. 수명이 짧다는 것은 새어 나갔을 때의 피해를 줄일 뿐, 위조된 토큰이나 조건이 맞지 않는 토큰까지 걸러 주지는 않기 때문이다.


이 토큰에 담기는 정보도 최소한으로 둔다. 사용자를 가리키는 정보와 유효 구간 정도만 담고, 자주 바뀌는 권한 같은 것은 담지 않거나 신중히 다룬다. 짧게 살고 자주 오가는 토큰일수록 가볍고 단순하게 유지하는 것이 좋다.

조용히 다시 발급받는 토큰

짧은 토큰이 만료되면 새 토큰이 필요하다. 이 새 발급을 담당하는 것이 긴 수명의 갱신 토큰이다. 이 토큰은 자원 접근에는 쓰이지 않고, 오직 짧은 토큰을 다시 받아 오는 데만 쓰인다. 쓰임이 한정되어 있으므로 오가는 빈도가 낮고, 그만큼 노출의 기회도 적다.


갱신 토큰은 쓰임이 제한된 만큼 더 안전한 곳에 보관된다. 자원 요청마다 실려 다니지 않으니, 갱신을 위한 특정 경로로만 오가게 두고 그 밖의 요청에는 실리지 않게 할 수 있다. 오가는 경로를 좁힐수록 새어 나갈 표면이 줄어드는데, 갱신 토큰은 이 좁히기가 특히 잘 들어맞는다.


이 토큰은 수명이 길지만, 그 대가로 즉시 무효화할 수 있어야 한다. 갱신 토큰이 오래 사는 이상, 새어 나갔을 때 이를 끊어 낼 손잡이가 없으면 위험하다. 그래서 갱신 토큰은 짧은 토큰과 달리 서버가 상태를 두어 관리하는 경우가 많다. 서버가 유효한 갱신 토큰의 목록을 쥐고 있으면, 필요할 때 특정 토큰을 폐기해 갱신의 연결을 끊을 수 있다.


갱신 토큰이 폐기되면 그것으로 이어지던 짧은 토큰의 발급도 멈춘다. 이미 발급된 짧은 토큰은 곧 만료되고, 새 토큰은 더 나오지 않으므로 방문이 자연스럽게 끊긴다. 짧은 토큰의 무상태 이점과 갱신 토큰의 무효화 손잡이가 이렇게 서로를 보완한다.

갱신 토큰을 회전시키기

갱신 토큰을 한 번 발급해 오래 그대로 쓰면, 그 하나가 새어 나갔을 때 긴 시간 악용될 수 있다. 이 위험을 줄이는 방법이 회전이다. 갱신 토큰으로 새 짧은 토큰을 받을 때마다 갱신 토큰 자체도 새것으로 바꾸고 이전 것을 폐기하는 것이다. 그러면 갱신 토큰이 한 번 쓰일 때마다 값이 달라진다.


회전을 두면 갱신 토큰의 유효 기간이 실질적으로 짧아진다. 이름상 수명은 길어도, 다음 갱신이 일어나는 순간 이전 값은 무효가 되므로 각 값이 실제로 통하는 시간은 다음 갱신까지로 좁혀진다. 오래 로그인을 유지하면서도 각 토큰 값의 노출 창은 짧게 유지하는 절충이 회전으로 이루어진다.


회전은 재사용을 감지하는 발판이 되기도 한다. 값이 매번 바뀌므로, 이미 새것으로 교체되어 폐기되었어야 할 이전 값이 다시 나타난다면 무언가 잘못되었다는 신호다. 정상적인 흐름에서는 이전 값이 다시 쓰일 일이 없기 때문이다. 이 신호를 잡아내는 것이 다음 주제인 재사용 감지다.


다만 회전에는 정상적인 흐름에서도 이전 값이 잠깐 겹칠 수 있다는 점을 고려해야 한다. 네트워크가 끊겨 새 값을 받지 못한 채 이전 값으로 다시 시도하는 경우가 그렇다. 그래서 회전 설계에는 이런 정상적인 겹침과 실제 재사용을 구분하는 여유를 함께 두어야, 멀쩡한 사용자가 갑자기 로그아웃되는 일을 줄일 수 있다.

재사용을 감지하고 계열을 폐기하기

회전을 두면, 폐기되었어야 할 이전 갱신 토큰이 다시 나타나는 상황을 감지할 수 있다. 이런 재사용은 갱신 토큰이 새어 나가 두 곳에서 쓰이고 있음을 시사하는 강한 신호다. 정당한 쪽이 새 값으로 넘어간 뒤에도 누군가 이전 값을 들고 나타난다면, 그 값을 손에 넣은 다른 쪽이 있다는 뜻이기 때문이다.


이 신호에 대한 대응은 단호해야 한다. 재사용이 감지되면 그 갱신 토큰에서 이어져 온 일련의 토큰 전체를 폐기하는 것이다. 하나의 최초 로그인에서 회전을 거쳐 이어진 토큰들을 하나의 계열로 묶어 두고, 재사용이 드러나면 그 계열을 통째로 끊는다. 어느 쪽이 정당한 사용자인지 그 순간 가릴 수 없으므로, 안전하게 둘 다 끊고 다시 로그인하게 하는 것이다.


계열을 통째로 끊는 것은 언뜻 과해 보이지만, 방어의 관점에서는 합당한 선택이다. 새어 나간 값이 계속 쓰이도록 두는 위험보다, 정당한 사용자가 한 번 더 로그인하는 불편이 훨씬 작기 때문이다. 의심스러운 순간에는 연결을 끊고 신원을 다시 확인받게 하는 편이 안전하다.


이 감지가 작동하려면 서버가 갱신 토큰의 상태를 쥐고 있어야 한다. 어느 값이 폐기되었고 어느 계열에 속하는지를 서버가 기록해 두어야, 이전 값의 재등장을 알아채고 계열을 끊을 수 있다. 짧은 토큰은 무상태로 두어 이점을 살리되, 갱신 토큰은 상태를 두어 이런 감지와 폐기를 가능하게 하는 것이 흔한 절충이다.

토큰을 어디에 보관할 것인가

두 토큰은 그 성질에 맞게 서로 다른 방식으로 보관하는 것이 좋다. 짧은 토큰은 자주 쓰이므로 다루기 편한 곳에 두되 노출을 수명으로 억누르고, 긴 갱신 토큰은 좀처럼 쓰이지 않으므로 접근을 좁힌 안전한 곳에 둔다. 위험이 큰 값일수록 접근을 좁히는 원칙이 여기서도 적용된다.


갱신 토큰은 스크립트가 읽지 못하게 막은 쿠키에, 갱신 경로에만 실리도록 범위를 좁혀 두는 방식이 흔히 쓰인다. 이렇게 하면 스크립트 주입으로 값을 읽어 가는 경로가 막히고, 다른 요청에는 실리지 않아 노출 표면이 줄어든다. 오래 사는 값일수록 이런 보호를 촘촘히 두어야 한다.


짧은 토큰은 상대적으로 다루기 편한 곳에 두더라도, 그 값이 새어 나갔을 때의 피해가 짧은 수명으로 제한된다는 점이 완충이 된다. 다만 편하다고 해서 아무렇게나 노출하는 것은 아니며, 되도록 오래 저장하지 않고 필요할 때만 쥐고 있다가 버리는 방식이 안전하다.


어느 토큰이든 새어 나가는 경로를 하나하나 따져 막는 자세가 밑바탕이 된다. 전송은 암호화된 연결로만 하고, 기록이나 주소에 토큰이 남지 않게 하며, 필요 없는 곳에는 실리지 않게 한다. 두 토큰의 분업이 아무리 정교해도 보관과 전송이 허술하면 그 이점이 무너지기 때문이다.

만료와 로그아웃을 매듭짓기

두 토큰 구조에서 로그아웃은 갱신 토큰을 폐기하는 일로 이루어진다. 서버가 그 갱신 토큰을 무효 목록에 올리거나 상태 기록에서 지우면, 그 토큰으로는 더 이상 새 짧은 토큰을 받을 수 없다. 이미 발급된 짧은 토큰은 곧 만료되므로, 방문이 자연스럽게 끝난다. 로그아웃이 갱신의 연결을 끊는 일로 정리되는 것이다.


이때 클라이언트에 남은 토큰도 함께 지우는 것이 안전하다. 서버가 갱신 토큰을 폐기해도 클라이언트에 값이 남아 있으면, 그 값이 새어 나갈 여지가 남는다. 그래서 로그아웃 처리는 서버의 폐기와 클라이언트의 정리를 함께 수행해, 양쪽 모두에서 토큰의 흔적을 지워야 온전해진다.


도용이 의심되는 상황에서는 특정 계열만이 아니라 그 사용자의 모든 갱신 토큰을 폐기하는 처리도 필요하다. 비밀번호를 바꾸거나 이상 징후가 감지되면, 그 사용자에게 딸린 갱신 토큰을 모두 끊어 모든 기기의 방문을 한꺼번에 종료한다. 갱신 토큰에 상태를 둔 덕분에 이런 일괄 종료가 가능해진다.


갱신이 실패할 때의 처리

갱신 토큰으로 새 짧은 토큰을 받는 과정이 늘 성공하는 것은 아니다. 갱신 토큰이 만료되었거나, 폐기되었거나, 재사용 감지로 계열이 끊겼을 수 있다. 이런 경우 갱신은 실패하고, 클라이언트는 더 이상 짧은 토큰을 받을 수 없다. 이 실패를 어떻게 다루느냐가 사용자 경험과 방어 모두에 걸린다.


갱신 실패의 원인에 따라 대응이 갈린다. 단순히 갱신 토큰이 만료된 것이라면, 사용자를 로그인 화면으로 보내 다시 신원을 확인받게 하면 된다. 이는 정상적인 수명의 끝이므로 특별한 경고 없이 자연스럽게 재로그인으로 이어진다. 오래 유지되던 방문이 수명을 다한 것뿐이다.


반면 재사용 감지로 계열이 끊긴 경우라면, 그 실패는 도용의 가능성을 담고 있다. 이때는 단순한 재로그인을 넘어, 필요하면 사용자에게 알림을 보내거나 추가 확인을 요구하는 대응을 고려한다. 같은 재로그인이라도 그 배경이 정상적인 만료인지 의심스러운 신호인지에 따라 뒤따르는 처리가 달라져야 한다.


갱신이 실패했을 때 클라이언트가 무한히 갱신을 재시도하지 않도록 하는 것도 중요하다. 실패한 갱신을 계속 반복하면 서버에 불필요한 부담이 쌓이고, 정작 사용자는 막힌 상태에 갇힌다. 그래서 갱신 실패가 확인되면 재시도를 멈추고 재로그인 흐름으로 넘기는 처리를 명확히 두어야 한다.

정리하면 두 토큰 구조는 자주 오가는 짧은 토큰과 드물게 오가는 긴 갱신 토큰으로 수명과 노출을 갈라, 안전과 편의를 함께 얻는 설계다. 갱신 토큰을 회전시켜 각 값의 노출 창을 좁히고, 이전 값의 재등장을 재사용으로 감지해 계열을 통째로 끊으며, 두 토큰을 성질에 맞게 보관한다. 다음 편에서는 인증의 또 다른 축인 비밀번호 자체를 어떻게 저장해야 하는지를 들여다본다.