상태: 구현 완료
이 문서는 인증 기능의 고정 계약이다. 범위는 가입, 로그인, Refresh Token Rotation, 현재 사용자 조회이며 이메일 인증과 비밀번호 재설정은 포함하지 않는다.
모든 JSON 응답은 api-response.md의 공통 envelope를 사용한다. 아래 성공 예시는 전체 응답의 data에 들어가는 payload만 표시한다.
users 테이블은 다음 column만 가진다.
| Column | Type | 제약 |
|---|---|---|
id |
UUID | Primary key, 애플리케이션에서 UUID4 생성 |
name |
VARCHAR(50) | 필수, 앞뒤 공백 제거 후 1~50자 |
email |
VARCHAR(254) | 필수, 소문자 정규화, unique |
student_number |
CHAR(5) | 필수, 숫자 5자리, 중복 허용 |
password_hash |
TEXT | 필수, Argon2 hash만 저장 |
created_at |
TIMESTAMPTZ | 필수, DB의 현재 UTC 시각 |
학번은 프로필 정보일 뿐 로그인, 권한, 사용자 유일성 판단에 사용하지 않는다.
요청:
{
"name": "테스트학생",
"email": "student@example.com",
"studentNumber": "10311",
"password": "example-password"
}검증:
name: trim 후 1~50자email: 유효한 이메일, trim·소문자 변환 후 최대 254자studentNumber:^[0-9]{5}$password: 10~128자
성공: 201 Created
{
"id": "5fbbb3f2-f55d-4652-b138-3595e527ba37",
"name": "테스트학생",
"email": "student@example.com",
"studentNumber": "10311"
}오류:
409 EMAIL_ALREADY_EXISTS: 대소문자가 다른 동일 이메일 포함422 VALIDATION_ERROR: 필드 형식 또는 길이 오류
응답과 로그에 password와 password_hash를 포함하지 않는다.
요청:
{
"email": "student@example.com",
"password": "example-password"
}성공: 200 OK
{
"accessToken": "<signed-jwt>",
"refreshToken": "<signed-jwt>",
"tokenType": "bearer",
"expiresIn": 1800,
"refreshExpiresIn": 1209600
}존재하지 않는 이메일과 잘못된 비밀번호는 모두 같은 401 INVALID_CREDENTIALS를 반환한다. 계정 존재 여부와 어느 필드가 틀렸는지 노출하지 않는다.
요청:
{ "refreshToken": "<refresh-jwt>" }성공 시 새 access token과 새 refresh token을 반환한다. 사용한 refresh token은 즉시 폐기되며 같은 토큰을 다시 사용하면 401 INVALID_TOKEN을 반환한다.
요청 header:
Authorization: Bearer <access-token>
성공 응답은 가입 성공 응답과 같다.
오류:
- token 누락:
401 INVALID_TOKEN - token 위조·형식 오류·만료:
401 INVALID_TOKEN - token의 사용자가 DB에 없음:
401 INVALID_TOKEN
모든 401 응답은 WWW-Authenticate: Bearer header를 포함한다.
인증 오류는 api-response.md의 공통 오류 envelope를 사용한다. INVALID_CREDENTIALS, INVALID_TOKEN, EMAIL_ALREADY_EXISTS code와 기존 HTTP 상태는 그대로 유지하며 입력 검증 오류는 422 VALIDATION_ERROR로 변환한다.
- FastAPI 공식 권장 조합인
pwdlib[argon2]와PyJWT를 사용한다. - 비밀번호 원문은 저장하거나 로그에 남기지 않는다.
- JWT algorithm은
HS256이다. JWT_SECRET_KEY는 최소 32 random bytes이며.env와 운영 secret으로만 제공한다.- Access Token 만료는 30분이다.
- payload에는
sub,jti,iat,exp,tokenType을 넣는다. sub는 사용자 UUID 문자열이다. 이름, 이메일, 학번은 넣지 않는다.- refresh token의
jti는 DB에 저장하고 rotation 시 일회성으로 소비한다.
인증 구현은 다음 파일만 추가·수정하는 것을 기본으로 한다.
app/models.py:UserSQLAlchemy modelapp/auth.py: schema, password·JWT 함수, 인증 dependency, routerapp/config.py: JWT 설정app/main.py: 인증 router 등록alembic/versions/*_create_users.py: schema migrationtests/conftest.py: endpoint용 DB Session overridetests/test_auth.py: 인증 동작 테스트- dependency와 환경설정 파일
service·repository·JWT provider interface는 만들지 않는다.
| 시나리오 | 기대 결과 |
|---|---|
| 정상 가입 | 201, 사용자 응답, DB에는 Argon2 hash 저장 |
| 같은 이메일 재가입 | 409 EMAIL_ALREADY_EXISTS |
| 이메일 대소문자만 변경 | 동일한 중복으로 처리 |
| 학번 중복 | 가입 허용 |
| 잘못된 학번·짧은 비밀번호 | 422 |
| 정상 로그인 | 30분 Access Token 발급 |
| refresh token rotation | 새 토큰 발급, 기존 refresh token 재사용은 401 |
| 없는 이메일·틀린 비밀번호 | 동일한 401 응답 |
정상 token으로 /users/me |
자신의 정보 반환 |
| token 없음·위조·만료 | 401 INVALID_TOKEN |
| password 또는 hash 응답 노출 | 테스트 실패 |
각 단계에서 테스트를 먼저 실패시킨 뒤 최소 구현으로 통과시킨다. 커밋은 make check가 성공한 상태에서만 만든다.
feat: add user persistence- model, migration, model 검증 테스트
feat: implement user signup- schema, Argon2, 이메일 중복, 가입 endpoint
feat: implement login and current user- JWT, 로그인, 인증 dependency,
/users/me
- JWT, 로그인, 인증 dependency,
- 위 API와 오류 계약이 OpenAPI와 테스트에 반영된다.
- 사용자 비밀번호 원문이 DB·응답·로그 어디에도 없다.
- 중복 이메일이 DB unique constraint와 API 양쪽에서 차단된다.
- 위조·만료 JWT가 사용자 조회에 사용되지 않는다.
make check와git diff --check가 성공한다.
인증 완료 후 implementation-roadmap.md의 순서로 사용자별 과제 CRUD를 구현한다. 상세 계약은 assignments.md를 따르며 조회·수정·삭제는 반드시 과제 ID와 인증 사용자의 user_id를 같은 DB 조건에 사용한다.