インターンでAPIの門番を作った話〜JWT, OAuth2.0と過ごした1ヶ月〜 | CyberAgent Developers Blog

Data Access Layer: データベースとのやりとりを担当Domain Layerが中心にあり、他のレイヤーがDomain Layerに依存するアーキテクチャを理解し、GetUserに関するロジックを適切なレイヤーに適切な形でコードを配置しました。この経験を通して、大規模なプロジェクトにおいて、関心の分離が保守性を高めるためにいかに重要なのかを実践的に学ぶことができました。

2. パフォーマンス向上のための並列処理

GetUser APIでは「ユーザ情報」「リソースグループ」「ロール」という3つの異なる情報をデータベースから取得する必要がありました。これらを順番に取得すると、その合計時間がレスポンスタイムになってしまいます。

そこで、Go言語の強力な並行処理機能であるgoroutineを活用し、これら3つのクエリを並列で実行するように実装しました。これにより、APIのレスポンスタイムを改善することができました。初めてgoroutineやchannelを使った本格的な並行処理を実装し、パフォーマンスチューニングの面白さと難しさを実感しました。

認証認可基盤の連携

GetUser APIの実装と並行して、私が取り組んだもう一つの大きなタスクが、このAPIサーバの認証・認可基盤を既存のシステムと連携させることでした。これは、プラットフォーム全体のセキュリティを根幹から支える、非常に責任の重いタスクでした。

開発しているデータプラットフォームは多くの開発者が利用する重要な社内基盤となるため、エンタープライズグレードの堅牢なセキュリティが不可欠でした。具体的には以下の3つの認証認可要件を満たす必要がありました。

  1. ユーザ認証: APIを不正なアクセスから守るため、社内のIDプロバイダが発行したJWT(Json Web Token)を正しく検証し、リクエストしてきたユーザが本人であることを証明する仕組み
  2. サーバ間認証: APIサーバ自身が他の社内サービスと連携する際、自分が何者であるかを安全に証明する仕組み
  3. ユーザ認可: リクエストしてきたユーザがデータベースの特定の操作をする権限があるか、社内の別の権限チェッカーで検証する仕組み

このタスクでは、セキュリティのベストプラクティスを深く学び、Go言語の機能を最大限に活用して、細部にまでこだわった実装を行いました。特に注力した点は以下の2つです。

1.ユーザ認証: 鍵の自動更新とキャッシュによる高速化

はじめに、ユーザからのリクエストを検証する仕組みを構築しました。認証は以下の図のような流れで行われます。

①ユーザはトークンを取得するため、IDプロバイダに自身の情報を渡し、トークンをリクエストします。

②IDプロバイダはユーザ情報を元に、秘密鍵で署名を入れたトークンを生成しユーザに送ります。

③ユーザはAPIサーバにリクエストを送るとき、トークンも一緒に送ります。

④APIサーバはリクエストとトークンを受け取った際、トークンを検証するためIDプロバイダに公開鍵をリクエストします。

⑤IDプロバイダは公開鍵のリストをAPIサーバに送ります。

⑥APIサーバは公開鍵を用いて署名を検証し、成功したら有効なユーザとして認証します。

ここで公開鍵をリクエストする④の操作はリクエストが送られてくるごとに毎回行われるのではなく、キャッシュに保存しておくことで効率化します。しかし、セキュリティを担保するため、ユーザ認証に使う公開鍵はIDプロバイダ側で定期的に更新されます。この鍵の自動更新に追従する必要がありました。

そこで社内IDプロバイダの公開鍵のリスト(JWKS)をバックグラウンドで定期的に取得する仕組みを実装しました。もしリクエストされたJWTの署名検証に使う鍵が見つからない場合でも、その場でJWKSを強制的に一度だけ再取得するなど、可用性を高める工夫も凝らしました。

さらに、パフォーマンス向上のため、一度検証に成功したトークンはその結果をキャッシュに保存する仕組みも導入しました。これにより、API全体のレスポンス向上に大きく貢献しました。

  1. サーバ間認証: OAuth2.0に準拠した安全な連携

