이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Identity, identifier, credential, principal, authority를 어떻게 구분하는가?
- 클라이언트가 보낸
userId,role,emailVerified를 principal처럼 믿으면 어떤 공격이 생기는가?- Spring 백엔드에서 principal은 어떤 정보만 담아야 하며, 어떤 정보는 절대 담지 않아야 하는가?
개요
Authentication은 요청자가 주장한 identity가 맞는지 확인하는 절차다.
Identity는 사용자를 구분하는 속성이다. 내부 subject id, 이메일, 휴대폰 번호, 소셜 provider subject, 사번이 모두 identity와 관련될 수 있다.
Identifier는 로그인이나 조회에 쓰는 식별자다. 이메일은 흔한 identifier지만 변경될 수 있고, 재사용될 수 있으며, 소셜 계정 연결에서 충돌할 수 있다.
Credential은 identity 주장을 증명하는 값이다. 비밀번호, OTP, recovery code, passkey, API key, client secret이 여기에 해당한다.
Principal은 인증이 끝난 뒤 서버가 요청 처리에 사용하는 실행 주체 표현이다. principal은 credential 원문이 아니라 서버가 검증한 subject id와 필요한 최소 속성만 담아야 한다.
공격 시나리오
공격자는 일반 사용자로 로그인한 뒤 프로필 수정 API 요청을 관찰한다.
프론트엔드가 편의를 위해 다음 값을 body에 넣고 있다.
userIdemailemailVerifiedroledisplayName
서버가 이 값으로 principal이나 계정 상태를 갱신하면 공격자는 자기 요청에 다른 사용자의 ID를 넣거나 emailVerified=true, role=ADMIN을 추가할 수 있다.
이 공격은 인증 우회가 아니다. 인증은 성공했지만 인증 결과와 클라이언트 입력의 경계를 섞어서 발생한 identity confusion이다.
취약한 요청/응답 예시
PATCH /api/me/profile HTTP/1.1
Host: api.example.com
Cookie: SESSION=valid-user-session
Content-Type: application/json
{
"userId": 2005,
"email": "attacker@example.net",
"emailVerified": true,
"role": "ADMIN",
"displayName": "new-name"
}HTTP/1.1 200 OK
Content-Type: application/json
{
"userId": 2005,
"email": "attacker@example.net",
"emailVerified": true,
"role": "ADMIN"
}서버가 “요청을 보낸 사용자가 누구인가”를 session principal에서 읽지 않고 body에서 읽으면 principal 위조가 된다.
주제별 핵심 판단
이 구분이 흐려지면 다음 실수가 생긴다.
- body의
userId를 principal처럼 사용한다. - 이메일을 내부 불변 ID로 보고 계정 병합과 이메일 변경을 잘못 처리한다.
- 인증된 principal에 role을 넣어 두고 권한 변경 후에도 오래된 권한을 계속 사용한다.
- principal 객체에 password hash, raw token, MFA secret을 넣어 로그와 trace로 흘린다.
- “로그인 성공”을 “모든 자원 접근 가능”으로 확대 해석한다.
보안 설계에서는 principal을 신뢰할 수 있는 입력으로 만들기까지의 경계를 분명히 해야 한다.
개선된 요청/응답 예시
내부 subject id를 기준으로 삼는다.
- 개선된 요청은 사용자가 보낸 식별자를 principal로 승격하지 않고 인증 성공 후 서버가 principal을 만든다.
- 응답과 로그에는 principal id, 인증 방식, 인증 강도만 남기고 credential 원문은 남기지 않는다.
방어 설계
subject_id: 1004
login_identifier: user@example.com
provider_subject: google-oauth2|99192
principal_name: user@example.com
authorities: ROLE_USER, ORDER_READ이 값들은 같은 것이 아니다.
- subject id는 내부 DB에서 불변에 가깝게 관리한다.
- 이메일은 로그인 identifier이자 연락처일 수 있지만 변경 절차를 거친다.
- provider subject는 소셜 provider 안에서의 식별자다.
- principal name은 화면 표시나 로그용 이름일 수 있다.
- authority는 현재 요청에서 참고하는 권한이며 변경 가능성을 고려해야 한다.
principal에는 최소 정보만 담는다.
public record AccountPrincipal(
long accountId,
String principalName,
Set<String> authorities,
Instant authenticatedAt,
boolean mfaVerified
) {
}비밀번호, password hash, raw session id, refresh token, TOTP secret, recovery code는 principal에 넣지 않는다.
Spring/HTTP 예시
Controller는 body의 사용자 식별자를 받지 않는다.
public record UpdateProfileRequest(
@Size(max = 30) String displayName
) {
}@PatchMapping("/api/me/profile")
public ProfileResponse updateProfile(
@AuthenticationPrincipal AccountPrincipal principal,
@Valid @RequestBody UpdateProfileRequest request) {
Profile profile = profileService.updateDisplayName(
principal.accountId(),
request.displayName());
return ProfileResponse.from(profile);
}권한은 principal에 들어 있는 문자열만 믿고 끝내지 않는다. 고위험 action은 DB의 최신 상태와 정책으로 다시 확인한다.
@Transactional
public void changeEmail(long actorAccountId, ChangeEmailCommand command) {
Account account = accountRepository.getById(actorAccountId);
if (!account.canStartEmailChange()) {
audit.identityChangeDenied(actorAccountId, "email_change_not_allowed");
throw new AccessDeniedException("email change denied");
}
emailChangeTokenService.issue(actorAccountId, command.newEmail());
audit.identityChangeRequested(actorAccountId, "email_change");
}운영 로그와 감사 지점
level=WARN event=identity_input_rejected result=blocked actor_id=1004 input_field=userId path=/api/me/profile reason=client_supplied_identity trace_id=bf438a11level=INFO event=principal_created result=success actor_id=1004 principal_name_hash=58aa authorities_count=2 mfa_verified=false auth_method=password trace_id=bf438a12로그에는 credential 원문을 남기지 않는다. principal name도 이메일 원문 대신 hash나 마스킹 값을 사용하는 편이 안전하다.
실전 판단 기준
/api/me계열 요청 body에userId,role,emailVerified를 추가해도 무시되거나 거부되는가?- principal에 들어간 값이 검증된 신원인지, 단순 요청 파라미터인지 먼저 확인한다.
- 계정 상태 변경, 탈퇴, 잠금, 권한 강등 후 principal cache가 갱신되는지 확인한다.
테스트 포인트
- 이메일 변경 후에도 내부 subject id가 유지되는가?
- role 변경 직후 오래된 principal이나 session이 과도한 권한을 유지하지 않는가?
- principal 직렬화 로그에 password hash, raw token, MFA secret이 없는가?
- 소셜 로그인 연결 시 provider subject와 내부 subject id 충돌을 검사하는가?
위험 신호!
- request DTO에
userId,role,authorities,emailVerified가 있고 서버가 이를 그대로 사용한다. - 이메일을 DB primary key처럼 사용한다.
- principal 객체가 너무 커서 계정의 민감 속성과 credential 정보를 함께 담는다.
- 인증 성공 후 resource ownership 검사가 없다.
- 권한 변경, MFA 변경, 이메일 변경이 일반 프로필 수정 로그와 구분되지 않는다.
확인 질문
확인 질문
- Authentication과 Principal의 차이는 무엇인가?
- Authentication은 신원 확인 절차이고, Principal은 그 결과로 서버가 요청 처리에 사용하는 실행 주체 표현이다.
- 이메일을 내부 subject id처럼 쓰면 어떤 문제가 생기는가?
- 이메일 변경, 재사용, 소셜 계정 연결, 계정 병합에서 서로 다른 identity가 섞일 수 있다.
- principal에 credential 원문을 넣으면 안 되는 이유는 무엇인가?
- principal은 로그, trace, session 저장소로 흘러갈 수 있어 credential 유출 지점이 되기 때문이다.