테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

인증과 인가

당신이 누구인지 확인하고, 무엇을 할 수 있는지 정하는 방법이다.

목차 (8)
  1. 인증과 인가는 다른 질문이다
  2. 비밀번호 저장: 되돌릴 수 없게, 일부러 느리게
  3. HTTPS가 있으면 브라우저에서 비밀번호를 암호화하지 않아도 되는 이유
  4. 다중 인증: 종류가 다른 요소를 겹친다
  5. 패스키: 비밀을 보내지 않는 로그인
  6. 세션과 토큰: 로그인 상태를 기억하는 두 방식
  7. OAuth 2.0과 OpenID Connect: 비밀번호를 건네지 않고 권한을 빌려준다
  8. 인가 모델: 역할, 속성, 그리고 객체마다 확인하기

인증과 인가는 다른 질문이다

인증(authentication)은 "당신은 누구인가"를 확인하는 일이고, 인가(authorization)는 "그 사람이 이 일을 해도 되는가"를 정하는 일이다. 건물로 치면 출입구에서 신분증을 보는 것이 인증, 신분이 확인된 사람의 출입 카드로 몇 층 문까지 열리는지 정하는 것이 인가다. 영어로는 둘을 줄여 authn, authz라고도 쓴다.

둘은 순서가 있다. 누구인지 모르면 권한을 따질 수 없으므로 인증이 먼저다. HTTP도 둘을 다른 상태 코드로 나눈다. 401(Unauthorized)은 이름과 달리 "누군지 모르니 인증하라"는 뜻이고, 403(Forbidden)은 "누군지는 알지만 이 일은 허락되지 않는다"는 뜻이다. 실제 사고는 인증보다 인가 쪽에서 더 자주 난다. 로그인은 한 곳에서 하지만, 권한 확인은 기능과 데이터마다 빠짐없이 해야 하기 때문이다.

요청이 인증과 인가 두 관문을 차례로 지나 실행되고, 인증에 실패하면 401, 인가에 실패하면 403이 된다요청주문 보기인증당신은 누구인가?인가이 일을 해도 되나?실행응답401 Unauthorized누군지 모른다: 다시 로그인403 Forbidden누군지는 알지만 권한이 없다아래 화살표: 그 관문에서 막힌 경우의 HTTP 응답
요청은 두 관문을 차례로 지난다. 인증에서 막히면 401, 인증은 됐지만 권한이 없으면 403이다.

비밀번호 저장: 되돌릴 수 없게, 일부러 느리게

서버는 비밀번호를 알 필요가 없다. 로그인 때 입력한 값이 처음 정한 값과 같은지만 확인하면 된다. 그래서 비밀번호는 암호화(되돌릴 수 있음)가 아니라 해시(hash, 되돌릴 수 없음)로 저장한다. 해시가 무엇인지는 암호 노트와 데이터 노트의 암호와 해시에 있고, 인코딩 도구에서 SHA-256 값을 직접 계산해 볼 수 있다.

해시만으로는 모자라다. 같은 비밀번호는 같은 해시가 되므로, 유출된 표에서 같은 값끼리 묶으면 누가 흔한 비밀번호를 쓰는지 바로 보이고, 흔한 비밀번호들의 해시를 미리 계산해 둔 표로 한꺼번에 찾을 수 있다. 이를 막는 것이 사용자마다 다른 무작위 값을 섞는 솔트(salt)다. 솔트는 비밀이 아니어서 해시 옆에 그대로 저장한다. 하는 일은 미리 계산한 표를 쓸모없게 만들고, 사용자 하나하나를 따로 맞혀 보게 만드는 것이다.

마지막 문제는 속도다. SHA-256 같은 일반 해시는 빠르게 만들어졌고, GPU로는 초당 수십억 번을 계산한다. 유출된 해시를 놓고 후보를 넣어 보는 공격(오프라인 추측)에 그 속도가 그대로 공격자의 속도가 된다. 그래서 비밀번호에는 일부러 시간과 메모리를 많이 쓰도록 만든 비밀번호 해시 함수(password hashing function, 키 유도 함수 KDF라고도 한다)를 쓴다. 로그인 한 번에 수십 밀리초는 사용자에게 티가 안 나지만, 수십억 개를 맞혀 보려는 쪽에는 감당하기 어려운 비용이 된다. 비용 설정값은 해시와 함께 저장해, 하드웨어가 빨라지면 다음 로그인 때 더 높은 값으로 다시 해시한다.