次に、サーバー間認証を実現するため、標準規格である「OAuth 2.0 JWT Bearer Grant」フローを実装しました。これは、APIサーバが持つ「サービスアカウントの秘密鍵」で署名したJWTを認証情報として相手のサーバーに提示する方式です。

この実装で最もこだわったのが、秘密鍵のセキュアなメモリ管理です。秘密鍵がメモリ上に平文で長時間残ることは、重大なセキュリティリスクに繋がります。そこで、Goのunsafeパッケージを使った低レベルなメモリ操作を行い、秘密鍵を利用した直後にメモリ上から完全にゼロクリアする処理を実装しました。さらに、もしクリーンアップ処理が呼ばれなくてもGC(ガベージコレクション)のタイミングでメモリが掃除されるようruntime.SetFinalizerを設定するなど、多層防御の考え方を取り入れました。

学び

今回のインターンシップで特に大きな学びとなったのは、GetUser APIと認証・認可基盤という2つの大きな機能を実装するプロセスでした。この経験を通じて、実務における設計思想の重要性と、複雑な仕様を正確に理解するスキルを身につけることができました。

はじめは、2つの機能を実装すべきか、コード全体のどこに手を入れるべきか分からず戸惑いました。しかし、既存コードがドメイン駆動設計に基づいて認証や認可といった責務ごとに明確に分割されていたおかげで、自分が触るべき範囲を特定しやすかったです。Goのinternalパッケージによるアクセス制御も、意図しない依存を防ぐ上で非常に効果的だと実感しました。

また、単にコードを読むだけでなく、「なぜこの認証フローになっているのか」「なぜこのAPIはこの認可チェックが必要なのか」という背景をメンターに質問し、仕様の意図を深く理解することを心がけました。その結果、既存の設計を尊重しつつ、セキュアで保守性の高い形で2つの機能を連携させることができました。技術と仕様の両輪を深く理解することの重要性を、実務を通して学ぶ貴重な経験となりました。

インターン全体を通して

部署の方々とはほぼ毎日ランチをご一緒し、様々なお話を聞くことができました。また、部署外の方々とも積極的に交流する機会もありました。

これらのランチでの交流を通して、配属部署の業務だけでなく、他部署の業務内容や雰囲気、CyberAgent全体に対する理解を深めることができました。

インターン期間の中でAI Meetupというイベントが開催され、他部署の方のお話を聞ける貴重な機会だと考え参加しました。このイベントでは主にAIを活用している部署の方々が業務内容を紹介しており、興味深い技術やプロジェクトを色々と知ることができました。
さらにイベントでは懇親会も設けられており、企業の方だけでなく参加していた他の学生ともお話をする機会がありました。他の学生の研究内容や興味などを話し合うことができてとても刺激的でした。

インターン中にCyberAgentのデータセンターを見学する機会もいただきました。サーバやネットワークがどのように構成されるのか、サーバの管理はどのように行われているのかを実際に見ることができました。データセンターを案内していただいたのはCIUと呼ばれる別の部署の方でしたが、彼らがどういうことを行っているのか、特にプライベートクラウドの整備のお話などはとても興味深かったです。

最後に

トレーナーさん、担当人事さん、DPUの皆様、関わってくださった社員の皆様、本当にありがとうございました!ランチやAIMeetup、Data Center見学など様々な交流の機会を設けていただき、大変充実した一ヶ月間となりました!

特に部署の方々はいつも暖かく接してくださり、たくさんお話をさせていただきました。またメンターさんには、多忙にも関わらず、私がした多くの質問に対し手を止めて丁寧にお答えいただきありがとうございました。

このインターンシップではエンジニアとして成長しただけでなく、多くの知識と貴重な経験を得ることができました。今回得た知恵や経験を糧として、今後も大きく成長できるように努力していきたいと思います。

少しでも私が得た学びやCyberAgentの魅力をお伝えできたら幸いです。最後までお読みいただき、ありがとうございました!




元の記事を確認する

関連記事