인증은 사용자가 누구인지 확인하는 검사이고, 인가는 요청마다 무엇을 할 수 있는지 판단하는 결정으로, 안전한 시스템에는 둘 다 필수입니다. 호텔이 이를 구체적으로 보여 줍니다. 프런트에서 제시하는 신분증이 인증이고, 412호를 여는, 그리고 412호만 여는 키카드가 인가(권한 부여)입니다. 둘은 서로 다른 질문이며, 서로 다른 시점에 제기되고, 서로 다른 장치가 답하며, 서로 다른 방식으로 실패합니다. 둘을 뭉뚱그리는 시스템이 두 종류의 침해를 모두 겪는 이유입니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 인증(AuthN) | 누구입니까? – 자격 증명 → 신원, 세션당 한 번 |
| 인가(AuthZ) | 무엇을 할 수 있습니까? – 정책 → 허용/거부, 모든 요청마다 |
| HTTP 코드 | 401 = 미인증(잘못 붙은 이름) · 403 = 신원은 알지만 거부 |
| 프로토콜의 구분 | OIDC/SAML/패스키 = 인증 · OAuth 범위/RBAC/ACL = 인가 |
| 실패의 구분 | 인증 실패는 계정 탈취로 · 인가 실패는 권한 상승으로 |
하나의 요청에서 인증과 인가가 실행되는 방식
1 POST /login (자격 증명 + MFA) ── 인증, 한 번
← 세션 토큰 / 쿠키: 신원 확립
2 GET /documents/xKd91m ── 이후의 모든 요청:
a · 토큰 검증 → 신원 재확립 (인증 장치)
b · 권한 평가 → ADA가 이 문서를 읽어도 되는가? (인가 결정)
→ 200 둘 다 통과
→ 401 토큰 없음/무효 – WWW-Authenticate 챌린지; 로그인하면 해결됨
→ 403 토큰은 유효, 권한 거부 – 다시 로그인해도 달라지는 것 없음
→ 404 일부 API는 금지된 객체를 아예 숨김 (RFC 9110이 허용)
애플리케이션 코드에서의 같은 분리입니다. 로그인은 한 번, 판정은 작업마다 받습니다:
// JavaScript / Node.js — Back4app JS SDK
// Authentication: once — who are you?
const user = await Parse.User.logIn('ada', password); // session token issued
// Authorization: every request — may YOU do THIS?
const doc = await new Parse.Query('Document').get('xKd91m');
// found if an ACL entry grants ada read · "not found" if not
doc.set('title', 'Renamed');
await doc.save(); // succeeds only if an entry grants ada WRITE // Flutter / Dart — Back4app Flutter SDK
// Authentication: once — who are you?
final user = ParseUser('ada', password, null);
await user.login(); // session token issued
// Authorization: every request — may YOU do THIS?
final response = await QueryBuilder<ParseObject>(ParseObject('Document'))
.query(); // returns only rows whose ACLs grant ada read
final doc = response.results!.first as ParseObject;
doc.set('title', 'Renamed');
await doc.save(); // succeeds only if an entry grants ada WRITE // iOS / Swift — Back4app Swift SDK
// Authentication: once — who are you?
let user = try await User.login(username: "ada", password: password)
// session token issued
// Authorization: every request — may YOU do THIS?
guard var doc = try await Document.query("objectId" == "xKd91m").first()
else { return } // found only if an ACL entry grants ada read
doc.title = "Renamed"
_ = try await doc.save() // succeeds only if an entry grants ada WRITE // Android / Kotlin — Back4app Android SDK
// Authentication: once — who are you?
val user = ParseUser.logIn("ada", password) // session token issued
// Authorization: every request — may YOU do THIS?
val doc = ParseQuery.getQuery<ParseObject>("Document").get("xKd91m")
// found if an ACL entry grants ada read · "not found" if not
doc.put("title", "Renamed")
doc.save() // succeeds only if an entry grants ada WRITE 인증 vs. 인가, 나란히 비교
| 인증 | 인가 | |
|---|---|---|
| 질문 | 누구입니까? | 무엇을 할 수 있습니까? |
| 입력 | 자격 증명: 비밀번호, MFA 코드, 패스키, 생체 인식 | 정책: 역할, ACL, 속성, 범위 |
| 빈도 | 세션당 한 번(+ 민감한 동작에는 단계적 인증) | 모든 요청, 모든 객체마다 |
| 산출물 | 세션 또는 토큰 | 허용/거부 결정 |
| 사용자에게 보이는가 | 예 – 로그인 화면 | 거의 없음 – 보이지 않게 동작 |
| 실행 위치 | 경계부: ID 계층, 로그인 흐름 | 깊은 곳: 비즈니스 계층과 데이터 계층, 데이터 바로 옆 |
| 프로토콜 | OpenID Connect, SAML, WebAuthn | OAuth 2.0 범위, RBAC/ABAC/ReBAC 엔진 |
| 대표 토큰 | ID 토큰 – 누가 인증했는가 | 액세스 토큰 – 소지자가 무엇을 할 수 있는가 |
| 실패 형태 | 계정 탈취 | 권한 상승, 데이터 노출 |
두 행에는 각주가 필요합니다. 실행 위치: 인증은 자연스럽게 경계부에 집중됩니다. 로그인 흐름 하나, ID 공급자(IdP) 하나입니다. 반면 인가는 보호하는 데이터 옆에 있어야 합니다. “이 사용자가 이 객체를 만져도 되는가”에 답하려면 그 객체가 필요하기 때문입니다. 실패 형태: 위협 모델이 서로 겹치지 않습니다. 크리덴셜 스터핑과 피싱은 인증을 공격하며(MFA가 가장 효과적인 인증 방어 수단인 이유입니다), 객체 수준 인가 취약점(BOLA)은 대표적인 인가 실패로 API 보안 목록의 최상위를 차지합니다. 로그인한 사용자, 엉뚱한 객체, 아무도 확인하지 않은 상황입니다.
401과 403, 정확하게
상태 코드는 이 구분에 숫자를 입힌 것이며, RFC 9110은 이를 정확하게 규정합니다. 401 Unauthorized는 역사상 가장 오래 살아남은 잘못된 이름입니다. 실제 의미는 미인증으로, 요청에 “유효한 인증 자격 증명이 없다”는 뜻이며, 응답에는 WWW-Authenticate 챌린지가 실려야 하고, 자격 증명을 제시하면 해결될 수 있습니다. 403 Forbidden은 서버가 누가 요청했는지 정확히 알면서도 거부한다는 뜻입니다. 정의상 다시 인증해도 소용이 없습니다. 그리고 명세는 보안 팀이 선호하는 세 번째 선택지도 허용합니다. 403 대신 404로 답하여, 금지된 리소스가 자신의 존재를 확인해 주지 않게 하는 것입니다. 코드를 올바르게 쓰는 것은 현학이 아닙니다. 클라이언트는 이 코드 위에 재시도와 재로그인 로직을 구축하며, 401이어야 할 자리의 403은 사용자를 로그인 폼 대신 벽으로 보냅니다.
OAuth, OIDC, 그리고 끝없는 혼동
이 혼동에 대한 답은 명세에 있습니다. OAuth 2.0의 제목은 “The OAuth 2.0 Authorization Framework”입니다. 범위가 지정된 권한을 옮기는 프레임워크이며, OpenID Connect 명세는 OAuth만으로는 “최종 사용자의 인증에 관한 정보를 제공할 수 없기” 때문에 존재합니다. OIDC는 신원 계층을 더합니다. 누가 인증했는가를 단언하는 ID 토큰으로, 소지자가 무엇을 할 수 있는가를 단언하는 액세스 토큰과는 구별됩니다. 모든 ”…로 로그인” 버튼은 OAuth의 인가 배관 위에서 OIDC가 인증을 수행하는 것입니다. 하나의 흐름에 두 개념이 혼동되지 않고 깔끔하게 층을 이룹니다.
프로덕션에 배포되는 세 가지 실수
클라이언트 측 인가. 삭제 버튼을 숨기는 것은 인터페이스 설계이고, 삭제가 실행될지를 서버가 결정하는 것이 보안입니다. OWASP에 따르면 기본적으로 거부하고, 서버 측에서 강제하며, 클라이언트 검사는 UX 힌트로 취급해야 합니다. 로그인 ≠ 허용. 인증은 확인하지만 객체 소유권은 확인하지 않는 것, 즉 /documents/42를 유효한 세션이라면 누구에게나 제공하는 것이 객체 수준 인가 취약점이며, 가장 흔한 API 취약점 유형입니다. 검사는 객체마다, 요청마다, 예외 없이 이루어져야 합니다. 미들웨어 순서. 개념에서처럼 코드에서도 인증이 먼저, 인가가 그 다음입니다. 신원 미들웨어가 누구인지를 확립한 뒤 핸들러가 허용되는지를 묻습니다. 신원이 확립되기 전에 실행되는 인가 검사는 익명 사용자를 소리 없이 인가합니다.
일반적인 사용 사례
- 앱 로그인 + 데이터 권한 – 일상적인 조합입니다. 인증 흐름 하나, 그 뒤의 모든 것에 객체별 규칙.
- API 설계 – 계약의 오류 언어로서의 401/403 의미론, 범위가 지정된 토큰, 객체 수준 검사.
- 멀티테넌트 SaaS – 인증은 테넌트 간에 공유되고, 인가는 각 테넌트 안에서 엄격하게 범위가 지정됩니다.
- 관리자와 지원 도구 – 단계적 인증과 상승된 인가, 의도적으로 분리된 두 이벤트.
- 공개 + 비공개 콘텐츠 – 인가 규칙으로서의 익명 읽기는 두 개념이 독립적으로 동작함을 증명합니다.
어떤 검사가 빠져 있을까? 결정 매트릭스
| 증상 | 빠진 조각 |
|---|---|
| 링크만 있으면 누구나 비공개 데이터를 읽음 | 인가 – 객체별 검사 |
| 도난당한 비밀번호가 계속 통함 | 인증 – MFA 추가 |
| 로그인 오류 뒤 사용자에게 403 벽이 보임 | 잘못된 코드 – 401 흐름이어야 함 |
| 로그인한 사용자라면 누구나 관리자 API를 호출함 | 인가 – 역할, 기본 거부 |
| ”…로 로그인”을 인가로 취급함 | 개념 혼동 – OIDC는 인증, 범위가 인가 |
| 버튼은 숨겼지만 엔드포인트는 열려 있음 | 인가가 클라이언트 측에서만 강제됨 |
한계와 트레이드오프
- 분리는 개념적인 것이지 항상 아키텍처적인 것은 아닙니다. 작은 앱이 둘을 하나의 미들웨어 스택에서 실행하는 것은 합리적입니다. 규율은 검사를 구분하는 데 있지 서버를 나누는 데 있지 않습니다.
- 강한 인증이 약한 인가를 구하지 못합니다. 패스키와 MFA가 신원을 완벽하게 검증해도, 그 뒤에 누구나 무엇이든 읽게 두는 시스템이라면 소용이 없습니다. 두 실패는 독립적입니다.
- 요청마다 수행하는 인가에는 비용이 있습니다. 결정 캐싱, 규칙을 데이터 계층으로 내리기, 권한 인덱싱이 “모든 것을 검사”를 빠르게 유지하는 방법입니다.
- 단계적 인증은 시간축을 흐립니다. 민감한 동작은 세션 중간에 다시 인증합니다. 인증은 엄밀히 “한 번”이 아니라 “보증 수준당 한 번”입니다.
- 용어의 혼용은 실제 버그를 낳습니다. 두 절반을 모두 “auth”라고 부르는 팀은 둘을 뒤섞은 티켓, 테스트, 오류 코드를 작성합니다. 정확한 단어가 가장 저렴한 방어입니다.
Back4app에서의 인증과 인가
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 플랫폼의 아키텍처 자체가 이 분리입니다. User 클래스, 세션, 소셜 로그인, MFA 어댑터가 누구인지에 답하며 폐기 가능한 세션 토큰을 만들고, 그 뒤 ACL, 클래스 수준 권한, 역할이 무엇을 할 수 있는지에 답하며, 모든 REST, GraphQL, Live Query 요청마다 서버 측에서 평가됩니다. 코드 탭이 보여 주는 그대로, 로그인은 한 번, 판정은 작업마다입니다. 실수 섹션은 구조적으로 해결됩니다. Back4app이 API 뒤에서 인가를 강제하므로 인가는 클라이언트 측에 있을 수 없고, 인가되지 않은 객체는 존재 여부를 드러내는 대신 ‘찾을 수 없음’(not found)으로 응답되며, 기본 거부는 클래스의 체크박스 하나입니다. 플랫폼이 두 질문을 결코 합쳐지게 두지 않으므로 둘은 분리된 채로 남습니다.
자주 묻는 질문
인증과 인가의 차이를 쉽게 설명하면 무엇입니까?
인증은 사용자가 누구인지 검증합니다. 자격 증명, 코드, 생체 인식이 그 수단입니다. 인가(권한 부여)는 사용자가 무엇을 할 수 있는지 결정합니다. 역할, 권한, 정책이 그 수단입니다. 호텔에 비유하면, 프런트에서 신분증을 보여 주는 것이 인증이고, 자신의 객실은 열지만 펜트하우스는 열지 못하는 키카드가 인가입니다.
인증과 인가 중 무엇이 먼저입니까?
거의 항상 인증이 먼저입니다. 시스템은 누구인지 모르는 상대에게 권한을 부여할 수 없기 때문입니다. 주목할 만한 예외는 공개 리소스입니다. 이는 인증 없이 내려지는 인가 결정으로, "누구나 읽을 수 있다"는 것도 익명 사용자에게 적용되는 권한 규칙입니다.
401 오류와 403 오류의 차이는 무엇입니까?
401은 요청에 유효한 인증 자격 증명이 없다는 뜻입니다. 공식 명칭이 "Unauthorized"이지만 실제 의미는 미인증이며, 로그인하면 해결됩니다. 403은 서버가 사용자가 누구인지 알면서도 거부한다는 뜻입니다. 권한이 거부된 것이므로 다시 로그인해도 달라지는 것이 없습니다.
인증 없이 인가가 가능합니까?
가능합니다. 모든 공개 엔드포인트가 그 증거입니다. 익명 접근은 신원 없이 평가되는 인가 규칙입니다. 그 반대도 존재합니다. 인증은 되었지만 아무것도 인가받지 못한 상태로, 역할이 없는 새 계정이 정확히 그래야 하는 상태입니다.
어떤 프로토콜이 인증을 다루고 어떤 프로토콜이 인가를 다룹니까?
인증: 비밀번호, MFA, 패스키와 WebAuthn, 세션, OpenID Connect, SAML. 인가: 역할(RBAC), 속성 규칙(ABAC), ACL, OAuth 2.0 범위(scope), 정책 엔진. OAuth는 잘 알려진 대로 인가 쪽에 속하며, 로그인 수단이라는 평판은 그 위에 얹힌 OpenID Connect에서 온 것입니다.
OAuth는 인증입니까, 인가입니까?
인가입니다. 명세의 제목 자체가 "The OAuth 2.0 Authorization Framework"이며, OpenID Connect 명세는 OAuth만으로는 누가 인증했는지 증명할 수 없기 때문에 존재합니다. 실제로 쓰이는 모든 "…로 로그인" 버튼은 OAuth의 배관 위에 OIDC가 신원 계층을 더한 것입니다.
하나의 요청에서 인증과 인가는 어떻게 함께 동작합니까?
로그인 시 한 번 인증하여 세션이나 토큰을 만듭니다. 그 뒤 모든 요청마다 서버는 그 토큰에서 신원을 다시 확립하고, 특정 리소스와 동작에 대해 인가를 평가합니다. 세션당 한 번 대 요청마다 매번, 이 비대칭이 아키텍처의 전부입니다.
가장 흔한 실수는 무엇입니까?
오류 처리에서 둘을 혼동하는 것(401이 맞는 자리에 403), 클라이언트에서 인가를 강제하는 것(버튼을 숨기는 것은 UX이지 보안이 아님), 그리고 사용자가 로그인했는지는 확인하지만 특정 객체의 소유자인지는 확인하지 않는 것입니다. 마지막은 API 보안 목록 최상위에 오르는 객체 수준 인가 취약점(BOLA)입니다.