비밀번호와 세션 수명
65분 안팎
학습 목표
비밀번호 해시와 세션 만료·폐기의 역할을 설명합니다.
개념
로그인 이후의 요청을 연결합니다
비밀번호를 매 자료 요청마다 전송하는 대신 서버는 로그인 성공 뒤 세션을 만듭니다. 브라우저는 세션 식별자를 보내고 서버는 저장소에서 그 식별자에 연결된 사용자를 찾습니다. 식별자 자체가 회원 이름을 뜻하지 않습니다. demo-alice라는 문자열을 토큰 자리에 넣어도 세션이 없으면 거절되어야 합니다. 세션은 소지자가 사용자의 권한으로 요청할 수 있는 민감한 값이므로 일반 요청 식별자처럼 로그에 남기지 않습니다.
저장 사고의 영향을 줄입니다
비밀번호 원문을 저장하면 계정 저장소가 유출될 때 그대로 사용할 수 있습니다. password 전용 키 유도 함수는 입력과 솔트를 사용해 검증 값을 만듭니다. 이번 auth.cjs는 Node의 scryptSync를 호출하고 계정마다 randomBytes로 16바이트 솔트를 생성합니다. 로그인에서는 저장된 솔트로 후보 비밀번호를 다시 계산하여 비교합니다. 암호 알고리즘을 직접 구현하거나 단순 SHA 해시를 비밀번호 저장 함수로 쓰지 않습니다.
솔트와 비용을 구별합니다
솔트는 같은 비밀번호를 가진 두 계정의 저장 값이 같아지는 것을 피하게 합니다. 솔트가 공개되어도 비밀번호 원문처럼 취급할 필요는 없으며 검증 시 같은 값을 사용해야 합니다. 비용은 추측 한 번에 필요한 자원과 관련됩니다. 이 교재의 N=16384, r=8, p=1과 32바이트 결과는 작은 오프라인 실행용 설정입니다. 운영 서비스의 권장 비용을 충족했다고 주장하지 않습니다. 운영 비용은 위협·자원·지연 예산을 별도로 검토합니다.
라이브러리의 비교 계약
verifyPassword는 후보가 문자열인지와 256바이트 이하인지 먼저 검사합니다. 그다음 같은 길이의 키를 만들어 timingSafeEqual로 비교합니다. 이 함수는 서로 다른 길이 버퍼를 받으면 오류를 낼 수 있습니다. 전체 로그인 처리의 응답 시간을 일정하게 보장하는 함수도 아닙니다. Node crypto 문서에서 scrypt와 솔트, 비교 함수 계약을 확인합니다.
잘못된 자격 증명 처리
accounts에 없는 이름과 잘못된 비밀번호는 모두 null을 반환합니다. 서버 본문도 invalid_credentials 하나로 표현해 계정 존재 여부를 메시지로 구별하지 않습니다. 없는 계정에도 더미 해시로 검증을 수행하지만 이 사실만으로 계정 열거가 전부 막혔다고 말하지 않습니다. 속도 제한·복구 절차·MFA·시간 분석은 이 실습의 구현 범위 밖입니다. 로그인 오류에 입력한 비밀번호를 되돌려주는 진단 코드를 추가하지 않습니다.
발급과 저장소 연결
로그인 성공 시 randomBytes(32)의 결과를 16진수로 만들어 sid를 발급합니다. 저장소 Map에는 sid를 키로 사용자 id·role과 expiresAt을 저장합니다. 사용자 객체는 서버 계정에서 얻으며 로그인 본문의 role을 사용하지 않습니다. resolve가 반환할 때 복사본을 만들면 호출자가 반환 객체의 role을 바꿔 저장된 세션을 수정할 수 없습니다. 토큰은 테스트에서도 실제 원문을 출력하기보다 길이와 교체 여부만 비교합니다.
절대 만료 경계
함수 실습은 기본 ttl 1000ms를 쓰고 누적 미션의 HTTP 데모는 30분을 씁니다. 로그인 시각에 ttl을 더해 절대 만료 시각을 정합니다. now가 expiresAt 이상이면 삭제하고 null을 반환합니다. 만료 시각과 정확히 같은 요청을 허용하지 않는 계약입니다. 테스트는 now 함수를 주입해 999ms와 1000ms를 비교합니다. 실제로 1초를 기다리는 테스트는 느리고 경계가 흔들릴 수 있습니다. 시계를 되돌려도 이미 삭제한 세션은 되살아나지 않는지 추가 확인합니다.
로그아웃은 서버 상태를 지웁니다
화면에서 로그인 버튼을 다시 보이게 하는 것만으로 로그아웃이 완료되지 않습니다. 쿠키를 지운 뒤에도 이전 토큰을 복사한 요청이 성공한다면 서버 세션이 남아 있습니다. logout은 sessions에서 해당 키를 삭제하고 같은 토큰의 resolve는 null을 반환해야 합니다. 이미 지운 토큰을 다시 로그아웃해도 오류 없이 처리합니다. 로그아웃 성공 상태와 다음 자료 요청의 거절 상태를 함께 확인해야 폐기 동작을 검증할 수 있습니다.
로그인 성공 뒤 토큰 교체
이전에 받은 세션으로 다시 로그인하면 새 토큰을 만들고 이전 키를 삭제합니다. 클라이언트가 정한 토큰을 로그인 세션으로 승격시키지 않습니다. 잘못된 비밀번호의 실패 요청은 새 세션을 발급하지 않습니다. OWASP 세션 지침은 권한 변화 시 식별자 갱신과 이전 세션 폐기의 필요성을 설명합니다. 교체 후 이전 토큰과 새 토큰을 각각 조회하여 차이를 확인합니다.
요청 전달과 응답 노출
누적 미션의 server.cjs는 sid 쿠키만 읽어 앱에 전달합니다. 로그인 토큰은 Set-Cookie 헤더로 발급하며 JSON 본문에는 ok만 넣습니다. HttpOnly와 SameSite=Strict를 사용하되 평문 루프백 실습이므로 Secure를 운영 설정처럼 검증하지 않습니다. HTTPS 운영에서는 Secure와 전송 보호를 별도로 확인해야 합니다. 쿠키는 발급·전달에 쓰이지만 본문·로그·증거 파일에는 토큰을 넣지 않는다는 경계를 구분합니다.
starter 실패를 진단합니다
starter에는 만료 비교가 false로 막혀 있고 logout이 빈 함수입니다. salt와 비밀번호 확인 시험은 통과하지만 expiry와 logout 시험은 실패합니다. expected null, actual 사용자 객체라면 삭제 또는 만료 판단이 실행되지 않은 것입니다. expiresAt을 갱신해서 테스트를 통과시키려 하지 말고 now와 기존 expiresAt을 비교합니다. 테스트 파일을 수정해 사용자 객체를 기대값으로 바꾸면 폐기해야 한다는 요구가 사라집니다.
운영 모델과 남은 차이
Map 저장은 프로세스가 끝나면 사라지고 여러 서버 사이에 공유되지 않습니다. scryptSync는 이벤트 루프를 막으므로 공개 서비스에서 그대로 확장할 구현이 아닙니다. 동시 로그인, 세션 수 제한, 활동 기반 만료와 계정 권한 변경 반영은 다음 설계 항목입니다. 이번 합격 기준은 네 테스트 묶음에서 비밀번호 검증·틀린 자격 증명·경계 만료·폐기가 통과하는 것입니다. 작은 모델의 검증 결과를 서비스 전체의 인증 보증으로 확대하지 않습니다.
따라하기
솔트별 키와 후보 검증
아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.
const {randomBytes,scryptSync,timingSafeEqual}=require('node:crypto');
const salt=randomBytes(16);
const key=scryptSync('synthetic',salt,32);
console.log('salt bytes:',salt.length);
console.log('correct:',timingSafeEqual(key,scryptSync('synthetic',salt,32)));
console.log('wrong:',timingSafeEqual(key,scryptSync('wrong',salt,32)));실행 결과
salt bytes: 16 correct: true wrong: false
만료 시각의 두 경계
아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.
for(const now of [999,1000])console.log(now,now>=1000?'expired':'active');실행 결과
999 active 1000 expired
로그아웃의 저장소 효과
아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.
const sessions=new Map([['copied-demo',{id:'demo-alice'}]]);
console.log('before:',sessions.has('copied-demo'));
sessions.delete('copied-demo');
console.log('after:',sessions.has('copied-demo'));실행 결과
before: true after: false
실습의 실패와 수정 대조
ZIP 루트에서 bash check.sh를 실행합니다. starter의 실패 시험 이름과 expected/actual을 읽고 구현을 수정합니다. solution에서도 같은 명령으로 전체 통과와 종료 코드 0을 확인합니다.
확인 문제
실습
auth.cjs의 만료 검사와 logout을 완성합니다. 정확한 만료 시각 거절, 토큰 교체와 옛 토큰 폐기, 잘못된 비밀번호를 검증합니다. 테스트 파일을 수정하지 않습니다.
실행 명령
bash check.sh
기대 결과
4개 테스트 묶음 통과, 종료 코드 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 로그아웃과 세션 만료를 서버에서 어떻게 검증합니까?