SuperTokensの紹介

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/d4a43c6e-a1ac-14e3-84ec-26113191cc89.png)

# はじめに

[NRI OpenStandia Advent Calendar 2022](https://qiita.com/advent-calendar/2022/nri-openstandia) の18日目は、認証の仕組みを提供するオープンソースである**SuperTokens**の概要から簡単な実装までを説明します。

本記事を読んでより詳しくSuperTokensの使い方を知りたいと思った人は、公式ドキュメントが充実しているので、そちらを見ることをお勧めします。

https://supertokens.com/

# 1 SuperTokensの概要
## 1.1 SuperTokensとは
SuperTokensとは「**オープンソースの認証プラットフォーム**」です。


SuperTokensの特徴は、**簡単に**認証機能を実現することです。
認証機能の実現にはアプリのUX、開発、セキュリティといった様々な知識や技術を要するため、認証機能を作り上げるためには多くの時間がかかります。
SuperTokensは安全なログインとセッション管理を行う機能をSDKで提供します。
そのため、開発者の負荷が少なくユーザに安全な認証を提供できます。

## 1.2 SuperTokensの仕組み
### SuperTokensのコンポーネント
![画像2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/519e897f-6d5e-569c-e163-a342e082da13.png)

SuperTokensには3つのコンポーネントがあります。

| コンポーネント |　説明 |
|:-|:-|
| フロントエンドSDK | ログインUIのレンダリングとセッショントークンの自動管理を担当する。 |
| バックエンドSDK | サインアップ、サインイン、サインアウト、セッションの更新などのAPIを提供する。<br\>フロントエンドはこれらのAPIと通信する。<br\>これらの API は、アプリケーションの API と同じドメインで公開される。 |
| SuperToken Core | 認証のコアロジックを含むHTTPサービス。<br>DBとのインターフェイスを担当し、バックエンドSDKによってDBを必要とする操作についてクエリされる。 |

### ログインの仕組み
メールパスワードログインを例にログインの仕組みを説明します。
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/509e04c4-819a-87e3-9082-3c3fe89eb76c.png)


1. ログインするために、フロントエンドSDKがユーザの資格情報と共にバックエンドSDKのAPI`/auth/signin`を呼び出す。
1. バックエンドSDKはリクエストの検証を行い、資格情報を使用してSuperTokens Coreの`/recipe/signin`を呼び出す。
1. SuperTokens Coreはパスワードを検証し、ステータスをバックエンドSDKに返す。
1. 検証失敗のステータスがバックエンドSDKに帰ってきた場合、バックエンドSDKはそれをフロントエンドSDKに送信し、フロントエンドSDKはユーザにエラーメッセージを表示する。
1. 検証成功のステータスがバックエンドSDKに帰ってきた場合、バックエンドSDKはセッションレシピ（※1）を使用して新しいセッショントークン（アクセストークン + リフレッシュトークン）を作成するようにSuperTokens Coreの`/recipe/session`を呼び出す。
1. SuperTokens Coreがセッショントークンを作成してバックエンドSDKに返した後、バックエンドSDKはセッショントークンをCookieにアタッチし、フロントエンドSDKに送信する。
1. ユーザログイン完了。

※1 SupenrTokensでは、ログインに使用されるさまざまな処理をそれぞれ「レシピ」と呼びます。概要は「[2.1 SuperTokensが提供するログイン方法](#21-supertokensが提供するログイン方法)」で説明しています。

## 1.3 SuperTokensの長所と短所

- 長所
    - オープンソース（ユーザ数に制限なく永久に無料）
    - カスタマイズ性が高い
- 短所
    - IdPではない

:::note warn
SuperTokensはSaaSとしても提供しており、その場合は有料になります。
:::

# 2 SuperTokensのサンプルアプリを動かす

## 2.1 SuperTokensが提供するログイン方法
SuperTokensでは、各ログイン方法を「レシピ」と表現しています。
レシピは公式ドキュメントの[User Guides](https://supertokens.com/docs/guides)にまとめられています。
レシピは以下のものがあります。
- パスワードレス
- ソーシャルログイン
- メールパスワードログイン
- 電話パスワードログイン
- ユーザ管理ダッシュボード
- セッション管理
- ユーザロール

これらのレシピは「メールパスワード＋ソーシャルログイン」のように組み合わせて使用することもできます。

## 2.2 サポートされているフレームワーク

### フロントエンド
React、Next.js、Angular、Vue.js
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/d5fc9879-8078-1887-53f4-3cb31d58321c.png)

### バックエンド
Node.js、Nest.js、Python、GoLang
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/0acff27a-9480-4464-8af5-da9029e79f6e.png)

