Skip to content

[Feat] JWT 기반 회원가입/로그인 인증 구현#79

Merged
zweadfx merged 8 commits intomainfrom
feat/jwt-auth
Apr 6, 2026
Merged

[Feat] JWT 기반 회원가입/로그인 인증 구현#79
zweadfx merged 8 commits intomainfrom
feat/jwt-auth

Conversation

@zweadfx
Copy link
Copy Markdown
Owner

@zweadfx zweadfx commented Apr 6, 2026

어떤 변경사항인가요?

JWT 기반 회원가입/로그인/토큰 갱신 인증 시스템을 구현합니다.
달력 저장 등 유저별 데이터 관리 기능의 선행 작업입니다.

작업 상세 내용

  • 유저 모델 및 SQLite RDB 설정 (src/db/database.py, src/db/models.py)
  • 비밀번호 bcrypt 해싱 및 JWT 토큰 유틸리티 (src/core/security.py)
  • 회원가입 엔드포인트 (POST /api/v1/auth/signup)
  • 로그인 엔드포인트 (POST /api/v1/auth/login) — access + refresh 토큰 발급
  • 토큰 갱신 엔드포인트 (POST /api/v1/auth/refresh)
  • get_current_user 의존성 주입 방식 인증 미들웨어

체크리스트

  • self-test를 수행하였는가?
  • 관련 문서나 주석을 업데이트하였는가?
  • 설정한 코딩 컨벤션을 준수하였는가?

관련 이슈

리뷰 포인트

  • access/refresh 토큰 분리 방식: type 클레임으로 구분하여 토큰 혼용 방지
  • SECRET_KEY 기본값이 하드코딩되어 있으므로 배포 시 반드시 환경변수 설정 필요
  • 현재 refresh token은 stateless (DB 저장 없음) — 토큰 무효화가 필요하면 블랙리스트 추가 검토

참고사항 및 스크린샷(선택)

없음

Summary by CodeRabbit

  • New Features

    • Added user signup, login, and token refresh endpoints with JWT-based access and refresh tokens.
    • Password hashing and token issuance/validation for secure authentication.
  • Chores

    • Database initialization and user model added for credential storage.
    • Configuration and example environment template extended with authentication variables.
    • Updated project ignore rules to exclude IDE metadata.

@zweadfx zweadfx self-assigned this Apr 6, 2026
@zweadfx zweadfx added the feature 새로운 기능 구현 시 사용합니다. label Apr 6, 2026
@zweadfx zweadfx linked an issue Apr 6, 2026 that may be closed by this pull request
6 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds JWT-based authentication: configuration, DB models and session setup, password hashing and JWT utilities, Pydantic auth schemas, /auth endpoints (signup, login, refresh), router mounting, and startup DB table creation; plus dependency and packaging updates and .env/template edits.

Changes

Cohort / File(s) Summary
Env & Packaging
\.env.example, pyproject.toml
Added auth-related env vars (DATABASE_URL, SECRET_KEY, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS) and new runtime deps (sqlalchemy, python-jose[cryptography], passlib[bcrypt], email-validator).
Config
src/core/config.py
Extended Settings with DATABASE_URL, SECRET_KEY, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS.
Database layer
src/db/database.py, src/db/models.py
New SQLAlchemy engine/session factory, Declarative Base, get_db dependency; added User ORM model with id, email, hashed_password, nickname, created_at.
Security utilities
src/core/security.py
New bcrypt CryptContext, password hash/verify functions, create/decode access and refresh JWTs with type discriminator and expirations.
API schemas
src/models/auth_schema.py
Pydantic models for SignupRequest, LoginRequest, RefreshRequest, TokenResponse, UserResponse (with EmailStr and field constraints).
Endpoints & Router
src/api/v1/endpoints/auth.py, src/api/v1/router.py
New auth router with POST /signup, /login, /refresh; get_current_user dependency; router mounted at /auth.
App startup
src/main.py
Runs Base.metadata.create_all(bind=engine) on startup before vector DB initialization.
Misc
.gitignore
Enabled ignoring .idea/ directory.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant FastAPI as FastAPI
    participant Security as Security Module
    participant DB as Relational DB

    Client->>FastAPI: POST /auth/login {email,password}
    FastAPI->>DB: SELECT user WHERE email=...
    DB-->>FastAPI: user (or none)
    FastAPI->>Security: verify_password(plain, hashed)
    Security-->>FastAPI: bool
    alt valid
        FastAPI->>Security: create_access_token(sub=email)
        Security-->>FastAPI: access_token
        FastAPI->>Security: create_refresh_token(sub=email)
        Security-->>FastAPI: refresh_token
        FastAPI-->>Client: 200 TokenResponse
    else invalid
        FastAPI-->>Client: 401 Unauthorized
    end