비밀번호 저장 단계 그림: 세 사용자의 저장값이 평문, 해시, 솔트 해시, 느린 해시로 바뀌며, 솔트를 넣으면 같은 비밀번호도 다른 값이 된다사용자데이터베이스에 저장된 값1. 평문 저장민지sunshine1같은 값준호sunshine1같은 값서연correct horse유출되면 모든 비밀번호가 그대로 보인다2. 해시만 저장민지df9bf4e3같은 값준호df9bf4e3같은 값서연a2f171bc같은 비밀번호는 같은 해시: 흔한 비밀번호 표로 한 번에 찾는다3. 솔트 + 해시민지a3f1:bebe2888준호9c2e:f5b328e8서연51bd:caa2961c사용자마다 다른 솔트: 같은 비밀번호도 값이 갈린다4. 솔트 + 느린 해시민지$slow$r=1000$a3f1$5d98fe7e준호$slow$r=1000$9c2e$f1024de0서연$slow$r=1000$51bd$fdea889a한 번 맞혀 보는 데 1000배 일이 든다 (실제는 Argon2id 등)5. 로그인 확인민지$slow$r=1000$a3f1$5d98fe7e준호$slow$r=1000$9c2e$f1024de0서연$slow$r=1000$51bd$fdea889a입력 "sunshine1" + 민지의 솔트 a3f1 → 1000번 해시→ 저장값과 같다: 로그인 통과같은 비밀번호가 드러난 줄로그인을 확인하는 줄
1 / 5

평문 저장: 데이터베이스가 한 번 새면 모든 비밀번호가 그대로 드러난다.

비밀번호 저장 방식 네 가지와 로그인 확인. 민지와 준호는 같은 비밀번호를 쓴다. 해시 값은 원리를 보이기 위한 8자리 장난감 해시이고, 실제 솔트는 16바이트 이상의 무작위 값이다.

되돌리지 않고 어떻게 맞는지 아나

흔히 묻는 것이 "솔트 때문에 같은 비밀번호도 매번 다른 값이 되는데, 로그인할 때는 어떻게 비교하나"다. 답은 복호화하지 않고 같은 계산을 한 번 더 한다는 것이다. 매번 달라지는 것은 가입하거나 비밀번호를 바꿀 때 솔트를 새로 뽑기 때문이고, 뽑은 솔트는 해시 결과와 함께 저장해 둔다. 로그인 때는 그 사용자의 저장값에서 솔트와 비용 설정을 꺼내, 입력한 비밀번호에 그 솔트를 붙여 같은 비용으로 다시 해시한다. 새로 나온 값이 저장된 해시 결과와 같으면 처음과 같은 비밀번호다.

Argon2·bcrypt 같은 라이브러리는 알고리즘, 비용 설정, 솔트, 해시 결과를 PHC 문자열 형식 같은 문자열 하나에 묶어 저장한다. 그래서 코드에서는 솔트를 따로 다룰 일이 없다.

저장된 비밀번호 해시 문자열은 알고리즘, 버전, 비용 설정, 솔트, 해시 결과로 이뤄진다. 로그인 때 입력한 비밀번호에 문자열 속 솔트와 비용을 써서 다시 계산하고, 결과를 저장된 해시와 비교한다DB에 저장되는 문자열 하나 (예시 값)$argon2id알고리즘$v=19버전$m=19456,t=2,p=1비용 설정$c2FsdHNhbHQ솔트$Rk8xd2…해시 결과로그인할 때입력한 비밀번호"correct horse"문자열에서 꺼낸솔트 · 비용 설정Argon2id로다시 계산저장된 해시 결과와같은가?같으면 통과, 다르면 거절(상수 시간으로 비교)되돌리는 단계는 없다. 같은 재료로 같은 계산을 한 번 더 할 뿐이다.
저장된 해시 문자열의 다섯 부분과 로그인 때의 확인. m=19456(KiB, 19 MiB), t=2, p=1은 위 표의 OWASP 최소 설정이다. 솔트·해시 값은 형식을 보이기 위한 예시다.
# Python, argon2-cffi
from argon2 import PasswordHasher

