소셜 로그인이 신원 확인을 외부에 위임한다는 큰 그림을 세웠다면, 이제 그 위임이 실제로 어떤 절차로 이루어지는지를 볼 차례다. 사용자가 외부 서비스의 버튼을 누른 뒤부터 우리 서비스가 신원을 넘겨받기까지 여러 주체가 정해진 순서로 값을 주고받는다. 이 절차는 얼핏 복잡해 보이지만, 각 단계가 특정한 위험을 막기 위해 놓여 있음을 이해하면 그 구조가 필연처럼 읽힌다.
이번 편은 이 흐름에 등장하는 네 주체가 각각 무엇을 하는지, 신원을 넘기는 대신 임시 코드를 주고받는 절차가 왜 그렇게 짜였는지, 사용자를 돌려보내는 과정에서 위조를 어떻게 막는지, 그리고 코드가 도중에 가로채이는 것을 어떻게 무력화하는지를 다룬다. 절차의 각 조각이 어떤 방어를 담고 있는지를 따라가는 것이 목표다.
흐름에 등장하는 네 주체
이 흐름에는 네 주체가 등장한다. 첫째는 로그인하려는 사용자이고, 둘째는 사용자가 쓰는 브라우저다. 셋째는 로그인을 붙이려는 우리 서비스이고, 넷째는 신원을 확인해 주는 외부 서비스다. 이 넷이 정해진 순서로 값을 주고받으며 신원을 넘긴다. 각 주체의 역할을 구분하는 것이 흐름을 이해하는 출발점이다.
사용자는 자신의 신원을 외부 서비스에서 증명하고, 우리 서비스가 그 결과를 받는 것에 동의하는 주체다. 브라우저는 사용자를 대신해 우리 서비스와 외부 서비스 사이를 오가며 값을 나른다. 사용자가 직접 값을 다루는 것이 아니라, 브라우저가 사용자의 이동과 값의 전달을 맡는다.
우리 서비스는 외부 서비스에 신원 확인을 요청하고 그 결과를 넘겨받아 로그인을 완성하는 주체다. 외부 서비스는 사용자를 직접 확인하고, 사용자의 동의를 받아, 확인 결과를 우리 서비스에 넘겨준다. 신원을 실제로 확인하는 일은 외부 서비스가 하고, 우리 서비스는 그 결과를 받아 쓴다.
여기서 중요한 점은 사용자의 비밀번호가 우리 서비스를 거치지 않는다는 것이다. 사용자는 오직 외부 서비스에서만 비밀번호를 입력하고, 우리 서비스는 그 비밀번호를 보지 못한다. 우리 서비스가 받는 것은 비밀번호가 아니라 외부 서비스가 확인해 주었다는 증명이다. 이 분리가 흐름 전체를 설계하는 근본 원칙이다.
임시 코드를 주고받는 절차
흐름은 우리 서비스가 사용자를 외부 서비스로 보내는 것에서 시작한다. 사용자가 로그인 버튼을 누르면, 우리 서비스는 브라우저를 외부 서비스의 로그인 화면으로 이동시킨다. 이때 우리 서비스가 누구이고 무엇을 요청하는지를 함께 전달한다. 이제 사용자는 외부 서비스의 화면에서 자신의 신원을 증명한다.
사용자가 외부 서비스에서 로그인하고 정보 제공에 동의하면, 외부 서비스는 사용자를 다시 우리 서비스로 돌려보낸다. 이때 신원 증명을 곧바로 넘기지 않고, 그 증명으로 바꿀 수 있는 임시 코드만 브라우저를 통해 넘긴다. 브라우저를 거쳐 오는 이 경로가 완전히 안전하지는 않으므로, 실제 증명 대신 한 번 쓰고 버릴 임시 코드만 보내는 것이다.
우리 서비스는 이 임시 코드를 받은 뒤, 브라우저를 거치지 않고 외부 서비스에 직접 연결해 코드를 실제 증명으로 바꾼다. 이 직접 연결은 우리 서비스와 외부 서비스 사이에서만 이루어지므로 브라우저를 거치는 경로보다 안전하다. 민감한 실제 증명은 이 안전한 경로로만 오가고, 위험한 경로에는 임시 코드만 흐르게 하는 것이 이 절차의 핵심이다.
코드를 증명으로 바꿀 때 우리 서비스는 자신이 진짜 그 서비스임을 함께 증명한다. 우리 서비스만 아는 값을 외부 서비스에 함께 제시해, 코드를 손에 넣은 다른 쪽이 함부로 증명으로 바꾸지 못하게 한다. 임시 코드가 새어 나가더라도 우리 서비스의 비밀 없이는 증명으로 바꿀 수 없으므로, 코드만으로는 쓸모가 없다.
돌려보내는 과정과 위조 방어
사용자를 외부 서비스로 보냈다가 다시 돌려받는 이 왕복에는 위조의 위험이 따른다. 누군가 사용자를 속여, 사용자가 시작하지도 않은 로그인 흐름의 응답을 우리 서비스에 밀어 넣으려 할 수 있다. 이런 위조를 막으려면 돌아온 응답이 정말 우리 서비스가 시작한 흐름에 대한 것인지를 확인해야 한다.
이를 위해 우리 서비스는 흐름을 시작할 때 예측 불가능한 값을 하나 만들어 함께 보낸다. 그리고 사용자가 돌아올 때 그 값이 그대로 돌아오는지를 확인한다. 우리 서비스가 만들어 보낸 값이 정확히 돌아왔다면, 그 응답은 우리가 시작한 흐름에 대한 것이다. 값이 없거나 다르면 위조된 응답으로 보고 거부한다.
이 값은 매번 새로 만들어야 하고 예측할 수 없어야 한다. 값이 고정되어 있거나 짐작 가능하면, 위조하는 쪽이 그 값을 미리 맞춰 응답을 꾸며 낼 수 있기 때문이다. 그래서 흐름을 시작할 때마다 안전한 난수로 값을 만들어 두고, 돌아온 값과 대조한 뒤에는 그 값을 폐기한다. 한 번의 흐름에만 쓰이는 일회성 값이어야 한다.
이 값을 어디에 보관하느냐도 중요하다. 흐름을 시작할 때 만든 값을 사용자의 세션에 묶어 두고, 돌아온 응답의 값과 그 세션에 보관된 값을 비교한다. 값이 세션에 묶여 있으므로, 다른 사용자의 흐름에 끼어들어 응답을 밀어 넣으려는 시도가 세션 단위에서 걸러진다. 시작과 응답이 같은 사용자의 흐름임을 이 묶음이 보증한다.
코드 가로채기를 무력화하기
임시 코드는 브라우저를 거쳐 오므로, 그 경로에서 코드가 가로채일 가능성을 완전히 배제할 수 없다. 특히 우리 서비스가 비밀을 안전하게 지키기 어려운 환경에서는, 코드를 손에 넣은 쪽이 그것을 증명으로 바꾸려 시도할 수 있다. 이 가로채기를 무력화하는 장치가 흐름에 더해진다.
이 장치는 흐름을 시작할 때 우리 서비스가 임시 비밀을 하나 만들고, 그 비밀을 되돌릴 수 없는 형태로 바꾼 값만 먼저 외부 서비스에 보내는 방식으로 작동한다. 나중에 코드를 증명으로 바꿀 때 원래 비밀을 함께 제시하면, 외부 서비스는 앞서 받은 값과 대조해 같은 흐름임을 확인한다. 원래 비밀을 모르는 쪽은 코드를 손에 넣어도 증명으로 바꿀 수 없다.
이 방식의 핵심은 코드와 그것을 증명으로 바꿀 자격을 분리하는 데 있다. 코드가 새어 나가더라도, 그 코드를 증명으로 바꾸려면 흐름을 시작한 쪽만 아는 원래 비밀이 필요하다. 가로챈 코드만으로는 아무것도 할 수 없으므로, 브라우저 경로에서 코드가 새어 나가는 위험이 실질적으로 무력화된다.
이 장치는 원래 비밀을 안전하게 지키기 어려운 환경에서 특히 중요하다. 그런 환경에서는 우리 서비스의 고정된 비밀에만 기대기 어렵기 때문에, 흐름마다 새로 만드는 임시 비밀로 코드의 가로채기를 막는다. 오늘날에는 이 방식이 환경을 가리지 않고 기본으로 권장되는 방어로 자리 잡았다.
넘어온 증명을 검증하기
코드를 증명으로 바꾼 뒤에도 그 증명을 그대로 믿어서는 안 된다. 넘어온 증명이 진짜 그 외부 서비스에서 왔고, 우리 서비스를 위한 것이며, 도중에 위조되지 않았음을 검증해야 한다. 이 검증은 앞서 토큰을 다룰 때 짚은 원칙과 같다. 서명을 확인하고, 발급자와 대상을 대조하고, 유효 기간을 살핀다.
서명을 확인하는 것은 증명이 위조되지 않았음을 보는 것이다. 외부 서비스가 낸 증명에는 그 서비스만이 만들 수 있는 서명이 붙어 있으므로, 이 서명을 검증하면 증명이 진짜 그 서비스에서 왔음을 알 수 있다. 서명이 맞지 않으면 위조된 증명으로 보고 받아들이지 않는다.
발급자와 대상을 대조하는 것은 이 증명이 신뢰하는 외부 서비스에서 우리 서비스를 위해 발급되었는지를 보는 것이다. 발급자가 우리가 신뢰하는 서비스가 맞는지, 대상이 우리 서비스를 가리키는지를 확인해, 다른 곳을 위한 증명이 잘못 받아들여지는 것을 막는다. 이 대조가 없으면 엉뚱한 대상을 위한 증명에 속을 수 있다.
검증을 통과한 뒤에야 증명 안의 신원 정보를 꺼내 우리 계정에 잇는다. 이때도 좀처럼 바뀌지 않는 고유 식별자를 연결의 기준으로 삼고, 겹칠 수 있는 정보만으로 자동 연결하지 않는다. 넘어온 증명을 엄격히 검증하고 신중히 잇는 이 마지막 단계가, 위임의 편리함 위에 안전을 얹는다.
흐름을 다루며 조심할 것
이 흐름을 다룰 때 가장 조심할 것은 위조 방어 값을 빠뜨리는 것이다. 돌아온 응답이 우리가 시작한 흐름에 대한 것인지 확인하지 않으면, 사용자가 시작하지도 않은 로그인이 밀어 넣어질 수 있다. 흐름을 시작할 때 만든 일회성 값을 세션에 묶어 두고 돌아온 값과 대조하는 절차를 반드시 두어야 한다.
임시 비밀로 코드 가로채기를 막는 장치도 빠뜨리지 말아야 한다. 이 장치가 없으면 브라우저 경로에서 코드가 새어 나갔을 때 그대로 증명으로 바뀔 수 있다. 흐름마다 새 임시 비밀을 만들고 대조하는 이 방어를 기본으로 두어, 코드만으로는 아무것도 할 수 없게 해야 한다.
사용자를 돌려보낼 주소를 엄격히 제한하는 것도 중요하다. 외부 서비스가 사용자를 돌려보내는 주소를 미리 등록된 주소로만 한정하지 않으면, 엉뚱한 곳으로 사용자와 코드가 흘러갈 수 있다. 그래서 돌아올 주소를 미리 등록해 두고, 등록되지 않은 주소로는 돌려보내지 않도록 외부 서비스 쪽에서 막는다.
넘겨받는 정보의 범위를 최소한으로 두는 절제도 잊지 말아야 한다. 흐름을 시작할 때 우리 서비스가 요청하는 정보의 범위를 필요한 만큼으로 좁히면, 사용자가 내주는 정보도 줄고 우리가 지킬 것도 준다. 로그인에 꼭 필요한 것만 요청하는 것이 사용자의 신뢰를 얻고 위험을 줄이는 길이다.
흐름의 변형과 한계
지금까지 살펴본 절차는 임시 코드를 거쳐 안전한 경로로 증명을 받아 오는 방식이다. 과거에는 이보다 단순하게 증명을 브라우저 경로로 곧바로 받는 변형도 쓰였으나, 이 변형은 증명이 위험한 경로에 그대로 노출되는 약점이 있어 지금은 권장되지 않는다. 절차가 복잡해 보여도, 그 복잡함이 증명을 위험한 경로에서 떼어 놓기 위한 것임을 이해해야 한다.
흐름의 각 단계는 특정 위험을 겨냥한 방어이므로, 어느 하나를 편의를 위해 생략하면 그 위험이 되살아난다. 임시 코드를 거치는 단계를 건너뛰면 증명이 노출되고, 위조 방어 값을 빼면 응답 위조가 열리며, 코드 가로채기 방어를 빼면 새어 나간 코드가 그대로 쓰인다. 절차의 조각들은 장식이 아니라 각자의 자리에서 방어를 맡고 있다.
이 흐름이 다루는 것은 신원의 확인이지 그 뒤의 권한 관리가 아니라는 점도 분명히 해야 한다. 외부 서비스가 넘겨준 것은 이 사용자가 누구인지에 대한 증명일 뿐, 우리 서비스 안에서 그 사용자가 무엇을 해도 되는지는 우리가 따로 정해야 한다. 위임은 인증까지이고, 인가는 여전히 우리 서비스의 몫이다.
흐름을 직접 구현하기보다 검증된 구현에 기대는 편이 대체로 안전하다는 점도 짚어 둘 만하다. 이 절차에는 놓치기 쉬운 방어가 여럿 얽혀 있어, 직접 짜다 보면 위조 방어나 검증 가운데 하나를 빠뜨리기 쉽다. 이미 검증된 구현을 쓰되 그것이 무엇을 하고 있는지를 이해하는 것이, 흐름을 안전하게 다루는 현실적인 길이다.
정리하면 이 흐름은 사용자와 브라우저, 우리 서비스와 외부 서비스가 정해진 순서로 값을 주고받으며, 위험한 브라우저 경로에는 임시 코드만 흐르게 하고 민감한 증명은 안전한 직접 경로로만 오가게 한다. 일회성 값으로 응답의 위조를 막고, 임시 비밀로 코드 가로채기를 무력화하며, 넘어온 증명을 엄격히 검증한다. 각 단계가 특정 위험을 겨냥한 방어임을 이해하면 이 절차가 왜 이렇게 짜였는지가 드러난다. 다음 편에서는 계정의 진짜 주인임을 확인하는 이메일 인증을 들여다본다.