メインコンテンツへスキップ
見出し画像

Slackの“あなた”とGitHubの“あなた”をつなぐ。GitHub OAuth連携で作る紐付け基盤

    こんにちは!Accel-Hack (アクセルハック)開発チームです。

    社内AIアシスタント「Airi」の開発プロセスをお届けする開発記ブログ。

    第22回目の今回は、開発者にとって最も身近な外部サービス――GitHub との OAuth 連携をお届けします。

    これまで freee や Google とつないできた Airi。

    今回つなぐのは GitHub ですが、ゴールは少し違います。

    単に「ログインできるようにする」のではなく、「Slack のあなた」と「GitHub のあなた」を結ぶ対応表を作ること。

    これが、のちのプルリクエスト通知などを支える、地味だけれど欠かせない基盤になります。



    1. SlackとGitHubは、別々の“あなた”

    Airi はふだん Slack の中で動いています。

    一方、開発のやり取りは GitHub で行われます。

    ここで問題になるのが、Airi から見ると「Slack の田中さん」と「GitHub の tanaka」は、まったくの別人に見えるということです。

    たとえば将来、「あなたにレビュー依頼が来ました」と Slack で通知したいとします。

    でも GitHub 側のイベントが教えてくれるのは GitHub のユーザー名だけ。

    それを「Slack の誰に送ればいいか」に変換できなければ、通知は宛先不明で止まってしまいます。

    そこで必要になるのが、両者を結ぶ対応表です。

    今回の GitHub 連携は、この対応表を一度だけ作ってもらうための仕組み、というわけです。


    2. 認可コードフロー、ふたたび

    やり方の骨格は、Vol.11 の freee 連携と同じ 「認可コードフロー」 です。

    GitHub App の認可画面にユーザーを案内し、「許可」してもらって戻ってきた認可コードを、裏側でアクセストークンに交換します。

    入り口は、Vol.12 の App Home に置いた「GitHubと連携する」ボタン。
    押すと /github/auth へ飛び、処理を終えると /github/callback に戻ってきます。

    ルーターは Vol.6 の Hono でシンプルに定義しています。

    export const oauthApp = new Hono<{ Bindings: AppEnv }>();
    oauthApp.get("/auth", handleGithubAuthRedirect); // GitHub へ送り出す
    oauthApp.get("/callback", handleGithubAuthCallback); // 戻ってきたコードを受ける


    3. 「誰が連携しようとしているか」を往復で運ぶ

    ここで一つ、OAuth ならではの工夫が要ります。

    連携の流れは「Airi → GitHub → Airi」と一度外(GitHub)を経由するため、戻ってきたときに「これはどの Slack ユーザーの連携だったか」を見失わないようにしなければなりません。

    Airi では、GitHub へ送り出すときに state という受け渡し用のパラメータに Slack のユーザーIDを載せ、コールバックで取り出して本人を特定しています。

    // 送り出すとき:どの Slack ユーザーの連携かを state に載せる
    const state = btoa(JSON.stringify({ userId }));
    return c.redirect(github.getAuthorizationUrl(state));
    
    // 戻ってきたとき:state から userId を取り出す
    const decoded = JSON.parse(atob(state));
    userId = decoded.userId;

    これで、GitHub を経由して戻ってきても、「誰が連携したのか」を取り違えずに済みます。


    4. 紐付けを保存する:upsert という一手

    本人が特定できたら、いよいよ対応表づくりです。
    交換したアクセストークンで getMe を呼び、GitHub のユーザー名(login)を取得します。
    そして Vol.8 の D1 にある github_links テーブルへ保存します。

    const userData = await github.user.getMe(accessToken);
    const githubUsername = userData.login;
    
    await db
      .insert(githubLinks)
      .values({ userId, githubUsername, createdAt: new Date().toISOString() })
      .onConflictDoUpdate({ target: githubLinks.userId, set: { githubUsername } });

    ここでの主役は onConflictDoUpdate、いわゆる upsert(なければ挿入・あれば更新) です。

    これがあるおかげで、同じ人が連携をやり直しても、行が重複せず常に最新の1行に保たれます。

    「2回押したら2行できてしまった」という事故が起きません。

    この一手で、対応表は常にきれいな状態を保てます。
    一連の流れを図にすると、次のとおりです。

    画像

    5. Homeに「連携済み」を映す

    保存が終わったら、最後に publishHomeView で App Home を描き直します。

    すると Vol.12 で作った状態の出し分けが効いて、「✅ 連携済み (@username)」という表示に切り替わります。

    ユーザーは、自分がちゃんと連携できたのか、誰として紐付いているのかを、Home を開くだけで確認できます。

    「やったはずだけど、できてる?」という不安を残さない。

    地味ですが、連携機能にとって大切な仕上げです。


    6. おわりに

    今回は、GitHub OAuth 連携を題材に、「OAuth のゴールは“ログイン”ではなく“対応表づくり”でもある」という視点をお届けしました。

    state で本人を往復越しに運び、getMe で名前を取り、upsert できれいに紐付ける。

    この github_links という小さな対応表が、この先 Airi が「GitHub の出来事を、Slack の正しい相手に届ける」ための土台になります。

    次回は、ここまで Slack の中で完結してきた Airi に、新しい顔を加えます。

    「Vol.23: 管理機能の拡張『Web UIの実装』」 として、Hono のミドルウェアによるセッション管理と、React/Vite ベースの管理画面を統合する話をお届けします。
    お楽しみに!



    この記事は以下の体制で作成されました。

    • ドラフト作成・執筆: Airi (社内AIアシスタント)

    • アイデア出し・レビュー: CTO

    • デザイン: 事務スタッフ

    Accel-Hackでは、テクノロジーを駆使して世の中の課題を解決していく仲間を募集しています。
    私たちの取り組みに少しでも興味を持っていただけたら、ぜひ採用ページや会社サイトも覗いてみてくださいね。


    あなたへのおすすめ