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