図解+手を動かして理解する！OAuthの仕組み

アイレット株式会社 新卒 Advent Calendar 2023 1日目の投稿です！
[昨年](https://qiita.com/advent-calendar/2022/iret-22)に引き続き、社外へアウトプットする習慣を作ることを目的として、このアドベントカレンダーを企画させていただきました:fire:
今年は、新卒入社の方全体で募集し、21新卒〜23新卒の方が参加表明をしてくださいました！

私自身、新卒1年目かつ未経験からのスタートであるためまだまだ未熟ではありますが、精一杯アウトプットしていきたいと思っております！

## 概要
認可を実現する機能について興味が湧いたので「OAuth」についてまとめてみました。「OAuthって何？🤔」という疑問を持つ人にもできるだけ伝わりやすくまとめました。
本記事の前半でOAuthの仕組みを図解で説明したのち、後半では実際に手を動かして簡単なOAuth（今回はAuth0）を使ったAPIを作る流れで構成されています。
知識不足で誤ったことを言っているかもしれませんが、その際はご指摘いただけますと幸いです🙇

## OAuthとは
![スクリーンショット 2023-11-18 10.33.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/666aaa79-b214-c700-4b75-5e27a5b54a43.png)


- 認可のための業界標準プロトコル
    - →「安全かつ便利にデータを共有したりログインしたりする」方法について、フレームワーク化（＝汎用化）しておけば使いやすいですよね、という背景から作られた認可の仕組み
        - OAuth2.0やSAMLなどが現在の主流

- OAuthが存在する意義→悪意のあるアプリから好き勝手にユーザーのデータが取られないようにすること
    - OAuthがない世界＝「玄関に鍵のかかっていない家」状態


- OAuthを使ったサービスの代表例
    - [google OAuth](https://cloud.google.com/apigee/docs/api-platform/security/oauth/oauth-introduction?hl=ja)
    - [Auth0](https://auth0.com/jp)
    - [Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)
    - [Microsoft Entra ID](https://www.microsoft.com/ja-jp/security/business/identity-access/microsoft-entra-id)【旧 Azure Active Directory】..etc



## 仕組みを図で整理する
では、どんな仕組みなのか？
仕組みを理解をする上での前段階として、キーワードを簡単にまとめておきます。

-  リソース・・・ **「利用したいサービス」** のこと
    - リソースオーナー・・・サービスを利用するユーザー自身
    - リソースサーバー・・・サービスを提供するサーバー。データが入っている。

- クライアント・・・ **「認可を受けて利用するサイトやアプリ」**


-  トークン・・・**「認可を受けるための合言葉」**
    - アクセストークン・・・リソースサーバーのリソースにアクセスされるためのトークン
        - これには有効期限がついていて、一定時間を経過すると使えなくなる
    - リフレッシュトークン・・・アクセストークンを再発行するために使用するトークン
        - アクセストークンの有効期限が切れてしまった場合に、再度発行するために使用する
 

- 認可サーバー・・・クライアントからの要求に応じてアクセストークンを発行するサーバー
（認可サーバー＝リソースサーバーの役割を一つのサーバが兼ねることもある）


 以上を踏まえた上で、OAuthの仕組みを図にまとめます。
![スクリーンショット 2023-11-20 9.26.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/255b417c-2186-f926-1722-ea4b7e1ec516.png)
 
#### ①・②
「Googleでログイン」するボタンを押す時に出てくる画面をイメージしていただくとわかりやすいです。ここで、Googleが保持しているリソースユーザーの情報（画像の例であれば名前、メールアドレス、プロフィール写真）を、クライアントに提供して良いかを確認しています。
![IMG_0330.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/ffec1c48-8712-ad9b-5379-96fc4c53408d.png)

#### ③・④
②でユーザーが情報の提供の許可をした場合、その裏側ではリソースサーバー（Google）からクライアントに向けて（twitter.com）トークンの発行が行われています。

#### ⑤・⑥
トークンを受け取ったクライアントはリソースサーバーに対して受け取ったトークンを提示します。トークンの正当性が確認できればリソースを利用できる
→↑の画像の例で言うならば、GoogleのアカウントでX（旧Twitter）を利用できることになります。




## デメリット

- OAuthはあくまでも認可のみを行うものであり、認証の機能は行わない。
    - つまり、認証のフローは別で用意しなければいけない、という不便な点があります。
→そこで、<span style="color:#c94c03;">OIDC（OpenID Connect）</span>というものが生まれています。（本記事では触れませんが、別の機会にまとめようと思います。）


## 実際に使ってみる〜fastAPIで認可を実現〜
今回は認証・認可基盤を提供するAuth0を使って、FastAPIで、簡単な認可を実現するAPIを作ってみたいと思います。
作成にあたり、[こちら](https://auth0.com/blog/build-and-secure-fastapi-server-with-auth0/)を参考にさせていただいてます。
#### 事前準備

- auth0のアカウント作成
    - https://auth0.com/signup
    - 分からない用語がいろいろ出てくると思いますが、[こちら](https://dev.classmethod.jp/articles/auth0-architecture/)の記事がわかりやすいのでご参考までに。（ここでは触れないことにします。）
- ライブラリのインストール
```
pip install fastapi 'uvicorn[standard]' pydantic-settings 'pyjwt[crypto]'
```


#### STEP1 pythonファイル・envファイル作成
3つのファイル+環境変数を設定するenvファイルを作成します。
★APIのエンドポイントの定義→auth0fastapi.py
```python
from fastapi import FastAPI, Security
from utils import VerifyToken

# FastAPIインスタンス、VerifyTokenクラスの作成
app = FastAPI()
auth = VerifyToken()

@app.get("/api/public")
def public():
    """アクセストークン不要のエンドポイント"""
    result = {
        "status": "success",
        "msg": ("アクセストークン不要で誰でもアクセスできます")
    }
    return result

@app.get("/api/private")
def private(auth_result: str = Security(auth.verify)):
    """アクセストークンが必要なエンドポイント"""
    return auth_result
```
◎ポイント→依存性注入を使っているところ
依存性注入とは？？→関数の引数にオブジェクトそのものを渡してやること
これによって、クラスとクラスの関係性が下がり、保守性が上がります。（プログラムが疎結合なる）
FastAPIでは、関数に必要なデータを引数として宣言するだけで、必要なデータが提供されます。（[こちら](https://fastapi.tiangolo.com/tutorial/dependencies/)参照）

★トークンの検証→utils.py
```python
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import SecurityScopes, HTTPAuthorizationCredentials, HTTPBearer
from config import get_settings

# 認可されていない場合（アクセストークンが誤っている場合）403を返すクラス
class UnauthorizedException(HTTPException):
    def __init__(self, detail: str, **kwargs):
        """Returns HTTP 403"""
        super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)

# アクセストークンがセットされていない場合に401を返すクラス
class UnauthenticatedException(HTTPException):
    def __init__(self):
        super().__init__(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
        )

# PyJWTを使った認可を行うクラス
class VerifyToken:
    # config.pyから設定を取得する
    def __init__(self):
        self.config = get_settings()
         # Auth0のjwk(後述)を取得するためのURL
        jwks_url = f'https://{self.config.auth0_domain}/.well-known/jwks.json'
        self.jwks_client = jwt.PyJWKClient(jwks_url)

    # デコレーター：アクセストークンの検証を行い、成功した場合ペイロードを返す
    async def verify(self,
                     security_scopes: SecurityScopes,
                     token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer())
                     ):
        # トークンがセットされていない場合は401を返す
        if token is None:
            raise UnauthenticatedException
        # トークンから、'kid'を取得し、そのキーで署名検証を行う
        try:
            signing_key = self.jwks_client.get_signing_key_from_jwt(
                token.credentials
            ).key
       #jwksからのキー取得、処理やトークンをデコードする際にエラーが発生した場合は403を返す 
        except jwt.exceptions.PyJWKClientError as error:
            raise UnauthorizedException(str(error))
        except jwt.exceptions.DecodeError as error:
            raise UnauthorizedException(str(error))
        
        # トークンのデコードを行い検証を行う
        try:
            payload = jwt.decode(
                token.credentials,
                signing_key,
                algorithms=self.config.auth0_algorithms,
                audience=self.config.auth0_api_audience,
                issuer=self.config.auth0_issuer,
            )
        except Exception as error:
            raise UnauthorizedException(str(error))
        return payload

```
★アプリケーションの設定→config.py
```python
from functools import lru_cache
from pydantic_settings import BaseSettings

# BaseSettingクラスを継承して型検証とデータ変換を行うクラス
class Settings(BaseSettings):
    auth0_domain: str
    auth0_api_audience: str
    auth0_issuer: str
    auth0_algorithms: str
    
    # 作成した.envファイルから、環境変数の値を読み込むクラス
    class Config:
        env_file = ".env"

# @lru_cacheを頭につけることで、関数の結果（ここではSettingクラスのインスタンス）をキャッシュすることができる
@lru_cache()
def get_settings():
    return Settings()

```
.envファイル
```
AUTH0_DOMAIN = XXXXXXXXXXXX.us.auth0.com
AUTH0_API_AUDIENCE = https://fastapi-auth0-test.com
AUTH0_ISSUER = https://XXXXXXXXXXXX.us.auth0.com
AUTH0_ALGORITHMS = RS256
```


#### STEP2 サーバー起動、死活監視用のAPIで起動チェック
①下記コマンドでAPIを実行
```
uvicorn auth0fastapi:app --reload --env-file .
```

- `--reload`オプションで、ホットリロード（コードの変更時にサーバーを再起動してくれる）が有効になる
- `--env-file (.envファイルの位置する相対パス)`をつけてやることで、.envファイルの設定を読み込んでくれる


②http://127.0.0.1:8000/docs
にアクセスしSwaggerを開き、パブリックのエンドポイントにアクセスし、APIコールを行い、レスポンスが問題なく帰ってくることを確認する


#### STEP3 curlでエンドポイントにアクセスしアクセストークンを取得
![IMG_0346.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/5a773f1b-02c2-dd35-79a4-0283bb18f2f9.png)


client_id・client_secret, audience, grant_typeをセットしてcurlコマンドを叩きます。
Auth0ダッシュボード>Applications>APIs>から自分の作成したAPIを選択し、
Testの部分を押すと、何をセットすればいいのかが確認できます。（写真上段）

下記コマンドを実行すると、レスポンスが帰ってきます。（写真下段）
このレスポンスの中の、`"access_token": "ey......."`
の部分がアクセストークンであり、アクセストークンを取得できることを確認できました。


#### STEP4 HTTP Bearer tokenをセットしAPIの疎通確認
http://127.0.0.1:8000/docs
にアクセスしSwaggerを開き、プライベートのエンドポイント（/private）に、先ほど取得した、アクセストークンをセットしてAPIをコールします。
![IMG_0347.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/7c37b663-6a8e-80f1-2884-50a770857365.png)

比較のために、以下の以下の4つの条件で疎通確認をしてみました。
4条件それぞれで、帰ってくるレスポンスが違うことがわかります。
＜正しいトークンをセットした時＞
![IMG_0335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/bd60f8a4-ef97-ae55-750d-ad7232fc5437.png)


＜トークンをセットしない時＞→Not Authentificated
![IMG_0337.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/83ec7daa-3e0a-8d6b-e585-971a19557835.png)


＜誤ったトークンをセットした時＞→Not enough segments
![IMG_0338 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/f38e7320-e342-30d4-1526-d82e37304c61.png)


＜トークンの有効期限が切れている時＞→Signature has expired
![IMG_0341.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/3697d1d1-5fde-b175-79d9-ebd6a08fe16d.png)

正しいトークンがセットされている場合は、`"iss": XX, "sub": XX` のようなものがレスポンスとして帰ってきます。
それぞれの意味については下記の通りになっています。

:::note info


- iss(issuer)→トークンの発行者
- sub(subject)→認証を行う対象ユーザー
- aud(audience)→トークンが利用されるクライアント
- iat(issued at)→トークンが発行された日時（※エポック時間にて表現される）
- exp(expiration)→トークンの有効期限（※エポック時間にて表現される）
- azp(authorized party)→トークンの発行者が指定されたクライアントに発行されたトークン
- gty(grant type)→OAuth2.0の仕様において、クライアントが利用する認可の仕組みの種類を示すためのもの
    - 写真の場合だと、client_credential→ **「クライアントが自身の認証情報を使用してアクセストークンを取得する仕組み」** となっている。
    - 他にも、Resource owner password credentials、Implicit grant、Authorization code grantなどがある（詳細は[こちら](https://dev.classmethod.jp/articles/authentication-and-authorization-again/)参照）
:::

## 実現しようとしたができなかったこと
[こちら](https://tech.jxpress.net/entry/2019/12/05/161941)を参考に、
バックエンド：FastAPI,フロントエンド：Javascript（Node.js）で、認可機能を盛り込んだSPAを作成すること
→バックエンド側の理解で精一杯となってしまったので、断念しました。

イメージとしては、、「Auth0を使って、ログイン済みかつ権限のある特定のユーザーにのみページを表示させるアプリケーションを作成」するつもりでした。

参考までに、auth0が提供している[サンプルコード](https://auth0.com/docs/quickstart/spa/vanillajs/interactive)を使って、Auth0を使った簡単なソーシャルログインを作成することができますので、作成してみたい方は参考にしていただくといいのかもしれません。(下記参照)

ログイン前（右上にLog in ボタンが表示）
![IMG_0331.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/f07255e1-e439-4746-d005-466b59a5edb2.png)
↓
Log inボタン押下後（＝認証（ログイン）画面）
![IMG_0334.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/0b65d41c-0c57-d813-561b-107f228faeee.png)
↓
Continue with Google、その後の画面でAccceptを押下
![IMG_0332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/ce5a3d41-e3a3-8b50-b2a9-1565b0f301f1.png)

↓
ログイン成功→（登録したGoogleアカウントでログインができている）
![IMG_0333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/ec206473-3529-80b1-2f03-ad7ae750ab21.png)





※[ユニバーサルログイン](https://www.macnica.co.jp/business/security/manufacturers/okta/tech_auth0_universal_login.html)というものがあり、事前にAuth0側で準備されているログイン画面を使用・GUI上でポチポチしながらカスタマイズすることもできるようです。

## 最後に
OAuthの仕組みについてできるだけ、できるだけ噛み砕いた表現を意識しまとめてみましたがいかがだったでしょうか。本記事がOAuthについての理解の一助となれば幸いです。

まだまだ知識不足だなと痛感しているので、引き続きAuth0をはじめとしたOAuthが使われるサービスについて学び、認可の仕組みの理解度を上げていきたいと思っています。
最終的には独力で、Auth0を組み込んだ簡単なSPAを作ることが目標です！

## 補足事項（興味を持ったことや理解が浅い用語について）
- JWTとは
    -   JWT（「JSON Web Token」）とは、JSON形式のデータをBase64エンコードしたもの
    -   データに、署名や暗号化を行うことで、データを安全な方法で共有することが目的
    -   ヘッダ・ペイロード・署名の3つのセクションから構成されていて、各セクションは.(ドット)で区切られている
        -   ヘッダ→アルゴリズム・トークンのタイプが指定されている
        -   ペイロード→データの中身
        -   署名→トークンの正当性を証明するもの

-  JWKとは（JSON Web Key）
    -  JSON形式で表現された公開鍵や秘密鍵を定義するための仕様
    -  JWTの署名の検証を行うもの（そのJWTが本物かどうか？を確認する）
 
- HTTP Bearer Token
    - HTTP通信において、認証・認可のために使われるトークンのこと

## 参考文献
https://oauth.net/2/
https://auth0.com/jp/intro-to-iam/what-is-oauth-2
https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be
https://www.youtube.com/watch?v=ZV5yTm4pT8g
https://www.youtube.com/watch?v=PKPj_MmLq5E
https://www.youtube.com/watch?v=1aha4yrT2l0
https://auth0.com/blog/build-and-secure-fastapi-server-with-auth0/
https://qiita.com/ninomiyt/items/ee676d7f9b780b1d44e8
https://qiita.com/kotaroooo0/items/4d471932e299edd08b24
https://zenn.dev/mikakane/articles/tutorial_for_jwt
[「この一冊で全部わかる」Web技術の基本（SB Creative）](https://www.amazon.co.jp/%E3%82%A4%E3%83%A9%E3%82%B9%E3%83%88%E5%9B%B3%E8%A7%A3%E5%BC%8F-%E3%81%93%E3%81%AE%E4%B8%80%E5%86%8A%E3%81%A7%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8BWeb%E6%8A%80%E8%A1%93%E3%81%AE%E5%9F%BA%E6%9C%AC-%E5%B0%8F%E6%9E%97-%E6%81%AD%E5%B9%B3/dp/4797388811/ref=sr_1_1?adgrpid=146868169346&hvadid=665685742310&hvdev=c&hvlocphy=1028853&hvnetw=g&hvqmt=e&hvrand=6257548715910578456&hvtargid=kwd-2008547807236&hydadcr=27494_14701157&jp-ad-ap=0&keywords=%E3%81%93%E3%81%AE%E4%B8%80%E5%86%8A%E3%81%A7%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8Bweb%E6%8A%80%E8%A1%93%E3%81%AE%E5%9F%BA%E6%9C%AC&qid=1701212989&sr=8-1)