Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

トークン発行API #12

Merged
merged 16 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,26 @@
| `POSTGRES_PASSWORD` | Postgresのパスワード | `postgres` |
| `POSTGRES_HOST` | Postgresのホスト名 | `db` |
| `POSTGRES_PORT` | Postgresのポート番号 | `5432` |
| `JWT_SECRET_KEY` | JWTの秘密鍵 | `8ae240d39...376193c6` |
| `JWT_SECRET_ACCESS_KEY` | JWTの秘密鍵(アクセストークン) | `8ae240d39...376193c6` |
| `JWT_SECRET_REFRESH_KEY` | JWTの秘密鍵(リフレッシュトークン) | `1608144..afdbd` |

### Makefile

|コマンド|内容|
|-----|-----|
|make build |Dockerイメージの作成。開発用のdocker-compose.ymlを指定してビルドを行います。|
|make up |コンテナを起動。このコマンドはdocker-compose upコマンドをラップしており、プロジェクトに必要なサービスコンテナを起動します。|
|make down |コンテナを停止。これはdocker-compose downコマンドのラッパーで、起動しているコンテナを停止し、ネットワークを削除します。|
| make migration |マイグレーションファイルを自動生成。モデルに対して行われた変更を元に新しいマイグレーションファイルを作成します。|
| make build | Dockerイメージの作成。開発用のdocker-compose.ymlを指定してビルドを行います。|
| make up | コンテナを起動。このコマンドはdocker-compose upコマンドをラップしており、プロジェクトに必要なサービスコンテナを起動します。|
| make down | コンテナを停止。これはdocker-compose downコマンドのラッパーで、起動しているコンテナを停止し、ネットワークを削除します。|
| make migration | マイグレーションファイルを自動生成。モデルに対して行われた変更を元に新しいマイグレーションファイルを作成します。|
| make upgrade | データベースに最新のマイグレーションを適用。データベースを最新のスキーマに更新します。|
| make fixtures | fixtures.jsonの中身をDBに登録<br>※DBの中身を初期化してから実施すること |

### Fixture
#### テストアカウント
|id| email | パスワード |
|-----|-----|-----|
| 1 | test01@example.com | pass |

### 動作確認
URL: http://localhost:8000
確認内容: Swagger UIの画面が表示されることを確認。これが表示されれば、アプリケーションが正しく起動している証拠です。
24 changes: 13 additions & 11 deletions application/command/load_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from sqlalchemy.dialects.postgresql import insert

from config.config import get_async_session
from config.config import SessionFactory


def create_file_path(args):
Expand Down Expand Up @@ -55,13 +55,15 @@ async def run(args):
if file_path is None:
return

db = await get_async_session().__anext__()
print(db)

with open(file_path) as f:
for data in json.load(f):
module = import_module(f"models.{data['file']}")
model = getattr(module, data["model"])
stmt = insert(model).values(**data["fields"])
await db.execute(stmt)
await db.commit()
async_session = SessionFactory.create()
db = async_session()
try:
with open(file_path) as f:
for data in json.load(f):
module = import_module(f"models.{data['file']}")
model = getattr(module, data["model"])
stmt = insert(model).values(**data["fields"])
await db.execute(stmt)
await db.commit()
finally:
await db.close()
16 changes: 13 additions & 3 deletions application/config/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,28 @@ class PostgresSettings(BaseSettings):
class JWTSettings(BaseSettings):
"""JWT関連の設定クラス."""

JWT_SECRET_KEY: str
JWT_SECRET_ACCESS_KEY: str
"""シークレットキー

下記コマンドで生成可能
$ openssl rand -hex 32
"""

JWT_SECRET_REFRESH_KEY: str
"""シークレットキー(リフレッシュトークン用)

下記コマンドで生成可能
$ openssl rand -hex 64
"""

JWT_ALGORITHM: str = "HS256"
"""JWTトークンの署名に使用するアルゴリズム"""

JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
"""トークンの有効期限(分)"""
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 9
"""アクセストークンの有効期限(時間)"""

JWT_REFRESH_TOKEN_EXPIRE_MINUTES: int = 90
"""リフレッシュトークンの有効期限(日)"""


settings = VariableSettings()
Expand Down
13 changes: 13 additions & 0 deletions application/crud.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""このスクリプトは、データベース操作に関連する複数の非同期関数を含んでいます."""

from sqlalchemy import delete
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -7,6 +8,7 @@
from models.book import Book
from models.follow import Follow
from models.read_history import ReadHistory
from models.user import User


async def ensure_book_exists(db: AsyncSession, ncode: str) -> int:
Expand Down Expand Up @@ -97,3 +99,14 @@ async def check_follow_exists_by_book_id(
if follow_existence_result.scalars().first() is None:
return False
return True


async def get_user(db: AsyncSession, email: str) -> User:
"""指定されたメールアドレスに紐づくユーザー情報を返す関数."""
result = await db.execute(
select(User).where(User.email == email),
)

user: User = result.scalar_one_or_none()

return user
Empty file.
82 changes: 82 additions & 0 deletions application/domain/user/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""このモジュールは、トークン認証の機能を提供します."""
from typing import Optional

from fastapi import HTTPException, status
from jose import JWTError, jwt
from sqlalchemy.ext.asyncio import AsyncSession

from config.environment import jwt_settings
from crud import get_user
from domain.user.token import create_token
from models.user import User
from schemas.user import AuthUserResponse