## 2.3 メールパスワードログイン
レシピの中から、一般的なログイン方法であるメールパスワードログインを実装してみます。

### 実装環境
- WSL2
- Windows10
- npm：9.1.3
- node：16.17.1

### フロントエンドとバックエンドの構築

ターミナルで次のコマンドを実行します。
```bash
$ npx create-supertokens-app@latest --recipe=emailpassword
```
今回はアプリ名を`my-app`、フロントエンドにReact、バックエンドにNode.jsを指定します。

```
$ npx create-supertokens-app@latest --recipe=emailpassword


 _____                     _____     _
/  ___|                   |_   _|   | |
\ `--. _   _ _ __   ___ _ __| | ___ | | _____ _ __  ___
 `--. \ | | | '_ \ / _ \ '__| |/ _ \| |/ / _ \ '_ \/ __|
/\__/ / |_| | |_) |  __/ |  | | (_) |   <  __/ | | \__ \
\____/ \__,_| .__/ \___|_|  \_/\___/|_|\_\___|_| |_|___/
            | |
            |_|


create-supertokens-app (v0.0.20) lets you quickly get started with using SuperTokens!

Choose your tech stack and the authentication method, we will create a working project that uses SuperTokens for you.


? What is your app called? my-app
? Choose a frontend framework (Visit our documentation for integration with other frameworks): React
? Choose a backend framework (Visit our documentation for integration with other frameworks): Node.js

✅ Finished setting up folder structure!

✅ Setup complete!


To start the application run the following command:

cd my-app
npm run start
```

これで、フロントエンドとバックエンドの構築は終了です。
今回は簡単に構築するためにSDKを使用しましたが、手動でセットアップすることも可能です。

実際に起動してみましょう。
```bash
$ cd my-app
$ npm start
```
ブラウザを開いて`http://localhost:3000/auth`にアクセスするとサインイン画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/c0ef5791-175b-e91b-b52f-b25d4fbf07e5.png)

### SuperTokens Coreの構築
今回はdockerイメージを使用して構築します。

ターミナルで次のコマンドを実行します。
今回は外部DBを使用せず、SuperTokens内部のDBを使用します。
DBの種類はmySQLを指定します。
```bash
docker run -p 3567:3567 -d registry.supertokens.io/supertokens/supertokens-mysql:4.2
```

ブラウザを開いて`http://localhost:3567/hello`にアクセスすると`Hello`と表示されているので、コンテナが正常に起動したことを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/483feb3d-7169-c33f-5b10-797dc603b140.png)

