OAuth는 "다른 서비스에 내 비밀번호를 넘기지 않고도 제한된 권한을 위임하는" 표준이다. "구글로 로그인", "깃허브로 연동" 같은 버튼 뒤에서 돌아가는 게 바로 이것이다. 세션(session)은 로그인에 성공한 뒤 "이 사람은 이미 인증됐다"는 상태를 서버가 기억하는 방식이다. 이 둘은 웹 로그인의 뼈대인데, 동시에 개발자가 가장 자주 실수하는 지점이기도 하다.
흥미로운 건, 이 영역의 사고 대부분이 표준 자체의 결함이 아니라 그걸 잘못 갖다 쓴 데서 나온다는 점이다. OAuth는 인증(누구인가)이 아니라 인가(무엇을 할 수 있는가)를 위한 틀인데, 이 구분을 놓치는 순간부터 함정이 시작된다. 나는 이 미묘한 차이를 이해하는 것이 웹 인증 보안의 절반이라고 생각한다.
OAuth는 인증이 아니라 인가의 도구다.
"이 앱에 권한을 준다"와 "이 사람이 로그인했다"는 다른 문제다. 이 둘을 혼동하면 남의 계정으로 로그인되는 취약점이 열린다.
state CSRF 방지 필수 파라미터 |
PKCE 인가 코드 가로채기 차단 |
HttpOnly 쿠키를 JS에서 못 읽게 |
만료 세션 수명· 재발급 관리 |
함정 1 — 인증과 인가를 혼동하기
가장 근본적인 실수다. OAuth로 받은 액세스 토큰은 "이 앱이 어떤 자원에 접근할 권한을 받았다"는 증표일 뿐, "이 사용자가 지금 로그인했다"는 증명이 아니다. 그런데 많은 구현이 "구글에서 토큰을 받았으니 이 사람은 이 이메일의 주인이 맞다"고 성급히 결론 내린다.
여기서 사고가 난다. 예를 들어 토큰이 정말 우리 앱을 위해 발급된 것인지 확인하지 않으면, 다른 앱용으로 발급된 토큰을 가져와 남의 계정으로 로그인하는 공격이 가능해진다. 순수 로그인 목적이라면 OAuth 위에 얹힌 신원 확인용 계층(OpenID Connect 같은)을 쓰고, 받은 신원 증표가 정말 우리 서비스를 대상으로, 신뢰할 발급자로부터 나왔는지를 반드시 검증해야 한다. "토큰을 받았다"와 "이 사람이 누구인지 안다"는 결코 같은 말이 아니다.
함정 2 — state와 PKCE를 빼먹기
OAuth 흐름에는 공격을 막기 위한 안전장치가 마련돼 있는데, 이걸 귀찮다고 생략하는 경우가 많다.
state 파라미터
로그인 요청을 시작할 때 예측 불가능한 임의의 값을 붙여 보내고, 돌아올 때 그 값이 일치하는지 확인하는 장치다. 이게 없으면 공격자가 자기 인가 코드를 피해자에게 떠넘겨 피해자가 공격자의 계정으로 로그인되게 만드는 등의 교차 요청 위조가 가능해진다. state는 선택이 아니라 필수인데, 놀랍게도 자주 빠진다.
PKCE
인가 코드가 중간에 탈취돼도 공격자가 그걸 토큰으로 바꾸지 못하게 막는 장치다. 요청을 시작한 쪽만 아는 비밀값을 걸어두고, 코드를 토큰으로 교환할 때 그 값을 함께 제시하게 한다. 원래는 모바일·단일 페이지 앱을 위해 나왔지만, 지금은 모든 유형의 클라이언트에 권장된다. 코드 가로채기 공격을 원천 차단하는 저렴하고 강력한 방어다.
함정 3 — 세션과 쿠키를 허술하게 다루기
로그인에 성공하면 그 상태를 세션으로 유지하는데, 이 세션 식별자(대개 쿠키)를 잘못 관리하면 애써 만든 인증이 무너진다. 흔한 실수를 표로 정리하면 이렇다.
| 항목 | 안전한 설정 | 흔한 실수 |
|---|---|---|
| 쿠키 접근 | HttpOnly로 JS 차단 | 스크립트로 읽기 허용 |
| 전송 구간 | Secure로 HTTPS 한정 | 평문 전송 허용 |
| 교차 사이트 | SameSite 지정 | 기본값 방치 |
| 로그인 후 | 세션 ID 재발급 | 같은 ID 유지 |
세션 쿠키에 HttpOnly를 걸면 자바스크립트가 그 값을 읽지 못해, 스크립트 주입(XSS) 공격이 성공해도 세션을 통째로 훔쳐가기 어려워진다. Secure를 걸면 암호화된 연결에서만 쿠키가 오간다. 또 하나 자주 놓치는 게 세션 고정(session fixation) 방어인데, 로그인에 성공하는 순간 세션 식별자를 새 값으로 갈아줘야 한다. 그렇지 않으면 공격자가 미리 심어둔 식별자를 피해자가 그대로 물고 로그인해, 공격자가 같은 세션에 올라타는 일이 벌어진다.
함정 4 — 로그아웃과 만료를 잊기
세션은 영원해선 안 된다. 적절한 만료 시간을 두고, 오래 앉아 있으면 다시 인증하도록 해야 한다. 로그아웃도 제대로 처리해야 한다. 화면에서 로그아웃 버튼만 눌렸을 뿐 서버 쪽 세션은 여전히 살아 있다면, 그 세션 식별자를 손에 쥔 공격자는 아무 일 없다는 듯 계속 접근한다. 로그아웃은 반드시 서버에서 해당 세션을 무효화하는 것까지여야 한다. 특히 여러 기기에서 로그인한 상태를 관리한다면, 한 곳에서의 로그아웃이나 비밀번호 변경 시 다른 세션들을 어떻게 처리할지 정책이 있어야 한다.
표준을 이기려 하지 말자
이 모든 함정을 관통하는 교훈은 하나다. 인증은 직접 발명하지 말고, 검증된 표준과 성숙한 라이브러리를 제대로 쓰라는 것이다. OAuth와 세션의 취약점은 대부분 "표준의 안전장치를 생략하거나 오해한" 데서 나온다. state와 PKCE를 켜고, 토큰의 대상과 발급자를 검증하고, 쿠키 속성을 안전하게 설정하고, 로그인 시 세션을 재발급하고, 만료와 로그아웃을 성실히 처리한다. 화려하지 않지만, 이 기본기들이 실제 사고의 대부분을 막는다. 나는 보안에서 가장 과소평가된 미덕이 "이미 잘 만들어진 걸 얌전히 따라 쓰는 절제"라고 믿는다.
자주 묻는 질문
OAuth로 로그인을 구현하면 안 되나요?
OAuth 자체는 권한 위임을 위한 틀이라, 순수 로그인에는 그 위에 얹힌 신원 확인 계층(OpenID Connect 같은)을 쓰는 것이 맞습니다. 그리고 받은 신원 증표가 정말 우리 서비스를 대상으로, 신뢰할 발급자에게서 나왔는지 반드시 검증해야 합니다. 이 검증을 빠뜨리면 남의 계정으로 로그인되는 취약점이 생깁니다.
state 파라미터를 꼭 써야 하나요?
필수입니다. 로그인 요청에 예측 불가능한 값을 붙이고 돌아올 때 일치를 확인함으로써 교차 요청 위조를 막습니다. 생략하면 공격자가 자신의 인가 코드를 피해자에게 떠넘기는 등의 공격에 노출됩니다. 함께 PKCE도 적용하면 코드 가로채기까지 방어됩니다.
세션 쿠키에 HttpOnly는 왜 중요한가요?
HttpOnly를 걸면 자바스크립트가 쿠키 값을 읽지 못합니다. 그래서 스크립트 주입(XSS) 공격이 일부 성공하더라도 세션 식별자를 통째로 훔쳐가기 어려워집니다. 여기에 HTTPS 전용인 Secure 속성과 SameSite 설정을 더하면 세션 탈취 위험이 크게 줄어듭니다.
로그아웃은 그냥 쿠키만 지우면 되나요?
충분하지 않습니다. 브라우저에서 쿠키를 지워도 서버가 그 세션을 여전히 유효한 것으로 알고 있으면, 그 식별자를 가진 공격자는 계속 접근할 수 있습니다. 로그아웃은 반드시 서버 측에서 해당 세션을 무효화하는 것까지 포함해야 합니다.

댓글 0