Loading
sequenceDiagram
    actor Client
    participant FastAPI as FastAPI
    participant Security as Security Module
    participant DB as Relational DB

    Client->>FastAPI: POST /auth/refresh {refresh_token}
    FastAPI->>Security: decode_refresh_token(token)
    alt token valid & type=refresh
        Security-->>FastAPI: subject (email)
        FastAPI->>DB: SELECT user WHERE email=subject
        DB-->>FastAPI: user (or none)
        alt user exists
            FastAPI->>Security: create_access_token(...)
            Security-->>FastAPI: new_access_token
            FastAPI->>Security: create_refresh_token(...)
            Security-->>FastAPI: new_refresh_token
            FastAPI-->>Client: 200 TokenResponse
        else
            FastAPI-->>Client: 401 Unauthorized
        end
    else invalid
        FastAPI-->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 In burrows of code I quietly hop,
I salt the keys and hash each crop,
Tokens I craft—access and refresh,
Users sign up in cozy mesh,
Hop, login, leap—secure every stop! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: JWT-based signup/login authentication implementation.
Linked Issues check ✅ Passed All objectives from issue #78 are addressed: user model and RDB setup [#78], signup/login endpoints [#78], password hashing and JWT auth [#78], token refresh [#78], and dependency-injection middleware [#78].
Out of Scope Changes check ✅ Passed All changes are directly related to the JWT authentication implementation; no unrelated modifications found (including .gitignore refinement for IDE artifacts).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/jwt-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/api/v1/endpoints/auth.py (2)

55-79: Consider anonymizing PII in logs.

Line 78 logs the user's email address, which is personally identifiable information. Depending on your compliance requirements (GDPR/CCPA), consider logging a user ID or anonymized identifier instead.