hasher = PasswordHasher()
stored = hasher.hash("correct horse")   # 가입: 솔트를 새로 뽑아 문자열에 함께 담는다
hasher.verify(stored, "correct horse")  # 로그인: 문자열 속 솔트·비용으로 다시 계산해 비교 → 통과
hasher.verify(stored, "wrong")          # 다르면 VerifyMismatchError
hasher.check_needs_rehash(stored)       # 비용 설정을 올렸다면 True → 이번 로그인 때 다시 해시해 저장

비교는 상수 시간으로 한다. 앞에서부터 한 글자씩 보다가 다르면 바로 멈추면, 걸린 시간 차이로 몇 글자까지 맞았는지가 샌다. verify 같은 라이브러리 함수는 이것을 알아서 한다(argon2-cffi API). 직접 비교해야 한다면 hmac.compare_digest처럼 상수 시간 비교 함수를 쓴다.

OWASP Password Storage Cheat Sheet가 권하는 선택지와 최소 설정은 다음과 같다.

함수특징OWASP 최소 설정
Argon2id2015년 비밀번호 해시 대회 우승작. 메모리를 많이 써서 GPU·전용 칩으로 병렬화하기 어렵다. RFC 9106으로 표준화됐다메모리 19 MiB, 반복 2, 병렬 1. 새로 만든다면 첫 선택
scrypt메모리를 많이 쓰게 설계한 앞 세대 함수N=2^17, r=8, p=1 (Argon2id를 쓸 수 없을 때)
bcrypt1999년부터 쓰인 검증된 함수. 입력이 72바이트에서 잘린다작업 계수 10 이상. 이미 쓰는 시스템에서
PBKDF2HMAC을 여러 번 반복한다. 메모리를 쓰지 않아 GPU에 약하지만 FIPS 인증 환경에서 쓸 수 있다HMAC-SHA-256으로 600,000회 이상 (FIPS-140이 필요할 때)

사용자에게 무엇을 요구할지는 미국 표준 기술 연구소(NIST)의 디지털 신원 지침 SP 800-63B(개정 4판)가 기준으로 많이 쓰인다. 흔히 보던 규칙 몇 가지는 오히려 하지 말라고 적혀 있다. 대문자·숫자·특수문자를 섞으라는 규칙은 사람들이 "Password1!"처럼 뻔한 변형을 고르게 만들 뿐 효과가 작았다는 것이 유출 데이터 분석의 결론이다.

항목SP 800-63B 개정 4판의 요지
길이비밀번호만으로 로그인한다면 15자 이상, 다중 인증의 한 요소라면 8자 이상. 최대 길이는 64자 이상 허용
조합 규칙문자 종류를 섞으라는 규칙을 강제하지 않는다. 공백과 유니코드도 받는다
금지 목록흔한 비밀번호, 예상하기 쉬운 값, 유출된 비밀번호 목록과 비교해 걸리면 다른 것을 고르게 하고 이유를 알려 준다
주기적 변경정기적으로 바꾸라고 요구하지 않는다. 유출 증거가 있을 때만 바꾸게 한다
비밀번호 관리자자동 채우기와 붙여넣기를 막지 않는다. 관리자를 쓰면 더 강한 비밀번호를 고르게 된다
추측 제한로그인 실패가 이어지면 시도 횟수를 제한한다. 금지 목록이 작아도 되는 이유가 이것이다

HTTPS가 있으면 브라우저에서 비밀번호를 암호화하지 않아도 되는 이유

로그인 폼에서 비밀번호를 서버로 보내기 전에 자바스크립트로 해시하거나 공개키로 암호화하는 경우가 많다. "평문 비밀번호가 네트워크로 나가면 안 된다"는 생각에서다. 하지만 HTTPS를 쓰면 그 걱정은 이미 TLS가 맡고 있고, 브라우저에서 한 번 더 하는 작업은 대개 보호를 더하지 못한다.