### バックエンドSDKとSuperTokens Coreの接続
バックエンドの`backend/config.ts`にSuperTokens Coreの情報を追加します。
```diff_javascript
export const SuperTokensConfig: TypeInput = {
    supertokens: {
        // this is the location of the SuperTokens core.
-        connectionURI: "https://try.supertokens.com",
+        connectionURI: "http://localhost:3567",
+        apiKey: "someKey" // OR can be undefined
    },
    appInfo: {
        appName: "SuperTokens Demo App",
        apiDomain: "http://localhost:3001",
        websiteDomain: "http://localhost:3000",
    },
    // recipeList contains all the modules that you want to
    // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides
    recipeList: [
        EmailPassword.init(),
        Session.init(),
        Dashboard.init({
            apiKey: "supertokens_is_awesome",
        }),
    ],
};
```
`supertokens: {...}`の中にある`connectionURI`はSuperTokens CoreのURIです。
`apiKey`はAPIキーを表しますが、デフォルトではAPIキーはないためとりあえず適当な文字列を置きます。
APIキーの追加方法は公式ドキュメント「[Adding API Keys](https://supertokens.com/docs/emailpassword/common-customizations/core/api-keys)」を参考にしてください。

`appInfo: {...}`はアプリの基本情報を表します。
アプリの情報をまとめると次のようになります。
| 名前 | 値 |
|:-|:-|
| アプリの名前 | my-app |
| APIドメイン | http://localhost:3001 |
| APIベースパス | /auth（デフォルト） |
| ウェブサイトのドメイン | http://localhost:3000 |
| ウェブサイトのベースパス | /auth（デフォルト） |

### ユーザ管理ダッシュボードの設定と表示
ユーザ管理ダッシュボードは、SuperTokens上のユーザリストを表示し、セッション、メタデータ、ロール、アカウント情報を簡単に表示、変更、削除することができます。
バックエンドの`backend/config.ts`に一意のAPIキーを指定します。
```diff_javascript
export const SuperTokensConfig: TypeInput = {
    supertokens: {
        // this is the location of the SuperTokens core.
        connectionURI: "http://localhost:3567",
        apiKey: "someKey" // OR can be undefined
    },
    appInfo: {
        appName: "SuperTokens Demo App",
        apiDomain: "http://localhost:3001",
        websiteDomain: "http://localhost:3000",
    },
    // recipeList contains all the modules that you want to
    // use from SuperTokens. See the full list here: https://supertokens.com/docs/guides
    recipeList: [
        EmailPassword.init(),
        Session.init(),
        Dashboard.init({
-            apiKey: "supertokens_is_awesome",
+            apiKey: "my_api_key",
        }),
    ],
};
```
`recipeList: {...}`はSuperTokensのレシピの追加や設定を行います。

ブラウザで`http://localhost:3001/auth/dashboard`にアクセスするとAPIキーの入力画面が表示されます。
先ほど指定したAPIキーを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/8951432a-2cab-ec66-316a-3de2f2253162.png)

認証に成功すると、ダッシュボードが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/192462d7-cbba-e285-8d5a-eaf6341f2b88.png)

次に、フロントエンドアプリに移動し、サインアップフローを介してユーザを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/6bda441d-cc21-d0da-1dd9-503f59ec7395.png)

ダッシュボードに戻り、ページを更新するとユーザが表示されます。
ダッシュボードには標準で、ユーザ情報確認・修正機能、パスワード変更機能、メールアドレス修正機能、ユーザ削除機能が備わっています。
![6198d-ityar.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/67a61986-abea-149a-edf0-a4491c2dc16e.gif)

