次世代プロジェクト管理ツールの実装　ログイン認証

https://qiita.com/EasyCording/items/7c2e51031b818b94b283

https://qiita.com/EasyCording/items/fc60ea6e80f59edcb8ec

# 状況
* 今月中にツール完成を目指しコーディングを開始
* API認証で３日ほど悩んでいたが、ようやく理解できるようになってきた。

## ログイン認証
　いろいろBINGに質問したりして、適当に記事を書いたのであるが、なんと自分のQiita投稿が、勝手にBingに引用されて困っている。これからはCHATGPT自身が、CHATGPTの記事を引っ張ってくるというような、信憑性のかけらもない世の中が来るのではないかと危惧している（ちなみにゴミ投稿は２度と出ないように削除した）
　そんな汚染された世の中は嫌なので、BINGは使わず、VSCODE経由でCHATGPT-APIを使うようにした。さらに日本語ソースは信用ならないので、英語ソースで聞くことにした。

## CHATGPT
ポイントは中間サーバーを使ってREDMINE-APIにアクセスする方法である。
Authentication method to REDMINE-API via an intermediate server

## REACT

```Js.js
import React, { useState } from "react";

const Login = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [errorMessage, setErrorMessage] = useState(null);

  const handleLogin = async (event) => {
    event.preventDefault();
    try {
      const response = await fetch("/api/token", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ username, password })
      });
      if (!response.ok) {
        throw new Error("Invalid credentials. Please try again.");
      }
      const data = await response.json();
      localStorage.setItem("token", data.access_token);
      // Redirect to the main app dashboard
      window.location.replace("/dashboard");
    } catch (error) {
      setErrorMessage(error.message || "An unexpected error occurred. Please try again later.");
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input type="text" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} />
      <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Log in</button>
      {errorMessage && <div style={{ color: "red" }}>{errorMessage}</div>}
    </form>
  );
};

export default Login;
```

# Mid Server

```fastapi.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from datetime import timedelta, datetime
from jose import JWTError, jwt

app = FastAPI()

SECRET_KEY = "mysecretkey"  # Change this to a more secure value in production
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedpassword",
        "disabled": False,
    },
}

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")

def generate_token(username: str):
    access_token_expires = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode = {"sub": username, "exp": access_token_expires}
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Could not validate credentials")
    except JWTError:
        raise HTTPException(status_code=401, detail="Could not validate credentials")

    user = fake_users_db.get(username)
    if user is None:
        raise HTTPException(status_code=401, detail="Could not validate credentials")
    return user

@app.post("/api/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user_dict = fake_users_db.get(form_data.username)
    if not user_dict or form_data.password != "secret":
        raise HTTPException(status_code=401, detail="Incorrect username or password")

    access_token = generate_token(user_dict["username"])
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/api/items/")
async def read_items(current_user: dict = Depends(get_current_user)):
    return [{"item_id": "Foo", "description": "An item"}, {"item_id": "Bar", "description": "Another item"}]

```

# 疑問

じっくり見ると、エンドポイント```/api/token```が２つあるではあーりませんか？？

一つ目
```oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")```
二つ目
```@app.post("/api/token"):async def login(form_data: OAuth2PasswordRequestForm = Depends()):```

仕方なく追加で質問する（英語勉強して良かった～。すらすら質問できる）

 why end point "/api/token" is repeated twice in the fastapi code? Which is work first? Why it dupplicate? it is work corectlly?

これをBingに渡すと説明してくれる。（これからはCHATGPTも分業の時代か～）

# 要約

このフローでは、以下のことが起こっています。

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: POST /api/token with username and password
    S->>C: Return JWT token if authenticated
    C->>S: GET /protected with Authorization header (Bearer token)
    S->>C: Validate token and return protected resource if authorized
```

* クライアントは、ユーザー名とパスワードを含むPOSTリクエストを/api/tokenというURLに送ります。
* サーバーは、ユーザーを認証し、JWTトークンを返します。
* クライアントは、JWTトークンをAuthorizationヘッダーに付与して、/protectedというURLにGETリクエストを送ります。
* サーバーは、トークンを検証し、認可されていれば保護されたリソースを返します。

このフローでは、/api/tokenというURLが2回登場しますが、それぞれ異なる役割を果たしています。

# 解説
* OAuth2PasswordBearer関数のtokenUrlパラメータに/api/tokenというURLを指定することで、FastAPIにトークンの検証を行う場所を教えています。この関数は、クライアントから送られたトークンを読み取り、有効かどうかを判断します。有効であれば、ユーザーは保護されたルートにアクセスできます。
* @app.post(“/api/token”)デコレータは、トークンの取得を行うエンドポイントを指定しています。このエンドポイントは、ユーザー名とパスワードを含むPOSTリクエストを受け取り、JWTトークンと引き換えに返します。このトークンは、クライアントが保護されたルートに認可されたリクエストを送るために使用できます。
* つまり、両方のURLは同じリソースを指していますが、目的は異なります。一方はトークンの検証のためであり、もう一方はログインとトークンの取得のためです。これらのエンドポイントを分けることが重要です。なぜなら、認証プロセスはログインプロセスと異なるからです。

### 要約すると、このURLの重複は意図的なものであり、両方のエンドポイントはFastAPIでトークンベースの認証機能を提供するために協力しています。