비밀번호 전송 방식 세 가지 비교. 평문 HTTP는 비밀번호가 보이고, 브라우저에서 해시만 하면 가로챈 해시로 그대로 로그인할 수 있으며, HTTPS는 중간에서 암호문만 보이고 서버가 받은 비밀번호를 느린 해시로 저장한다브라우저중간에서 엿보는 사람서버HTTP평문로그인correct horse✗ 비밀번호가 그대로 보인다받은 값을 해시HTTP +브라우저 해시로그인f3a9…(해시)✗ 해시를 그대로 다시 보내 로그인해시가 곧 비밀번호HTTPS(TLS)로그인알아볼 수 없는 암호문✓ 읽지도 바꾸지도 못한다복호화 뒤 Argon2id브라우저 해시는 TLS를 대신하지 못하고, 서버의 느린 해시도 대신하지 못한다.
중간에서 엿보는 사람이 보는 것. 브라우저에서 해시만 하면 그 해시가 새 비밀번호가 되어, 가로챈 값을 그대로 보내 로그인할 수 있다. HTTPS에서는 내용을 읽지도 바꾸지도 못하고, 서버가 받은 비밀번호를 느린 해시로 저장한다.
  • TLS가 이미 전송 구간을 지킨다: 오가는 내용을 암호화하고(기밀성), 중간에서 바꾸면 알아채며(무결성), 상대가 진짜 그 서버인지 인증서로 확인한다(인증). 비밀번호도 이 안에서 암호문으로만 지나간다.
  • 브라우저 해시는 새 비밀번호가 될 뿐이다: 서버는 받은 해시가 맞는지만 보므로, 해시를 가로챈 사람은 원래 비밀번호를 몰라도 그 해시를 그대로 보내 로그인한다. 서버가 그 값을 그대로 저장하면 DB가 새는 순간 바로 쓸 수 있는 로그인 정보가 된다. 그래서 서버는 받은 값이 무엇이든 다시 느린 해시로 저장해야 하고, 결국 브라우저 해시가 빠진 셈이 된다.
  • 그 자바스크립트도 같은 서버가 보낸다: TLS가 깨진 상황이라면 공격자는 해시하는 스크립트 자체를 바꿔 평문을 빼돌릴 수 있다. TLS가 멀쩡하다면 스크립트 암호화는 겹칠 뿐이다. 어느 쪽이든 브라우저 쪽 암호화가 TLS를 대신하지 못한다.
  • 직접 만든 암호화는 새 문제를 부른다: 페이지에 공개키를 넣어 비밀번호를 RSA로 암호화하는 식은 키 교체, 같은 암호문을 다시 보내는 재전송, 패딩 실수 같은 문제를 스스로 풀어야 한다. TLS가 이미 표준으로 풀어 둔 것들이다.

대신 해야 할 일은 이렇다. 모든 페이지와 API를 HTTPS로만 받고 HSTS로 첫 요청까지 막는다(보안 › 네트워크). 서버에서 Argon2id 같은 느린 해시로 저장한다. 로드 밸런서에서 TLS를 풀었다면 그 뒤 내부 구간도 암호화한다. 그리고 요청 본문이 로그에 그대로 남아 비밀번호가 로그 파일에 찍히는 실수를 막는다. 큰 서비스들도 평문 비밀번호가 내부 로그에 남아 있던 것을 뒤늦게 찾아 사용자에게 비밀번호 변경을 권한 일이 있었다.

브라우저에서 키를 만드는 것이 의미 있는 경우도 있다. 서버조차 내용을 몰라야 하는 종단 간 암호화다. 비밀번호 관리자나 종단 간 암호화 백업은 브라우저·앱에서 비밀번호로 암호화 키를 유도하고, 서버에는 암호문만 맡긴다. 이때도 로그인용 값과 암호화 키는 서로 다른 유도 경로로 만든다. 비밀번호를 아예 서버에 보내지 않는 방법도 있다. 비밀번호 기반 키 교환(PAKE)인 OPAQUE(RFC 9807)와, 비밀 자체가 없는 아래의 패스키다.

다중 인증: 종류가 다른 요소를 겹친다

비밀번호는 피싱, 재사용, 유출로 언제든 남의 손에 들어갈 수 있다. 다중 인증(MFA, multi-factor authentication)은 종류가 다른 증거를 둘 이상 요구해, 하나가 새도 그것만으로는 들어올 수 없게 한다. 인증 요소는 전통적으로 셋으로 나눈다. 아는 것(비밀번호, PIN), 가진 것(휴대폰, 보안 키), 나인 것(지문, 얼굴)이다. 같은 종류를 두 번 묻는 것, 예를 들어 비밀번호 다음에 보안 질문을 묻는 것은 다중 인증이 아니다.

