SPA の認証に GitHub OAuth を使おうとしてつまづいた話

この記事は [N予備校プログラミングコース Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nyobi) の 5 日目の記事です。

大遅刻してごめんなさい！

こんにちは、 N予備校の教材制作補助アルバイトをしている dorimiamn です。

今回は N予備校第 4 章の内容で扱う GitHub OAuth を自分なりに SPA で使おうとして詰まった話を書きます！

試行錯誤の結果から記事を書いており、検証したい箇所が依然残っています。
その部分含めて致命的な部分は加筆したいと考えています。

# やりたいこと

私は習作として、 サーバをフロントエンドとバックエンドに分けた Todo アプリを作成していました。

このアプリのログイン機構にせっかくなので、 4 章で習う GitHub OAuth を利用したいと考えました。

しかし、この Todo アプリはサーバが 2 つある為、 [Passport.js](https://www.passportjs.org/) を使う方法では上手くいきませんでした。

# 要約

:::note warn 

これはあくまで OAuth だけを考えた処理です。
実際にはセキュリティの観点からさらなる実装が要求されます。

例えば、認証コードを要求する際に、 state を持たせることでアクセストークンを悪意ある第三者に奪われることを防ぐ処理が必要です。

参考 : [【連載】世界一わかりみの深いOAuth入門 〜 その4:stateパラメーターによるCSRF対策 〜](https://tech-lab.sios.jp/archives/25642)

:::

[Passport.js](https://www.passportjs.org/) が上手く機能しなかったので、公式が公開している [GitHub OAuth](https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps) のページを参考に仕組みを調べ、それを用いて認可しました。
(これは私の調査不足で、Passport.js で実装できるかもしれませんが、私の調べた限りでは厳しそうでした。)

具体的には以下のような流れで処理を行いました。

![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266262/cc268625-bc5d-c256-7192-296a3b526c91.png)

簡単に言葉で説明をすると、

1. クライアントから操作を行い、認可コードを取得
1. バックエンドにて認可コードを使って Access Token, Refresh Token, ID Token を取得する。
1. アクセストークンをバックエンド側で管理し、セッションを発行する。

といった流れです。

なお、途中の通信において CORS を設定しなければならない部分があります。

今回、特に問題だった部分は Access Token を取得するまでの部分です。
セッションについてはまた別の記事に譲ろうと思います。

以下に具体的な実装を載せます。

<details><summary>サンプルコード</summary>

## フロント側 React + TypeScript

```tsx
axios.defaults.withCredentials=true;

const endPoint: string = 'http://localhost:3001/'

type User_Info={
    username:string;
}

export default function github_callback(){
    const [username,setUsername]=useState<String | null>();
    const location=useLocation()
    
    useEffect(() => {
        //ユーザー認証
        axios.get('http://localhost:3001/auth/github'+location.search)
            .then((res:AxiosResponse<User_Info>)=>{
                console.log(res.data.username);
                setUsername(res.data.username);
            })
    }, []);

    return(
        <div>
            <h1>コールバック</h1>
            <p>あなたのユーザ名は{username}</p>
        </div>
    )
}
```

## バックエンド側 Node.js + Express

CORS 設定

cors モジュールを用いています。

```ts
app.use(cors({
    origin: ['http://localhost:3000'],
    credentials: true,
}))
```

OAuth 処理

```ts
router.get('/github',(req,res,next)=>{
    console.log('code:',req.query['code']);
    //クライアント側から受け取った code を使って、 GitHub から code を取得
    axios.post('https://github.com/login/oauth/access_token',{
        client_id:GITHUB_CLIENT_ID,
        client_secret:GITHUB_CLIENT_SECRET,
        code:req.query['code'],
    },{
        headers:{
            'Accept':'application/json'
        },
    }).then(async(token_res:AxiosResponse<Token_Res>)=>{
        // 意図した情報を受け取れているかの確認
        const {data,status}=token_res;
        console.log('data:',data);
        console.log('access_token:',data['access_token']);
        // access_token を用いてユーザ情報を取得
        await axios.get('https://api.github.com/user',{
            headers:{
                Authorization: `Bearer ${data['access_token']}`,
            }
        }).then((user:AxiosResponse<User_Info>)=>{
            console.log('GitHub access Succeeded!')
            // きちんとユーザ情報を受け取れているかの確認
            console.log(user.data);
            
            res.json({
                username:user.data.login,
            });
        }).catch(err=>{
            console.log(err);
        })
    }).catch(err=>{
        console.log(err);
    })
});
```

</details>

## 環境

### フロントエンド

ポート : 3000

Vite を利用して開発環境を整えています。

- React
- TypeScript

### バックエンド

ポート : 3001

こちらは、プレーンなものを使っています。

- Express
- TypeScript


# 何がしたかったか？

N予備校で学んだことを更に生かすために、モダンな Web 開発の勉強がしたくて TODO アプリを作っていました。

構成はフロントエンドは React を、バックエンドは Express をとった形でした。

アプリにはユーザ認証が必要です。
ここで GitHub OAuth を使おうと思いました。
これを用いることで自分の作ったアプリの認証を GitHub の認証機構を借りて行えます。
これを SPA で、つまりフロント用のサーバとバック用のサーバに分けた状態でも行えるようにしたかったわけです。

簡単に行えるだろうと思ったらところどころ結構詰まったので、記事にまとめてみようという試みです。

# つまづいたところ

つまづいた部分としては主に、

- OAuth
- CORS

の 2 点です。

サーバ 1 台でフロントエンドもバックエンドもこなす場合は両者ともに問題ではなかったのですが、サーバを分けた途端、セキュリティの問題と、使用したライブラリがその使用に適していなかった問題が出てきました。

元々、 Passport.js を用いて何とかするつもりだったのですが、調べていてもあまりに情報がなくかなり右往左往してしまいました。

何やかんやあって OAuth を使えば良いらしいとわかり、実装できました。

方向性を理解しても、 OAuth と CORS の処理を理解して実装するには時間がかかってしまいました。

# OAuth とは

一言で言えば、ユーザの許可を得てユーザの代わりに操作を行っても良いという許可を発行するプロセスを標準化したものを指します。

この、**許可を得てユーザの代わりに操作を行っても良い**という部分は認可というものに相当する部分でしょう。

OAuth は認証ではなく認可のための仕組みだとよく言われているのはこれが所以でしょう。
ところで認証はというと、本人かどうかを確認する仕組みを指すものと理解しています。

OAuth の仕組みについては、以下の記事が一番わかりやすいと思います。

https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be

私は OAuth を実装するとしても、フロントエンドとバックエンドを分けた構成でどのように実装すれば良いのか思いつきませんでした。

私の思いついた安直な実装ではセキュリティ上の問題がありそうでしたので、 Google を使って検索をしてみると、以下のような記事を見つけて実装の糸口が見つかりました。

https://trap.jp/post/1007/

こちらの記事では、 OAuth 2.0 の代表的な 2 つのフローの解説が載っています。

- Implicit Flow
- Authorization Code Flow

上記の内、私がやりたかったのは、 Authorization Code Flow です。
こちらならセキュリティ強度が高く問題なさそうでした。

流れはわかったけど、具体的な実装はどうしたら良いのだろうとさらに調べたところ、以下の記事を見つけ、こちらを元に GitHub OAuth の実装を行いました。

https://i-ssue.com/topics/d19a19d3-eaf6-4284-8f9a-8ea529bab6f0


## CORS とは

CORS とは、 Cross-Origin Resource Sharing の略称です。
セキュリティの観点から、ブラウザは異なるオリジン間での HTTP リクエストを弾きます。
ブラウザに弾かれないよう、特定のオリジンに対して権限を与えるのが CORS の仕組みです。

フロントエンドとバックエンド間での HTTP リクエストがこれによって弾かれてしまい、どうしたら弾かれないのかこちらも試行錯誤しました。

MDN のページとこちらの Qiita 記事を参考にさせていただきました。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

https://qiita.com/att55/items/2154a8aad8bf1409db2b

## GitHub OAuth の実装

[公式が公開している手順](https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps)に沿って実装しました。

要約にまとめてありますが改めて記述します。

### CORS の設定

まず、フロントエンドとバックエンド間での HTTP リクエストを許可するように、 CORS を設定します。

クライアントサイドでは特に設定はなく、バックエンドを設定します。

Express では [cors モジュール](https://expressjs.com/en/resources/middleware/cors.html)が用意されていますので、こちらを使います。

以下のように、設定を行います。

`origin` には、 リクエストを許可するオリジンを記述します。配列として渡しますので、複数のオリジンを許可できます。

```ts
import cors from 'cors';

// 省略

app.use(cors({
    origin: ['http://localhost:3000'],
    credentials: true,
}))
```

これによって、 `localhost:3000` と `localhost:3001` という異なるオリジン間でのリクエストが許可されました。

### GitHub へのログイン

まず、ユーザに以下のリンクへアクセスさせて GitHub での認証を行わせました。
GITHUB_CLIENT_ID は OAuth アプリの Client ID を、 callBackURI は認証後にリダレクトされる先の指定となります。

OAuth アプリの登録時にも callBackURI の登録はしたのでもしかしたら必要ないかもしれません？

```js
'https://github.com/login/oauth/authorize?client_id='+GITHUB_CLIENT_ID+'&redirect_uri='+callBackURI
```

今回は、 callBackURI として、 `http://localhost:3000/auth/github` を指定しました。

そのため、 認証が済めばこの URI に code パラメータが付与された状態で戻されます。

`http://localhost:3000/auth/github?code=XXXXXX`

### code パラメータをバックエンドに渡す

この code パラメータを読み取ってバックエンド側に渡します。

```tsx
axios.get('http://localhost:3001/auth/github'+location.search)
```

`location.search` と書くことでパラメータを取得できますので、それをそのままバックエンド側の URI にくっつけています。

GET メソッドを使っていますが安全性を高めるなら POST メソッドを使った方が良いです。
code だけ知られても CLIENT_SECRET がわからなければ認可はされないためです。

### GitHub API からアクセストークンを貰う

バックエンド側から GitHub API に、 code、 Client ID、 Client Secret を渡してアクセストークンを貰います。
この時、形式を指定することでユーザ情報を JSON 形式で受け取れますので、今回はその形式で受け取るように Header に指定しています。

JSON 以外にも XML 形式で受け取れるようです。

https://docs.github.com/ja/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github

```ts
    // code パラメータの情報を確認
    console.log('code:',req.query['code']);
    // GitHub API からユーザ情報を貰う
    axios.post('https://github.com/login/oauth/access_token',{
        client_id:GITHUB_CLIENT_ID,
        client_secret:GITHUB_CLIENT_SECRET,
        code:req.query['code'],
    },{
        headers:{
            'Accept':'application/json'
        },
    })
```

### アクセストークンを用いてユーザ情報を取得

続いて、取得したアクセストークンを用いてユーザ情報を取得します。

リクエストヘッダーの [Authorization](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Authorization) を、`Bearer <AccessToken>` という形で設定します。

引数 `token_res:AxiosResponse<Token_Res>` は TokenRes 型のデータを受け取ることを明示しています。

これによって、 `token_res.data` を `TokenRes` 型のプロパティとして参照できます。

この引数の話は以下の記事を参考に定義しました。

https://zenn.dev/mkt_engr/articles/axios-req-res-typescript


```ts
    .then(async(token_res:AxiosResponse<Token_Res>)=>{
        const {data,status}=token_res;
        console.log('data:',data);
        console.log('access_token:',data['access_token']);
        await axios.get('https://api.github.com/user',{
            headers:{
                Authorization: `Bearer ${data['access_token']}`,
            }
        })
```

`TokenRes` は以下のように宣言されています。

```ts
type Token_Res={
    access_token:string;
    scope:string;
    token_type:string;
}
```

### 取得したユーザ情報の確認

先ほどと同様に、 `User_Info` という型を定義して、ユーザ情報を受け取ります。


```ts
    }).then((user:AxiosResponse<User_Info>)=>{
        console.log('GitHub access Succeeded!')
        console.log(user.data);
        // フロント側にユーザ情報を返す処理
```

以上の処理によってユーザ情報を取得できました。



## まとめ

### 反省

自分の検索能力の低さを痛感しました。

Passport.js に固執するのではなく、 GitHub OAuth を実装するように考えるべきでした。

自分が行いたいことをどのように言語化するかで求められる情報にどれだけたどり着けるかがかなり変わりそうです。

これからは一旦何かに自分の行いたいことを書き出してイメージを作ってから、調べ物をしたいというのが反省です。

もう一つ反省するならば、様々なモジュールが公開されており、自分で実装するという考えよりも何かないだろうか？という考えが強かったのかなと思います。

必要に迫られれば自力実装をすぐにしたほうが良さそうです。

### 認証・認可のロジックを実装してみて

認証・認可については理論などについては[暗号技術入門](https://www.sbcr.jp/product/4797382228/)を読んでいたので何となく知ってはいたのですが実際に実装できたのは嬉しかったです！

理論と実装のギャップが埋まる感覚を経験できたのは、遠回りしてよかったなという気持ちにさせてくれます。

### 今後

セキュリティに関わる部分をよりしっかりさせて再度記事として書ければ良いなというところと、習作として作っている Todo アプリをデプロイするところまでなんとか持っていければと考えています。

ここまで読んでくださりありがとうございました！
少しでもご参考になれば幸いです。