- 기술
제주 여행에서 탈 차를 노트북으로 예약한다고 해 보자. 렌터카 회사 JM렌터카의 예약 화면에서 마지막 버튼을 누르자 브라우저가 작은 창을 띄운다. “JM렌터카가 운전면허 정보를 요청해요. 이름, 면허 유효 여부, 성인 여부를 보여줄까요?” 안내에 따라 휴대폰을 꺼내 그 안에 넣어 둔 디지털 운전면허로 확인을 마치자 예약이 끝난다. 면허증 사진을 찍어 올리지도, 면허번호를 입력하지도 않았다.
이 시리즈는 다섯 편에 걸쳐 이 장면까지 가는 길을 따라간다. 출발점은 훨씬 익숙한 곳이다.
시간을 조금 돌려 보자. JM렌터카에 처음 가입하던 날, 가입 화면에는 ‘카카오 로그인’ 버튼이 있었다. 버튼을 누르자 카카오 화면이 열렸고, 거기서 로그인하고 동의를 한 번 누르니 다시 JM렌터카로 돌아왔다. 가입 양식에는 이미 이름과 이메일이 채워져 있었다.
JM렌터카에 카카오 비밀번호를 알려 준 적은 없다. 그렇다면 JM렌터카는 이름과 이메일을 어디서 받았고, 다음에 로그인했을 때 같은 사람인지는 어떻게 알아볼까? 이 글은 그 버튼 뒤에서 오가는 일을 살펴본다.
소셜 로그인은 버튼 하나로 끝나서 단순해 보인다. 하지만 그 뒤에는 세 참여자가 있다. 버튼을 누른 사용자, 가입을 받는 JM렌터카, 그리고 사용자의 계정을 가진 카카오다.
여기서 카카오는 IdP(신원 제공자)다. 사용자의 계정을 관리하고, 다른 서비스에 “이 사람이 로그인했다”는 결과를 알려 주는 쪽이다.
세 참여자가 주고받는 메시지에는 두 가지가 실려 있다. 하나는 ‘JM렌터카가 무엇을 해도 되는가’라는 권한이고, 다른 하나는 ‘지금 로그인한 사람이 누구인가’라는 신원이다. 이 둘을 나눠 보면 버튼 뒤의 구조가 보인다.
예전에는 다른 서비스의 정보를 가져오려고 그 서비스의 아이디와 비밀번호를 직접 받는 사이트도 있었다. 비밀번호를 받은 사이트는 그 계정으로 사용자와 똑같이 무엇이든 할 수 있다. 그 사이트 하나만 막고 싶어도 비밀번호를 바꾸는 것 말고는 방법이 없다.
OAuth 2.0은 이 문제를 푸는 규격이다. 사용자가 비밀번호를 넘기는 대신, 동의한 범위만큼의 권한을 다른 서비스에 맡기는 방법을 정한다. 이를 권한 위임이라고 부른다.
앞의 가입 장면을 OAuth 2.0의 흐름으로 다시 보면 다음과 같다. 이 흐름을 Authorization Code Flow라고 한다.
비밀번호는 사용자와 카카오 사이에서만 오간다. JM렌터카는 끝까지 사용자의 카카오 비밀번호를 모른다.
토큰은 서비스끼리 주고받는 값으로, 무엇이 허락되었는지 또는 누가 로그인했는지를 담는다. 마지막 단계에서 JM렌터카가 받는 토큰 가운데 하나가 Access Token이다. 발레파킹을 맡길 때 건네는 키 중에는 시동은 걸리지만 트렁크나 글로브박스는 열리지 않는 키가 있다. Access Token도 비슷하다. 정확히 말하면 Access Token은 사용자가 동의한 범위의 API를 정해진 시간 동안만 호출하도록 허락하는 값이다. JM렌터카는 이 값을 카카오의 API에 내밀고, 동의받은 정보만 가져온다.
OAuth 2.0은 권한을 맡기는 규격일 뿐이다. 카카오 화면에서 로그인은 일어나지만, 그 인증 결과를 서비스에 전달하는 방법은 OAuth 2.0이 정하지 않는다.
Access Token은 ‘이 API를 불러도 된다’는 허가다. ‘방금 로그인한 사람이 누구인가’에 답하지는 않는다.
OAuth 2.0에서 Access Token은 API를 제공하는 쪽이 해석하는 값이다. 그 토큰을 받아 쓰는 서비스는 보통 내용을 읽지 않고 그대로 전달만 한다.
JM렌터카가 정말 알고 싶은 것은 방금 카카오에서 로그인을 마친 사람이 누구이고, 전에 가입한 그 사용자가 맞는지다. Access Token만으로는 이 질문에 바로 답할 수 없다. 그래서 JM렌터카 서버는 받은 Access Token으로 카카오의 사용자 정보 API를 한 번 더 불러 사용자를 확인한다. 서버가 카카오에서 토큰을 직접 받았다면 이 방식은 안전하고, 지금도 널리 쓰인다. 이때 ‘누가 로그인했는가’에 답하는 것은 Access Token이 아니라 카카오 API의 응답이다.
다만 Access Token 자체를 로그인의 증거로 믿으면 사고가 날 수 있다. JM렌터카의 모바일 앱이 받은 Access Token을 서버에 넘겨 로그인을 처리한다고 해 보자. 누군가 다른 서비스를 운영하면서 그 서비스에 로그인한 사용자들의 Access Token을 모아 두었다가 JM렌터카 서버에 보낸다. 서버가 그 토큰이 누구를 위해 발급됐는지 IdP에 따로 확인하지 않으면, 자기 앱을 위한 토큰인지 알 수 없다. 그러면 공격자가 남의 계정으로 JM렌터카에 로그인할 수 있다. 권한을 받는 일과 누가 로그인했는지 아는 일은 따로 풀어야 하는 문제다.
OpenID Connect(OIDC)는 OAuth 2.0을 바탕으로 인증 결과까지 전달하도록 만든 규격이다. OAuth 2.0의 흐름을 그대로 쓰면서, 로그인한 사람이 누구인지 알려 주는 방법을 표준으로 정한다. 사용자 정보 API를 한 번 더 불러 얻던 결과를, 토큰을 받을 때 표준 형식으로 함께 받게 된 셈이다.
그 방법으로 전달되는 것이 ID Token이다. IdP가 OIDC를 지원하고 서비스가 요청하면, 앞의 흐름 마지막 단계에서 JM렌터카는 Access Token과 함께 ID Token을 받는다. IdP는 누가 로그인했는지와 함께, 어느 IdP가 어느 서비스를 위해 언제 발급한 결과인지를 ID Token에 담고 서명해서 넘긴다.
claims(ID Token에 담긴 각 정보 항목)는 세 갈래로 나눠 볼 수 있다.
iss), 이 토큰을 받을 서비스(aud), 만든 시각과 만료 시각(iat, exp)이다. 사용자가 로그인을 마친 시각(auth_time)을 함께 넣는 IdP도 있다. JM렌터카는 이 토큰을 카카오가 만들었는지, 받을 서비스가 자기 자신인지, 아직 유효한지를 함께 확인한다. 받을 서비스를 확인하므로, 앞의 사례처럼 누군가 다른 서비스용으로 발급된 토큰을 들고 와도 로그인 결과로 받아들이지 않는다.sub라는 값이다. 카카오가 이 사용자를 가리키도록 매긴 식별자이고, 같은 IdP 안에서 다른 사용자와 겹치지 않으며 다른 사람에게 다시 배정되지도 않는다.카카오가 JM렌터카에 준 ID Token의 내용을 풀어 보면 다음과 같다. iss는 카카오가 정해 둔 값이고, 나머지 값은 예시다. 숫자는 시각을 나타낸다.
JM렌터카 서버가 카카오에서 이 토큰을 직접 받았다면, 그 통신 자체가 출처를 보증한다. 토큰이 브라우저나 앱 같은 다른 곳을 거쳐 전달되더라도, 카카오의 서명이 있으니 위조 여부를 확인할 수 있다. 서명을 확인하는 방법은 뒤에 따로 나올 심화 글에서 다룬다.
가입 양식은 ID Token에 담긴 이름과 이메일로 채운다. 더 많은 정보가 필요하거나 나중에 다시 조회할 때는 Access Token으로 카카오의 사용자 정보 API를 부른다.
다음에 로그인하면 JM렌터카는 iss와 sub를 묶어 지난번에 가입한 그 사용자를 알아본다. sub는 한 IdP 안에서만 겹치지 않으므로, JM렌터카가 다른 IdP도 함께 받는다면 발급자까지 봐야 한다. 이메일은 사용자가 바꿀 수 있어서 사용자를 알아보는 기준으로 쓰지 않는다.
두 토큰을 나란히 놓으면 이렇게 정리된다. Access Token은 JM렌터카가 카카오의 API에 내미는 값이다. ID Token은 JM렌터카가 직접 읽는 로그인 결과다.
이제 JM렌터카는 이 사용자를 안다. 하지만 여기서 ‘안다’는 말이 어디까지인지 정확히 봐야 한다.
카카오는 사용자가 카카오 계정에 등록한 이름과 이메일을 ID Token에 담아 전달한다. 카카오는 “이 계정으로 방금 로그인했다”는 사실을 보증한다. 이 사람이 운전을 해도 되는지까지 카카오가 보증하지는 않는다.
JM렌터카가 차를 내주기 전에 알아야 하는 것은 따로 있다. 면허가 유효한지, 그리고 성인(만 19세 이상)인지다. 이 둘을 보증할 수 있는 곳은 운전면허를 발급하는 경찰청이다.
정리하면 두 질문은 다르다. 로그인은 ‘이 계정이 전에 온 그 사람인가?’에 답한다. 운전 자격 확인은 ‘공식 기관이 이 사람의 자격을 보증하는가?’에 답한다. 운전 자격을 확인하는 방법이 생겨도 로그인이 필요 없어지지는 않는다.
가입은 끝났지만, 예약 마지막 단계에서 JM렌터카는 여전히 운전 자격을 확인해야 한다. 면허증 사진을 올리거나 면허번호를 입력하는 방법을 떠올릴 수도 있다. 하지만 글 첫머리의 장면에서는 사진도 번호도 없이, 휴대폰에 든 디지털 운전면허를 보여 주는 것으로 확인이 끝났다. 두 방식의 차이가 다음 편의 출발점이다.
웹 로그인에는 권한 위임과 인증·신원 전달이라는 서로 다른 역할이 있다.
JM렌터카가 로그인 계정을 알아보는 일과 운전면허라는 증명서를 확인하는 일은 같지 않다. 운전면허증을 지갑에 넣어 다니듯 사용자가 그 증명서를 직접 가지고 다닌다면 구조는 어떻게 달라질까?