두 번째 요소도 방식마다 강도가 다르다. 문자로 받는 일회용 코드는 번호를 빼앗는 유심 바꿔치기(SIM swap)나 가로채기에 약하다. 앱이 만드는 시간 기반 일회용 코드(TOTP)는 그보다 낫지만, 사용자가 가짜 사이트에 코드를 직접 입력하면 공격자가 그 코드를 곧바로 진짜 사이트에 쓸 수 있다. 푸시 승인은 알림을 계속 보내 지친 사용자가 누르게 만드는 수법(MFA 피로 공격)에 노출된다. 출처에 묶인 공개키 서명을 쓰는 보안 키와 패스키만이 피싱 자체에 버틴다. 각 방식의 장단점은 OWASP Multifactor Authentication Cheat Sheet에 정리되어 있다.

인증 요소 세 종류와, SMS 코드에서 패스키까지 두 번째 요소가 피싱에 버티는 정도를 나타낸 눈금아는 것knowledge비밀번호, PIN가진 것possession휴대폰, 보안 키나인 것inherence지문, 얼굴두 번째 요소가 피싱에 버티는 정도SMS 코드가로채기·번호 탈취앱 OTP가짜 사이트에 입력 가능푸시 승인무심코 누르기 쉽다패스키·보안 키출처에 묶여 피싱 불가종류가 다른 요소를 둘 이상 겹쳐야 다중 인증이다 (비밀번호 + 보안 질문은 한 종류)
인증 요소 세 종류와, 두 번째 요소 방식별로 피싱에 버티는 정도. 오른쪽으로 갈수록 사용자가 속아도 공격자가 쓸 수 있는 것이 적다.

다중 인증은 복구 경로까지 같은 강도여야 의미가 있다. 두 번째 요소를 잃었을 때 이메일 링크 하나로 다시 설정할 수 있다면, 공격자도 그 길로 들어온다. 복구 코드를 미리 발급해 두거나 요소를 둘 이상 등록하게 하는 이유다.

서버 쪽에서는 공격자가 유출된 비밀번호 목록을 대량으로 넣어 보는 무차별 대입과 크리덴셜 스터핑(사람과 운영 노트)에도 대비한다. OWASP Credential Stuffing Prevention Cheat Sheet는 다중 인증을 가장 강한 방어로 꼽고, 그게 어려우면 여러 겹을 함께 쓰라고 권한다. 실패가 늘수록 단계적으로 늦추는 속도 제한, 새 기기나 수상한 주소에서만 요구하는 추가 인증, 유출된 비밀번호 목록과의 대조, 비밀번호는 맞았는데 두 번째 요소에서 막힌 경우의 사용자 알림이다. IP 차단은 공격자가 수많은 프록시 주소를 돌려 쓰므로 주된 방어가 될 수 없고, 계정을 오래 잠그면 공격자가 남의 계정을 일부러 잠그는 수단이 되므로 잠금은 짧게, 본인 확인과 묶어 둔다.

패스키: 비밀을 보내지 않는 로그인

패스키(passkey)는 공개키 암호로 비밀번호를 대신하는 로그인이다. 웹 표준 이름은 WebAuthn(Web Authentication)이고, 이를 포함한 규격 묶음을 FIDO2라고 부른다. 가입할 때 기기가 그 사이트 전용 키 쌍을 만들어 공개키만 서버에 맡기고, 로그인할 때는 서버가 보낸 무작위 값(챌린지, challenge)에 개인키로 서명해 돌려준다. 공개키 암호의 원리는 암호 노트에 있다.

이 구조에서 세 가지가 따라 나온다. 첫째, 서버에는 공개키만 있어서 서버의 데이터베이스가 유출돼도 로그인에 쓸 비밀이 없다. 둘째, 챌린지가 매번 새로 바뀌어 지난번 응답을 다시 보내는 공격(재전송)이 통하지 않는다. 셋째, 브라우저가 지금 접속한 출처(origin)를 서명할 내용에 넣고 키도 사이트마다 따로 쓰므로, 생김새가 똑같은 가짜 사이트에서는 진짜 사이트용 서명을 얻을 수 없다. 사용자가 속아도 피싱이 성립하지 않는 것이다. 지문이나 얼굴은 기기 안에서 개인키를 꺼내 쓰는 잠금장치일 뿐, 생체 정보가 서버로 가지 않는다.

