React nativeでoauth2.0の認証を行う方法



どうもこんちにはたくびー(@takubii)です。
react native内でoauth2.0で認証を行う方法を最近勉強していました。
動くものができたので知識共有もかねて紹介したいと思います。

### Linkingを使って外部ブラウザから認証を行う
`Linking`を使用してアプリからブラウザへ認証ページへのリンクを作成します。
早速コードの方を見てもらいたいと思います。
申し訳ないですが、細かいところは省略しています。

ざっくりとした全体像
```typescript
import React, {useState, useEffect} from 'react';
import {SafeAreaView, Linking} from 'react-native';

function App(): JSX.Element {
    const [accessToken, setAccessToken] = useState('');
    const [refreshToken, setRefreshToken] = useState('');

    // リダイレクトでアプリに帰ってきた時の処理をイベントリスナーに登録
    useEffect(() => {
        const listener = Linking.addEventListener('url', async event => {
            const redirectUrl = event.url;

            // トークンエンドポイントへアクセスしてアクセストークンを取得します
            const response = await fetch('{TOKEN_ENDPOINT}', {
                method: 'POST',
                // 必要な場合はヘッダーに情報を追加してください。
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    Authorization: `Basic ${encode('{YOUR_CLIENT_ID}:{YOUR_CLIENT_SECRET}')}`
                },
                body: `grant_type=authorization_code&redirect_uri=${redirectUrl}`,
            });

            if (!response.ok) {
                return;
            }

            const json = await response.json();
            setAccessToken(json.access_token);
            setRefreshToken(json.refresh_token);
        });

        return () => {
            listener.remove();
        };
    }, []);

    // アプリからブラウザの認証ページへ飛ばす処理
    const startAuth = async () => {
        // 認証に使うURLを入力してください
        const authUrl = '{AUTH_URL}';
        const canOpen = await Linking.canOpenURL(authUrl);

        if (!canOpen) {
            return;
        }

        await Linking.openURL(authUrl);
    };

    return (
        <SafeAreaView>
            <Button title="auth" onPress={startAuth} />
            {accessToken && <Text>{`access token = ${accessToken}`}</Text>}
        </SafeAreaView>
    );
}
```

### 各メソッドの詳細
全体のコードだけだと理解の把握に時間がかかると思うので、それぞれのメソッドで何をしているのか簡単に解説したいと思います。
#### startAuthの処理
以下のコードでボタンがクリックされた時に認証ページへ飛ばす処理をしています。
`Linking.openURL`を使うことでブラウザが開くので、ブラウザ側で認証を行ないます。
リダイレクトURLにアプリのスキーマに合わせたURLを含めることで認証後にアプリに戻ってきます。
※リダイレクトでアプリに戻す方法ですが、こちらの記事で書くととても長くなるので割愛します。
```typescript
    const startAuth = async () => {
        // 認証に使うURLを入力してください
        const authUrl = `${AUTH_URL}?/response_type=${responseType}&scope=${scope}&client_id=${clientId}&redirect_uri=${redirectUri}`;
        const canOpen = await Linking.canOpenURL(authUrl);

        if (!canOpen) {
            return;
        }

        await Linking.openURL(authUrl);
    };
```
Linkingを使うためにはIOS、Androidでそれぞれ別途設定が必要になります。
##### IOS
/ios/{APP_NAME}/AppDelegate.mmに以下を追加してください。
```objective-c:/ios/{APP_NAME}/AppDelegate.mm
#import <React/RCTLinkingManager.h>
  :
  :
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}
```

##### Android

```xml:/android/app/src/main/AndroidManifest.xml
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
```
#### useEffect内の処理
こちらのuseEffect内ではリダイレクトでアプリに戻ってきた時の処理を記載しています。
`AUTHORIZATION_ENDPOINT`から戻ってきたURLには`code`が含まれているので、そちらのcodeを利用してTokenの取得を行なっています。
やっていることは単純で、取得したURLを`redirect_uri`に含めて`TOKEN_ENDPOINT`へfetchしています。
通信が成功すればJSON形式でアクセストークンとリフレッシュトークンが返ってきます。
```typescript
    useEffect(() => {
        const listener = Linking.addEventListener('url', async event => {
            const redirectUrl = event.url;

            // トークンエンドポイントへアクセスしてアクセストークンを取得します
            const response = await fetch('{TOKEN_ENDPOINT}', {
                method: 'POST',
                // 必要な場合はヘッダーに情報を追加してください。
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    Authorization: `Basic ${encode('{YOUR_CLIENT_ID}:{YOUR_CLIENT_SECRET}')}`
                },
                body: `grant_type=authorization_code&redirect_uri=${redirectUrl}`,
            });

            if (!response.ok) {
                return;
            }

            const json = await response.json();
            setAccessToken(json.access_token);
            setRefreshToken(json.refresh_token);
        });

        return () => {
            listener.remove();
        };
    }, []);
```

### 終わりに
最後にこちらを実装するのに参考にしたサイトを添付します。
もしうまくいかなかった場合は以下を参考にしてください。
https://qiita.com/Nkzn/items/3a09c08286bd7d4bf88b
https://github.com/jhannes/react-native-oauth-demo/blob/master/README.md
https://reactnative.dev/docs/linking?syntax=ios&language=typescript

ここまで読んでいただきありがとうございます。
React NativeでOAuth2.0認証を実装する方法は調べても意外と出てこなくて思考錯誤を重ねました。
こちらの記事がお役に立っていただければ幸いです。
それでは、また別の機会があればお会いしましょう。