Discord Activity（Embeded Apps）でSupabaseを使う

# はじめに
ハッカソンにでた時、DiscordActivity上からsupabaseを使ったのだが、ちょっと工夫が必要だったので備忘録

ちなみに作ったものはこれ
https://topaz.dev/projects/cb973dc2c7328144e63f

# 前提・要件
- discord activtyを開いている人のDiscord JWTは取れる
- supabase側へのログインはDisocrd Oauthを使用している
- SupabaseのClient SDKを使いたい
- DBが複数あり、一部はrealtimeDBなのでWebsocketが使えると良い

# とりあえずやってみた
Discord Activity上からSupabaseのRealtimeDBを実装してみた。

DiscordにはCSPのセキュリティーがあるので、proxyで指定したドメイン以外に接続できない。（[参照](https://discord.com/developers/docs/activities/development-guides/local-development#url-mapping)）それだけなら、/.proxy/supabaseに対して、プロジェクトURLを渡せば解決しそうな気がしていた。


# 課題
問題になったのは、Supabaseのログイン。
Supabaseのログインは全てOauth2.0準拠なので、ログイン時に必ずリダイレクトが必要になる。

これが問題で、先述した通り、Discord Activityからは事前に登録されたドメインに対してproxyを通して通信をする必要がある。これのせいで、まずログインページに飛べないし、飛べたとしても帰って来られない。

### どうやってJWTを入手するねん！！！！！（嘆き 

# 嘆き中の考え

### 1. ユーザー情報から、supabaseへログイン
Discord Activityには専用SDKがあって、Activityに参加している人の情報は取れる。Discordから発行されるJWTもあるので、限られたscopeの中であればdiscordにリクエストを投げることも可能。

フローはこんな感じ。
![09d4a8103d122f2faf5242cdfcf09210.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819887/ca5b3072-001c-4891-bbbd-562b5cbdafff.png)

このJWTをそのままSupabase Clientにぶっ込んで使えないかな。と思ったり。
これよく考えればわかるんですが、supabaseにdiscordログインするときはDiscordからのJWTを受け取って、supabaseのJWTに変換しているから、無理なんですよねぇ(ISSも違うし。

### 2. メールとパスワード認証
調べた感じ、メールとパスワード認証であればリダイレクトは発生しなかった。
なので、DiscordSDKから入手できるメールアドレスと、JWTの中身を組み合わせてsha265にしたものをパスワードにして、自動ログインにする方法も考えた。

でもこれ、JWTの何を組み合わせてsha265にしているかバレると他の人のアカウントにログインし放題になっちゃうんですよ。（セキュリティの敗北

### 3. ユーザー情報からSupabaseのユーザー情報と比較する
ちなみに、専用SDKからユーザーIdも取ることができます。

前提の通り、supabaseへのログインはDiscordのOauthを使っているのですが、Disocrd側から帰ってきたJWTの内容がsupabaseのどこかに保存されていてもおかしくないなぁとずっと思っていて、discordによって保証されたJWTの中のsubが同じであれば、安全にJWTを発行するAPI立てられそうだなぁと思い。探していました。

![スクリーンショット 2025-05-19 21.59.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819887/d4c07578-ece6-44bf-9dec-72e6808eb778.png)
おっ？！なんかあるやん。

想像通り、JWTの内容が保存されてて、supabaseのauthのusersのraw_user_meta_dataの中にsubが入ってた。

### 勝ち確！

## 解決策・実装
3のアイディアのおかげで、アカウントの所有確認とsupabase側のアカウントを安全に特定できるようになった。

あとは、特定したsupabase側のidと有効期限を含めたJWTを作り、JWT Secretで署名できるAPIサーバーをつくればよさそう。


### サーバー側

1: 送られてくるDiscordのJWTからユーザーIdを入手
```ts
const response = await fetch("https://discord.com/api/users/@me", {
      method: "GET",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": `Bearer ${body.access_token}`,
      }
    });
```

2: 全ユーザーのuser_metadataの中から当該ユーザーのSupabaseIDを見つける
```ts
const { data: user } = await supabase.auth.admin.listUsers();
const filtered = user.users.filter(user =>{
  if (user.user_metadata) {
    if (user.user_metadata.iss == "https://discord.com/api" && user.user_metadata.sub == data.id) {
      return user;
    }
  }
});
```

3: JWT作って、JWTSecretで署名

```ts
const payload = {
  sub: filtered[0].id,
  aud: 'authenticated',
  role: 'authenticated',
  iss: 'supabase',
  iat: now,
  exp: now + 60 * 60 * 24 * 7, // 1週間
};

return c.json({ access_token: jwt.sign(payload, process.env.JWT_SECRET!, { algorithm: 'HS256' }), refresh_token: crypto.randomBytes(64).toString('base64url') });  
```

### Discord側の設定
こうやって設定しておきます。

![スクリーンショット 2025-05-19 22.19.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2819887/3780d48f-cc94-4559-9f0d-690227ce3b28.png)

### クライアント側
```ts
const client = createClient<Database>("https://<ApplicationID>.discordsays.com/.proxy/supabase", "SUPABASE_ANON_KEY");

const supabaseTokenResponse = await fetch("/.proxy/api/supabase/token", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    access_token: "<DISCORD_JWT>",
  }),
});

const { access_token, refresh_token } = await supabaseTokenResponse.json();

await supabase.auth.setSession({
  access_token,
  refresh_token,
});
```

## 終わりに
とりあえず、実装見ればどうにかなります。
https://github.com/progate-hackathon-enpower/discord-activity