패스키 단계 그림: 인증기, 브라우저, 서버 세 줄 사이로 공개키 등록, 챌린지, 서명, 서명 확인이 차례로 오간다인증기휴대폰·보안 키브라우저출처를 붙인다서버example.com공개키공개키 등록챌린지(무작위)챌린지 + 출처서명서명 전달이 사이트 전용 키 쌍 생성공개키 저장지문·PIN으로 잠금 해제공개키로 서명 확인 ✓개인키: 인증기 안에만 있다공개키: 서버에 맡겨도 된다
1 / 5

등록: 인증기가 이 사이트만을 위한 키 쌍을 만들고, 공개키만 서버에 보낸다. 개인키는 기기 밖으로 나가지 않는다.

패스키 로그인의 흐름. 서버에는 공개키만 있어서 서버가 털려도 로그인에 쓸 비밀이 새지 않고, 서명이 출처에 묶여 피싱 사이트에서는 쓸 수 없다.

패스키는 기기 하나에 묶인 것(보안 키)과 운영체제 계정이나 비밀번호 관리자를 통해 여러 기기로 동기화되는 것이 있다. 동기화 패스키는 휴대폰을 잃어도 되찾을 수 있는 대신, 그 동기화 계정의 보안이 중요해진다. 개념과 지원 현황은 FIDO Alliance의 패스키 소개, 브라우저 API는 MDN의 Web Authentication API 문서에 있다.

세션과 토큰: 로그인 상태를 기억하는 두 방식

HTTP는 요청마다 따로 논다. 한 번 로그인한 뒤에도 요청마다 "아까 그 사람"임을 알려야 하는데, 방식은 크게 둘이다. 세션(session)은 서버가 로그인 상태를 저장해 두고 브라우저에는 추측할 수 없는 무작위 번호(세션 ID)만 쿠키로 준다. 서버는 요청마다 그 번호로 저장소를 찾아본다. 자체 포함 토큰은 "이 사람은 민지, 15분 뒤 만료"라는 내용 자체를 서명해서 건네고, 서버는 저장소를 찾지 않고 서명만 확인한다. 대표 형식이 RFC 7519의 JWT(JSON Web Token)다. JWT의 내용은 base64url로 적혀 있을 뿐 암호화된 것이 아니어서 누구나 읽을 수 있다.

세션 쿠키는 서버가 세션 저장소를 찾아 사용자를 알아내고, JWT는 서버가 서명만 확인하고 토큰에 적힌 내용을 그대로 믿는다세션 쿠키: 서버가 기억한다브라우저 쿠키sid=8f3a…서버저장소에서 찾기세션 저장소8f3a… → 민지, 30분로그아웃·강제 종료: 저장소에서 지우면 바로 끝난다토큰(JWT): 토큰이 스스로 말한다브라우저가 든 토큰header.payload.sig서버서명만 확인토큰 안의 내용sub=민지, exp=15분찾아볼 곳이 없어 빠르지만, 만료 전에 무르기 어렵다채운 상자: 서버가 "이 사람은 민지다"라고 믿게 되는 근거
세션 쿠키는 서버가 기억하고, JWT는 토큰이 스스로 말한다. 앞은 지우면 바로 끝나고, 뒤는 찾아볼 곳이 없어 빠르지만 만료 전에 무르기 어렵다.
함정무엇이 문제인가대책
로그인 후에도 같은 세션 ID로그인 전에 심어 둔 번호가 로그인 뒤에도 유효하면 남이 그 세션을 이어 쓴다(세션 고정)로그인·권한 변경 때 세션 ID를 새로 발급한다
자바스크립트가 읽는 곳에 보관localStorage에 둔 토큰은 XSS 한 번이면 통째로 빠져나간다HttpOnly·Secure·SameSite 쿠키에 둔다(웹 보안 노트)
토큰 서명 검사가 느슨함토큰이 스스로 밝힌 알고리즘을 그대로 믿거나 서명 없는 토큰(alg: none)을 받아들이면 위조된다서버가 쓸 알고리즘과 키를 미리 정해 두고, 발급자·대상·만료를 모두 확인한다
무를 수 없는 긴 토큰로그아웃하거나 기기를 잃어도 만료 전까지 유효하다액세스 토큰은 짧게, 오래 가는 갱신 토큰(refresh token)은 서버에서 무를 수 있게 둔다
로그인 뒤의 쿠키를 훔친다다중 인증은 로그인할 때만 묻는다. 정보 탈취 악성코드나 중간에서 중계하는 피싱 프록시가 로그인 뒤의 세션 쿠키를 가져가면 다중 인증을 다시 거치지 않고 들어온다(MITRE ATT&CK T1539)세션 수명을 줄이고, 결제·비밀번호 변경 같은 민감한 작업 앞에서 다시 인증하고, 갑자기 바뀐 위치·기기를 감시한다. 로그인 단계는 피싱에 버티는 패스키로 막는다