♻️ Suggested change
-    logger.info("New user registered: %s", user.email)
+    logger.info("New user registered: user_id=%s", user.id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/auth.py` around lines 55 - 79, The logger in signup
currently emits PII (user.email); update the log to avoid storing email by
logging a non-PII identifier instead — e.g., use the persisted User.id
(available after db.commit()/db.refresh(user)) or an anonymized token
(hash/UUID) and replace the logger.info("New user registered: %s", user.email)
call with a message that includes only that ID/anonymized identifier (reference:
signup function, logger.info and user.email/user.id).

98-116: Avoid shadowing the function name with a local variable.

Line 114 uses refresh as a variable name, which shadows the enclosing refresh function. While Python allows this, it reduces readability and can cause confusion.

♻️ Suggested change
     access = create_access_token(subject=user.email)
-    refresh = create_refresh_token(subject=user.email)
+    refresh_token = create_refresh_token(subject=user.email)
     logger.info("Token refreshed for: %s", user.email)
-    return TokenResponse(access_token=access, refresh_token=refresh)
+    return TokenResponse(access_token=access, refresh_token=refresh_token)

Additionally, the same PII logging consideration applies here (line 115).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/auth.py` around lines 98 - 116, The refresh endpoint
function `refresh` currently shadows its own name by assigning the local
variable `refresh = create_refresh_token(...)`; rename that variable to
something like `refresh_token` (or `new_refresh_token`) to avoid shadowing the
`refresh` function, update the return to use the new variable
(`TokenResponse(access_token=access, refresh_token=refresh_token)`), and adjust
the PII logger call (`logger.info`) to avoid logging full email (e.g., log user
id or masked email) instead of `user.email`; keep references to
`create_refresh_token`, `create_access_token`, and the `refresh` function name
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pyproject.toml`:
- Around line 22-24: The project uses EmailStr in src/models/auth_schema.py but
pyproject.toml's runtime dependencies list (the array containing
"sqlalchemy>=2.0", "python-jose[cryptography]>=3.3", "passlib[bcrypt]>=1.7")
does not include email-validator; add "email-validator" to the runtime
dependencies in pyproject.toml so Pydantic's EmailStr validation works at
runtime (ensure it's added alongside the existing dependency entries).

In `@src/api/v1/endpoints/auth.py`:
- Around line 82-95: The login endpoint's short-circuit "if not user or not
verify_password(...)" leaks timing for email enumeration; update the login
function to always perform a password hash comparison (use a configured
constant-time dummy verify when User is None) by calling verify_password with
either user.hashed_password or a dummy hash so timing is constant, then branch
on the result to raise HTTPException; also remove or redact direct PII from
logger.info (replace logging of user.email with a non-PII message or user.id
when available) and keep the rest of the flow (create_access_token,
create_refresh_token, TokenResponse) unchanged.

In `@src/core/config.py`:
- Line 12: The hardcoded fallback SECRET_KEY must be removed and made required
so the app fails fast if not provided: in src/core/config.py remove the default
value for SECRET_KEY and instead load it from environment/config where missing
will raise an explicit error (e.g., raise RuntimeError or let your settings
validation fail) so that SECRET_KEY cannot silently fall back to
"change-me-in-production"; update any settings class or loader that references
SECRET_KEY to validate presence at import/initialization time.

In `@src/db/database.py`:
- Around line 8-15: The module-level engine initialization using create_engine
with settings.DATABASE_URL can fail for SQLite if the parent directory doesn't
exist; before calling create_engine (i.e., before the engine variable is
created), detect when settings.DATABASE_URL startswith "sqlite" and ensure the
file's parent directory exists by resolving the DB file path and calling
os.makedirs(parent, exist_ok=True) (use pathlib or os.path to get the parent);
then proceed to call create_engine as currently written so the SQLite file can
be created without raising OperationalError.

In `@src/main.py`:
- Around line 42-45: Move the DB initialization call
Base.metadata.create_all(bind=engine) into the existing startup try block so
that any exceptions during table creation are caught by the same error handler
and logged via the structured critical path; locate the startup try/except
around application startup (where logger.info("Database tables initialized.") is
used), remove the standalone call before the try, and invoke
Base.metadata.create_all(bind=engine) inside the try just before the "Database
tables initialized." log so failures flow into the try's exception handling.

---

Nitpick comments:
In `@src/api/v1/endpoints/auth.py`:
- Around line 55-79: The logger in signup currently emits PII (user.email);
update the log to avoid storing email by logging a non-PII identifier instead —
e.g., use the persisted User.id (available after db.commit()/db.refresh(user))
or an anonymized token (hash/UUID) and replace the logger.info("New user
registered: %s", user.email) call with a message that includes only that
ID/anonymized identifier (reference: signup function, logger.info and
user.email/user.id).
- Around line 98-116: The refresh endpoint function `refresh` currently shadows
its own name by assigning the local variable `refresh =
create_refresh_token(...)`; rename that variable to something like
`refresh_token` (or `new_refresh_token`) to avoid shadowing the `refresh`
function, update the return to use the new variable
(`TokenResponse(access_token=access, refresh_token=refresh_token)`), and adjust
the PII logger call (`logger.info`) to avoid logging full email (e.g., log user
id or masked email) instead of `user.email`; keep references to
`create_refresh_token`, `create_access_token`, and the `refresh` function name
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 202e1d94-a5ac-4ced-a042-7809025332ac

📥 Commits

Reviewing files that changed from the base of the PR and between 0d87456 and c6eb841.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .env.example
  • pyproject.toml
  • src/api/v1/endpoints/auth.py
  • src/api/v1/router.py
  • src/core/config.py
  • src/core/security.py
  • src/db/__init__.py
  • src/db/database.py
  • src/db/models.py
  • src/main.py
  • src/models/auth_schema.py

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/api/v1/endpoints/auth.py (2)

101-119: Variable refresh shadows the enclosing function name.

On line 117, refresh = create_refresh_token(...) shadows the function async def refresh(...). While this works, it's confusing and violates naming clarity. Consider renaming the local variable.

♻️ Proposed fix
 `@router.post`("/refresh", response_model=TokenResponse)
 async def refresh(request: RefreshRequest, db: Session = Depends(get_db)):
     """Issue a new access token using a valid refresh token."""
     email = decode_refresh_token(request.refresh_token)
     if email is None:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail="Invalid or expired refresh token.",
         )
     user = db.query(User).filter(User.email == email).first()
     if user is None:
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail="User not found.",
         )
     access = create_access_token(subject=user.email)
-    refresh = create_refresh_token(subject=user.email)
+    refresh_token = create_refresh_token(subject=user.email)
     logger.info("Token refreshed: id=%s", user.id)
-    return TokenResponse(access_token=access, refresh_token=refresh)
+    return TokenResponse(access_token=access, refresh_token=refresh_token)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/auth.py` around lines 101 - 119, The local variable
refresh in the async def refresh(...) function shadows the function name; rename
the local variable (e.g., new_refresh_token or refresh_token) where
create_refresh_token(...) is called and update its use in the return statement
(TokenResponse) to avoid name collision while keeping create_access_token(...),
decode_refresh_token(...), and the logger.info("Token refreshed: id=%s",
user.id) references intact.

62-76: Consider handling IntegrityError for race-condition robustness.

The email uniqueness check on line 62 has a TOCTOU gap before the commit on line 75. While the database's unique=True constraint on User.email (in src/db/models.py:15) prevents duplicate entries, the current code would surface a 500 error if two concurrent signups with the same email hit the race window.

Wrapping the commit in a try/except for IntegrityError would provide a cleaner user experience:

🛡️ Proposed improvement
+from sqlalchemy.exc import IntegrityError
+
 `@router.post`(
     "/signup",
     response_model=UserResponse,
     status_code=status.HTTP_201_CREATED,
 )
 async def signup(request: SignupRequest, db: Session = Depends(get_db)):
     """Register a new user."""
     existing = db.query(User).filter(User.email == request.email).first()
     if existing:
         raise HTTPException(
             status_code=status.HTTP_409_CONFLICT,
             detail="Email already registered.",
         )

     user = User(
         email=request.email,
         hashed_password=hash_password(request.password),
         nickname=request.nickname,
     )
     db.add(user)
-    db.commit()
-    db.refresh(user)
+    try:
+        db.commit()
+        db.refresh(user)
+    except IntegrityError:
+        db.rollback()
+        raise HTTPException(
+            status_code=status.HTTP_409_CONFLICT,
+            detail="Email already registered.",
+        )

     logger.info("New user registered: id=%s", user.id)
     return user
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/v1/endpoints/auth.py` around lines 62 - 76, There is a TOCTOU race
between checking existing = db.query(User)... and db.commit() causing a 500 on
unique constraint violation; wrap the transaction around db.add(user);
db.commit() in a try/except that catches sqlalchemy.exc.IntegrityError, calls
db.rollback(), and raises HTTPException(status_code=409, detail="Email already
registered.") to surface a proper client error; ensure IntegrityError is
imported and keep db.refresh(user) only on success (after the commit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/api/v1/endpoints/auth.py`:
- Around line 101-119: The local variable refresh in the async def refresh(...)
function shadows the function name; rename the local variable (e.g.,
new_refresh_token or refresh_token) where create_refresh_token(...) is called
and update its use in the return statement (TokenResponse) to avoid name
collision while keeping create_access_token(...), decode_refresh_token(...), and
the logger.info("Token refreshed: id=%s", user.id) references intact.
- Around line 62-76: There is a TOCTOU race between checking existing =
db.query(User)... and db.commit() causing a 500 on unique constraint violation;
wrap the transaction around db.add(user); db.commit() in a try/except that
catches sqlalchemy.exc.IntegrityError, calls db.rollback(), and raises
HTTPException(status_code=409, detail="Email already registered.") to surface a
proper client error; ensure IntegrityError is imported and keep db.refresh(user)
only on success (after the commit).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c9066d8-e1c2-4db8-8379-cd0d300e4d82

📥 Commits

Reviewing files that changed from the base of the PR and between c6eb841 and 6e2e4bf.

📒 Files selected for processing (6)
  • .gitignore
  • pyproject.toml
  • src/api/v1/endpoints/auth.py
  • src/core/config.py
  • src/db/database.py
  • src/main.py
✅ Files skipped from review due to trivial changes (3)
  • .gitignore
  • pyproject.toml
  • src/core/config.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main.py
  • src/db/database.py

@zweadfx zweadfx merged commit c661a58 into main Apr 6, 2026
2 checks passed
@zweadfx zweadfx deleted the feat/jwt-auth branch April 6, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 새로운 기능 구현 시 사용합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] JWT 기반 회원가입/로그인 인증 구현

1 participant