인증과 접근 이벤트 모델
85분 안팎
학습 목표
실패 이유와 권한 거절을 구분해 기록합니다.
개념
기록이 없으면 판단이 흔들립니다
인증 실패가 늘었다는 제보를 받으면 먼저 어떤 동작을 실패라고 기록했는지 확인합니다. 개인 자료 보관 앱의 로그인 실패는 자격 증명이 맞지 않은 결과이고, 자료 조회 거절은 로그인 이후 소유자 정책을 통과하지 못한 결과입니다. 둘을 error 하나로 합치면 비밀번호 오타와 타인 자료 접근을 구분할 수 없습니다. 이번 레슨은 보안 이벤트를 작고 일정한 객체로 만들고 비밀 필드가 들어가지 않는지 검사합니다.
이번 모듈의 실습 경계
모든 입력은 본인 로컬 앱의 합성 계정과 고정 fixture입니다. 외부 서비스에 요청하거나 실제 사용자 기록을 가져오지 않습니다. Node.js와 Python 3, SQL 브라우저 채점기를 사용합니다. 로컬 ZIP을 풀고 그 루트에서 bash check.sh를 실행합니다. starter는 의도한 검사가 실패하며 solution은 통과합니다. 테스트를 지우거나 기대값을 바꾸는 대신 지정 TODO를 구현합니다. 기존 Docker 검사 대기는 이번 실습 성공과 별도로 유지합니다.
이벤트의 단위를 정합니다
id는 개별 사건의 식별자이고 requestId는 서버가 부여한 처리 흐름을 찾는 키입니다. account는 합성 계정의 정규화된 키입니다. 로그인 실패에서 account는 입력한 계정을 나타내므로 실제 소유자가 행동했다는 뜻이 아닙니다. 성공은 서버가 확인한 계정으로 기록합니다. type은 auth_failure·auth_success·access_denied 중 하나이고 reason은 내부의 제한된 사유 코드입니다. 자유로운 오류 문장을 그대로 reason에 넣지 않습니다.
실패 이유와 외부 응답
비밀번호 오류와 존재하지 않는 계정은 외부에 같은 invalid_credentials 응답을 줄 수 있습니다. 내부 보안 분석에서도 불필요하게 계정 존재 여부를 넓게 노출하지 않습니다. 로그인 Origin 검사가 거절된 403은 자격 증명 실패와 다른 사건입니다. observedLogin은 200과 401만 인증 이벤트로 변환합니다. 그 밖의 결과를 임의로 auth_failure로 분류하면 서버 장애나 요청 형식 오류가 인증 공격 횟수를 부풀립니다.
자료 거절은 판단 지점에서 기록합니다
secure-app.cjs는 없는 자료와 타인의 자료에 같은 404를 반환합니다. 따라서 공개 상태 코드만 보고 access_denied라고 단정할 수 없습니다. 신뢰할 수 있는 서버 소유자 비교 분기에서 recordDenial을 호출해야 합니다. 이 레슨의 어댑터는 호출 계약을 보여 주며 기존 404를 모두 바꾸지 않습니다. 존재하지 않는 자료와 권한 없는 자료의 외부 응답 일관성은 그대로 지키면서 내부 판단 정보를 제한된 로그로 남깁니다.
허용 목록을 코드로 적용합니다
events.cjs는 여섯 키만 골라 새 객체를 만듭니다. 요청 body를 펼친 뒤 password만 지우는 방식은 새 비밀 필드가 추가될 때 놓치기 쉽습니다. 허용 목록은 새로운 필드를 기본적으로 배제하므로 검토 범위가 작습니다. session·cookie·본문은 원문도 일부 조각도 넣지 않습니다. 합성 계정 키도 실제 환경에서는 개인정보가 될 수 있으므로 접근 권한과 보관 기간을 별도로 정해야 합니다.
시간을 비교 가능한 형태로 만듭니다
로그 시각은 오프셋이 있는 문자열을 받아 UTC ISO 문자열로 변환합니다. 09:00+09:00과 00:00Z는 같은 순간입니다. 오프셋 없는 00:00은 어느 시간대인지 알 수 없어 event: timestamp offset으로 거절합니다. Date.parse는 일부 문자열을 관대하게 해석하므로 이 교재 검사가 모든 달력 문법을 엄격하게 검증하는 것은 아닙니다. 실제 수집 계약은 허용 형식과 날짜 유효성 검사를 더 명확하게 정합니다.
서버 메타데이터와 입력을 구별합니다
meta의 id·requestId·time·account는 어댑터 호출자가 서버에서 만든 값이어야 합니다. 브라우저가 보낸 requestId를 그대로 신뢰하면 다른 요청과 연결되거나 로그를 혼동시킬 수 있습니다. 이 함수 실습은 문자열 필드와 이벤트 종류를 검사하지만 요청 헤더의 신뢰 경계를 구현하지 않습니다. 실제 전송 어댑터에서는 외부 상관 키와 내부 발급 키를 구분하고 길이와 허용 문자를 제한합니다.
오류를 읽는 순서
starter는 Object.assign으로 입력 전체를 복사합니다. 첫 실패는 비밀 키가 결과에 존재한다는 assertion입니다. actual과 expected를 보고 잘못된 필드가 무엇인지 확인합니다. required field 오류는 필수 메타데이터 누락이고 timestamp offset 오류는 시간대 계약 위반입니다. SyntaxError나 MODULE_NOT_FOUND라면 이벤트 정책 실패가 아니라 코드 문법 또는 실행 위치 문제를 먼저 해결합니다.
실제 로그인 결과와 연결합니다
observedLogin은 기존 handle을 호출하고 그 응답 상태에서 인증 결과를 선택합니다. 로그 객체를 만들기 위해 비밀번호를 다시 읽거나 세션을 저장할 필요가 없습니다. 호출자는 발급된 세션을 정상 응답 경로에만 전달하고 emit에는 허용 목록 이벤트를 넘깁니다. 인증 통제를 로깅 코드에서 재구현하면 원래 인증 결과와 로그가 달라질 수 있으므로 신뢰할 수 있는 실제 처리 결과를 사용합니다.
검사와 실제 수집의 차이
events-test.cjs는 세 종류, UTC 변환, 비밀 필드 제외, 필수값 누락과 오프셋 누락을 확인합니다. 이 성공은 파일 회전·권한·수집기 전송을 검사한 결과가 아닙니다. emit 실패가 업무 응답과 관찰 가능성에 어떤 영향을 주는지 또한 별도 설계가 필요합니다. 로그가 기록되지 않으면 정상으로 간주하지 않도록 수집 실패 지표를 함께 남기는 방식을 다음 실습의 결측 처리와 연결합니다.
제출 전 객체를 읽습니다
테스트를 통과한 뒤 허용 키를 눈으로 확인합니다. type과 reason이 서로 모순되지 않는지, time이 UTC인지, 입력 계정과 확인 계정의 의미를 혼동하지 않았는지 읽습니다. 로그 한 줄은 관찰이고 침해 확정 문장이 아닙니다. 더 읽기의 Node 로깅 장에서 일반 요청 로깅 구조를 확장하되 이 실습에서는 탐지에 필요한 최소 이벤트 계약에 집중합니다.
실습 완료 후 이벤트가 호출자 객체와 다른 새 객체인지도 살펴봅니다. 허용 목록을 만든 뒤 원본을 변경해 로그를 오염시키는 코드를 넣지 않습니다. 테스트는 값이 고정된 합성 사건으로 실행하므로 현재 시각이나 무작위 요청 키를 출력에 섞지 않습니다. 실제 서비스에서는 사건 키 충돌과 로그 저장 실패를 따로 감시하고 로그 읽기 권한을 최소화해야 합니다. 이번 검사 성공은 탐지용 객체의 계약에 한정해 보고합니다.
따라하기
작은 조건을 직접 실행합니다
const allowed=['id','type'];const input={id:'e1',type:'auth_failure',password:'dummy'};const out=Object.fromEntries(allowed.map(k=>[k,input[k]]));console.log(JSON.stringify(out));실행 결과
{"id":"e1","type":"auth_failure"}
허용 목록을 구현합니다
security-event-model ZIP에서 events.cjs의 Object.assign을 여섯 키 복사로 바꿉니다. 테스트는 수정하지 않습니다.
검사 오류를 대조합니다
bash check.sh를 실행하고 비밀 키 제외 assertion과 timestamp offset 메시지를 구분합니다.
기록 위치를 설명합니다
observed-auth.cjs를 읽고 Origin 403을 인증 실패로 만들지 않는 이유와 recordDenial 호출 지점을 적습니다.
확인 문제
실습
events.cjs의 TODO를 허용 목록 복사로 고칩니다. 세 유형·시간대·누락·민감 필드 테스트를 통과하고 observed-auth.cjs의 기록 위치 계약을 설명합니다.
실행 명령
bash check.sh
기대 결과
PASS: event schema and sensitive fields
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 인증 실패 경보를 분석하는 순서를 설명합니다.