어느 쪽이 더 안전하다기보다 쓰임이 다르다. 한 서비스 안의 웹 로그인이라면 세션 쿠키가 단순하고 무르기도 쉽다. 여러 서비스가 같은 신원을 확인해야 하거나 서버가 저장소를 공유하기 어려울 때 토큰이 쓸모 있다. 세션 관리 전반은 OWASP Session Management Cheat Sheet, JWT의 안전한 사용법은 RFC 8725(JWT Best Current Practices)에 있다.

OAuth 2.0과 OpenID Connect: 비밀번호를 건네지 않고 권한을 빌려준다

"구글 계정으로 계속하기"나 "이 앱이 내 캘린더를 읽도록 허용"의 뒤에는 OAuth 2.0(RFC 6749)이 있다. 예전에는 다른 서비스의 데이터를 쓰려면 그 서비스의 비밀번호를 앱에 알려 줘야 했다. OAuth는 사용자가 인가 서버에서 직접 동의하면 앱이 범위(scope)와 기한이 정해진 액세스 토큰만 받게 한다. 앱은 비밀번호를 끝내 보지 못하고, 사용자는 언제든 그 앱의 권한만 거둘 수 있다.

이름 그대로 OAuth는 인가 규격이다. 액세스 토큰은 "이 토큰을 든 쪽에 캘린더 읽기를 허락한다"는 뜻일 뿐 "이 사람이 민지다"라는 증명이 아니다. 로그인에 쓰려면 그 위에 얹은 OpenID Connect(OIDC)가 필요하다. OIDC는 같은 흐름에서 누가 언제 어느 앱을 위해 로그인했는지 적어 서명한 ID 토큰을 함께 준다. 자세한 규칙은 OpenID Connect Core 1.0에 있다.

회사에서 한 번 로그인으로 메일, 메신저, 사내 시스템에 모두 들어가는 것을 단일 로그인(SSO, single sign-on)이라 한다. 로그인을 맡는 신원 공급자(IdP, identity provider)가 신원을 확인하고, 각 서비스(SP, service provider)는 IdP가 서명해 준 증명만 믿는다. 새 서비스는 OIDC를 많이 쓰고, 기업용 SaaS에서는 XML로 쓴 서명된 주장(assertion)을 주고받는 SAML 2.0(2005년 OASIS 표준)도 여전히 흔하다. SSO는 비밀번호를 하나로 줄이고 퇴사자 계정을 한곳에서 끊게 해 주지만, 그만큼 IdP 계정 하나가 모든 문의 열쇠가 되므로 IdP 로그인에 가장 강한 다중 인증을 건다.

여러 흐름 가운데 지금 권장되는 것은 인가 코드 흐름(authorization code flow)에 PKCE(Proof Key for Code Exchange, RFC 7636)를 더한 것이다. 인가 코드는 브라우저 주소를 거쳐 앱에 오므로 도중에 새어 나갈 수 있다. PKCE는 앱이 처음에 비밀 값의 해시만 보내고 코드를 토큰으로 바꿀 때 원래 값을 보여 주게 해서, 코드만 가로챈 쪽은 토큰을 받지 못하게 한다. 해시를 되돌릴 수 없다는 성질이 여기서도 쓰인다.

OAuth PKCE 단계 그림: 앱, 브라우저, 인가 서버, API 네 줄 사이로 challenge, 로그인, 인가 코드, verifier 확인, 토큰, API 호출이 차례로 오간다앱클라이언트브라우저사용자인가 서버로그인·토큰API리소스 서버리다이렉트challenge·state인가 코드코드 전달코드 + verifier토큰 발급액세스 토큰으로 호출verifier 만들고 해시로그인 · 권한 동의해시(verifier) = challenge ✓verifier는 앱만 안다 · challenge는 공개돼도 된다(해시는 되돌릴 수 없다)
1 / 7

앱이 매번 새 무작위 code_verifier(dBjftJeZ4C…)를 만들고, 그 SHA-256 값인 code_challenge(E9Melhoa2O…)를 계산해 둔다.