# 3 SuperTokensのサンプルアプリをカスタマイズする
SuperTokensの公式ドキュメントでは、セッション管理や認証フローに関する様々なカスタマイズ方法が記載されています。
今回はそれらを参考に、「[2 SuperTokensのサンプルアプリを動かす](#2-supertokensのサンプルアプリを動かす)」で作成したアプリをカスタマイズしてみます。

## 3.1 サインアップフォームフィールドの追加
公式ドキュメント「[Adding Extra Fields](https://supertokens.com/docs/emailpassword/common-customizations/signup-form/adding-fields)」と「[Storing user metadata](https://supertokens.com/docs/emailpassword/common-customizations/usermetadata/about)」を参考に、サインアップフォームフィールドの追加をします。
今回は「First name」と「Last name」をフォームに追加します。

### フロントエンド
フロントエンドの`frontend/config.tsx`にフォームフィールドの情報を追加します。
```diff_javascript
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
import Session from "supertokens-auth-react/recipe/session";

export const SuperTokensConfig = {
    enableDebugLogs: true,
    appInfo: {
        appName: "SuperTokens Demo App",
        apiDomain: "http://localhost:3001",
        websiteDomain: "http://localhost:3000",
    },
    recipeList: [
+        EmailPassword.init({
+            signInAndUpFeature: {
+                signUpForm: {
+                    formFields: [{
+                        id: "first_name",
+                        label: "First name",
+                        placeholder: "First name"
+                    }, {
+                        id: "last_name",
+                        label: "Last name",
+                        placeholder: "Last name"
+                    }]
+                }
+            }
+        }), 
        Session.init()],
};

```

### バックエンド
バックエンドの`backend/config.tsx`にフォームフィールドの情報を追加します。
また、サインアップが成功した時にフォームフィールドを処理するために`signUpPOST`APIを`ovverride`します。

```diff_javascript
export const SuperTokensConfig: TypeInput = {
    supertokens: {...},
    appInfo: {...},
    recipeList: [
+        EmailPassword.init({
+            signUpFeature: {
+                formFields: [{
+                    id: "first_name"
+                  }, {
+                    id: "last_name"
+                  }]
+            },
            
+            override: {
+                apis: (originalImplementation) => {
+                    return {
+                        ...originalImplementation,
+                        signUpPOST: async function (input) {
+
+                            if (originalImplementation.signUpPOST === undefined) {
+                                throw Error("Should never come here");
+                            }
+
+                            // First we call the original implementation of signUpPOST.
+                            let response = await originalImplementation.signUpPOST(input);
+
+                            // Post sign up response, we check if it was successful
+                            if (response.status === "OK") {
+
+                                // These are the input form fields values that the user +used while signing up
+                                let formFields = input.formFields;
+
+                            }
+                            return response;
+                        }
+                    }
+                }
+            }
+          }),
        Session.init(),
        Dashboard.init({
            apiKey: "my_api_key",
        }),
    ],
};

```

ただし、このままではSuperTokensは追加したフォームフィールドを保存しません。
ユーザのサインアップ後に、各ユーザに関するデータを保存するために、ユーザメタデータの保存を行います。

```diff_javascript
+ import UserMetadata from "supertokens-node/recipe/usermetadata";

export const SuperTokensConfig: TypeInput = {
    supertokens: {...},
    appInfo: {...},
    recipeList: [
+        UserMetadata.init(),
        EmailPassword.init({
            signUpFeature: {
                formFields: [{
                    id: "first_name"
                  }, {
                    id: "last_name"
                  }]
            },
            override: {
                apis: (originalImplementation) => {
                    return {
                        ...originalImplementation,
                        signUpPOST: async function (input) {

                            if (originalImplementation.signUpPOST === undefined) {
                                throw Error("Should never come here");
                            }

                            // First we call the original implementation of signUpPOST.
                            let response = await originalImplementation.signUpPOST(input);

                            // Post sign up response, we check if it was successful
                            if (response.status === "OK") {

                                // These are the input form fields values that the user used while signing up
                                let formFields = input.formFields;

+                                for (const formField of formFields) {
+                                    if (formField.id === "first_name") await UserMetadata.updateUserMetadata(response.user.id, { first_name: formField.value });
+                                    if (formField.id === "last_name") await UserMetadata.updateUserMetadata(response.user.id, { last_name: formField.value });
+                                }

                            }
                            return response;
                        }
                    }
                }
            }
          }),
        Session.init(),
        Dashboard.init({
            apiKey: "my_api_key",
        }),
    ],
};

```

### 実行結果
サインアップ時に、「First name」と「Last name」を記入します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/e5555755-1443-04f0-45e8-a4eb7a762bcd.png)

サインアップ後ユーザのメタデータを確認すると、「First name」と「Last name」のidである`first_name`と`lasta_name`が追加されているのが確認できます。
また、`first_name`や`lasta_name`のように、ダッシュボードの「USER INFORMATION」に表示するために事前に準備されてある固定IDがあり、それらのIDにメタデータを保存すると「USER INFORMATION」に反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/7fac1858-232f-c524-a9f0-41259e2af15f.png)


# まとめ・所感

本記事では、オープンソースで認証の仕組みを提供するSuperTokensの概要から簡単な実装までを説明しました。
簡単に認証機能を提供することをSuperTokensの目的としているように、とても簡単に作ることができました。
特にフロントエンドとバックエンドがSDKで提供されているので、様々な種類のログイン機能がコマンド一つで出来上がる点がありがたいです。

SuperTokensでは[ロードマップ](https://supertokens.com/product-roadmap)が策定されており、いろんな機能がこれから開発予定です。
Open ID CoonectのIdProviderとして振る舞う機能拡張が予定されているなど、今後の発展に期待します。

# 参考資料

https://supertokens.com/

https://github.com/supertokens/supertokens-core