async def auth_password(
email: str,
password: str,
async_session: AsyncSession,
) -> AuthUserResponse:
"""id/passwordによる認証を行う関数.

Parameters:
- auth_data (AuthUserModel): 認証するためユーザー情報。
- async_session (AsyncSession): DBとのセッション。

Returns:
- create_tokenのレスポンス
"""

def authenticate_user(user: Optional[User]):
if user is None:
return False
return user.check_password(password)

user = await get_user(async_session, email)

if not authenticate_user(user):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={
"error": "bad_request",
"error_description": "メールアドレスかpasswordが異なります",
},
)

return await create_token(user.id)


async def auth_token(refresh_token: str) -> AuthUserResponse:
"""リフレッシュトークンによる認証を行う関数.

Parameters:
- auth_data (AuthUserModel): 認証するためのユーザー情報。

Returns:
- AuthUserResponse: アクセストークン及びリフレッシュトークン。
"""
try:
payload = jwt.decode(
refresh_token,
jwt_settings.JWT_SECRET_REFRESH_KEY,
algorithms=jwt_settings.JWT_ALGORITHM,
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"error": "unknown_user",
"error_description": "不明なユーザーです",
},
)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={
"error": "invalid_token",
"error_description": "アクセストークンの有効期限切れです。",
},
)

return await create_token(str(user_id))
36 changes: 36 additions & 0 deletions application/domain/user/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""このモジュールは、トークン認証の機能を提供します."""
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

from jose import jwt

from config.environment import jwt_settings
from schemas.user import AuthUserResponse


async def create_token(user_id: str) -> AuthUserResponse:
"""アクセストークン及びリフレッシュトークンを生成する関数.

Parameters:
- auth_data (AuthUserModel): 認証するためのユーザー情報。

Returns:
- AuthUserResponse: アクセストークン及びリフレッシュトークン。
"""
hours = timedelta(hours=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
month = timedelta(days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_MINUTES)
expire = datetime.now(ZoneInfo("Asia/Tokyo")) + hours
refresh_expire = datetime.now(ZoneInfo("Asia/Tokyo")) + month
access_token = jwt.encode(
{"sub": str(user_id), "exp": expire},
jwt_settings.JWT_SECRET_ACCESS_KEY,
algorithm=jwt_settings.JWT_ALGORITHM,
)
refresh_token = jwt.encode(
{"sub": str(user_id), "exp": refresh_expire},
jwt_settings.JWT_SECRET_REFRESH_KEY,
algorithm=jwt_settings.JWT_ALGORITHM,
)
return AuthUserResponse(
access_token=access_token, refresh_token=refresh_token
)
9 changes: 9 additions & 0 deletions application/fixtures/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,14 @@
"book_id": 1,
"read_episode": 1
}
},
{
"model": "User",
"file": "user",
"fields": {
"id": 1,
"email": "test01@example.com",
"password": "$2b$12$WQxKoPvRzBCskMNy38Z0zueGVR.x6ae3o.e0hQGE2NGk9WzAx3ikq"
}
}
]
20 changes: 20 additions & 0 deletions application/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
これにより、他のデータベースモデルクラスはこの基底クラスを継承して、
非同期操作を含むデータベースの操作が可能になります。
"""
import bcrypt
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

Expand All @@ -15,3 +17,21 @@ class Base(AsyncAttrs, DeclarativeBase):
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, comment="ID"
)


class PasswordMixin:
"""ハッシュ化したパスワードを設定する用のMixin."""

_password: Mapped[str] = mapped_column("password", String(60))

def set_password(self, password):
"""パスワードをハッシュ化して設定."""
pwd_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
self._password = bcrypt.hashpw(password=pwd_bytes, salt=salt)

def check_password(self, password):
"""設定したパスワードと一致するかどうかを検証."""
input_password_hash = password.encode("utf-8")
hashed_password = self._password.encode("utf-8")
return bcrypt.checkpw(input_password_hash, hashed_password)
Copy link
Owner Author

Choose a reason for hiding this comment

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

[memo]
設定したパスワード(userテーブルに保存されたデータ)と入力されたパスワードが一致しているか確認するためにエンコードする。
bcrypt.checkpwでinput_password_hash(入力されたパスワード)とhashed_password (設定したパスワード)の突き合わせを実施

15 changes: 15 additions & 0 deletions application/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""CustomerテーブルのORM."""
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from models.base import Base, PasswordMixin


class User(Base, PasswordMixin):
"""ユーザーテーブルのORM."""

__tablename__ = "user"

email: Mapped[str] = mapped_column(
String(254), nullable=False, unique=True, comment="メールアドレス"
)
24 changes: 2 additions & 22 deletions application/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion application/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ asyncpg = "^0.28.0"
alembic = "^1.12.0"
ulid-py = "^1.1.0"
bcrypt = "^4.0.1"
passlib = { version = "^1.7.4", extras = ["bcrypt"] }
Copy link
Owner Author

@furutahidehiko furutahidehiko Mar 22, 2024

Choose a reason for hiding this comment

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

[memo]
passlib使わなくなったので不要かなと思い削除しました

python-multipart = "^0.0.6"
python-jose = { version = "^3.3.0", extras = ["cryptography"] }
beautifulsoup4 = "^4.12.3"
Expand Down
Loading