OAuth 2.0 인가 코드 흐름과 PKCE. 중간에 인가 코드를 가로챈 쪽은 앱만 가진 code_verifier가 없어서 코드를 토큰으로 바꾸지 못한다. 예시 값은 RFC 7636 부록 B의 것이다.

2025년에 나온 RFC 9700(OAuth 2.0 Security Best Current Practice)은 그동안 드러난 문제를 모아 정리했다. 토큰을 주소에 바로 실어 보내던 암묵적 흐름(implicit)과 앱이 사용자 비밀번호를 직접 받는 흐름(password grant)은 쓰지 않고, 모든 클라이언트에 PKCE를 쓰며, 돌아올 주소(redirect URI)는 미리 등록한 값과 정확히 같을 때만 받아들이라는 것이 요지다.

인가 모델: 역할, 속성, 그리고 객체마다 확인하기

권한 규칙을 적는 방식은 몇 가지 틀로 나뉜다. 서비스가 작을 때는 역할 하나로 충분하지만, 조직이 커지고 데이터마다 주인이 생기면 사용자·자원·상황의 속성을 함께 보게 된다. 속성 기반 모델은 NIST SP 800-162가 개념을 정리했다.

모델판단 기준예잘 맞는 곳
ACL(접근 제어 목록)자원마다 허락된 사용자 목록이 문서는 민지와 준호가 편집 가능파일 공유처럼 자원마다 따로 정할 때
RBAC(역할 기반)사용자의 역할관리자는 모든 주문을 본다직무가 분명한 조직, 관리 화면
ABAC(속성 기반)사용자·자원·환경의 속성 조합같은 부서 사람이 업무 시간에만 본다규칙이 세밀하고 자주 바뀌는 곳
관계 기반(ReBAC)사용자와 자원 사이의 관계폴더를 공유받으면 안의 파일도 본다공유와 소유가 얽힌 협업 서비스

어떤 모델을 쓰든 가장 흔한 사고는 확인을 빠뜨리는 것이다. 주소나 요청에 들어 있는 번호(/orders/1002)만 보고 데이터를 내주면, 로그인한 누구든 번호를 바꿔 남의 데이터를 볼 수 있다. 이를 IDOR(Insecure Direct Object Reference, 안전하지 않은 직접 객체 참조)라고 한다. 번호를 추측하기 어렵게 바꾸는 것은 보조 수단일 뿐이고, 해법은 요청마다 서버에서 "이 사용자가 이 객체에 이 일을 해도 되는가"를 확인하는 것이다.

OWASP Top 10에서 접근 통제 실패(Broken Access Control)가 첫째 자리에 있는 이유도 이 확인이 기능마다 흩어져 빠지기 쉽기 때문이다.

민지가 준호의 주문 번호를 요청했을 때, 소유자를 확인하면 403으로 거부되고 확인을 빠뜨리면 남의 주문이 보이는 IDOR가 된다민지(일반 회원)GET /orders/1002인증 ✓로그인한 사람: 민지인가 확인주문 1002: 준호의 것403 거부이유: 준호의 주문확인을 빠뜨리면준호의 주문이 보인다 (IDOR)점선: 번호만 믿고 소유자를 안 본 경우 · 실선: 요청마다 서버에서 권한을 확인한 경우
로그인한 민지가 남의 주문 번호를 요청한 경우. 인증은 통과했으므로, 주문의 주인을 확인하는 인가 단계가 없으면 그대로 남의 데이터가 나간다.
원칙뜻
기본은 거부허락하는 규칙에 걸리지 않으면 거부한다. 새 기능이 규칙을 잊어도 열리지 않는다
서버에서 확인버튼을 숨기는 것은 화면 정리일 뿐이다. 요청은 누구나 직접 보낼 수 있다
요청마다, 객체마다목록에서 한 번 거른 것으로 끝내지 않고, 읽기·수정·삭제 요청마다 그 객체의 주인과 권한을 본다
한 곳에 모으기권한 판단을 공용 함수나 미들웨어로 모아, 기능마다 제각각 구현하다 빠뜨리지 않게 한다
최소 권한필요한 만큼만 준다(보안의 기초)

실무 점검 목록은 OWASP Authorization Cheat Sheet와 OWASP IDOR Prevention Cheat Sheet에 있다.