[Auth0] Auth0とAWSのOIDC連携でセキュアなS3アクセス制御を実現する
こんにちは。
業務効率化ソリューション部の西川です。
Webアプリケーションを開発していると、例えば「ユーザーごとにS3バケットを用意して、そこに直接ファイルをアップロードできるようにしたい」という要件があるかもしれません。
これって、意外と悩ましい問題になりそうですよね。Auth0でユーザー認証はできているけど、そのユーザーにどうやってAWSのリソースにアクセスさせるか...。
今回は、このような課題を解決するために、Auth0とAWSのOIDC連携を使う方法をご紹介します。
特にIAMユーザとの連携でリソースアクセスの権限を制御したい方や外部ID連携を検討されているエンジニアの方に役立つ内容になるんじゃないかと思います。
はじめに
外部ID連携って何がうれしいの?
外部ID連携(OIDC連携)は、認証基盤とクラウドサービスを連携させる仕組みです。
Auth0のような認証基盤を使うことで、認証周りの実装を自前で行う必要がなく、セキュリティリスクも軽減できます。
Auth0で認証したユーザーに対して、AWSのリソース(S3バケットなど)へのアクセス権限を付与する方法はいくつかあります。
一般的なアプローチとしては、バックエンドでPresigned URLを生成する方法がありますが、スケーラビリティの面で課題があることも。
そこで注目したいのが、AWSのOIDC連携機能です。
この機能を使うと、Auth0で認証したユーザーに対して、直接AWSのリソースへのアクセス権限を付与できます。
なぜAuth0とAWSの連携が必要なのか
一般的なシステム要件として、次のようなものがあります。
- 企業ごとに独立したストレージ領域が必要
- ユーザーが直接ファイルをアップロードできるようにしたい
- バックエンドサーバーの負荷を抑えたい
- セキュリティはしっかりと担保したい
バックエンドでPresigned URLを生成する方法には、次のような課題があります。
- バックエンドサーバーの負荷が気になる
- URLの有効期限の管理が面倒
- 大量のリクエストがきたときの対応が不安
また、IAMユーザーの認証情報をフロントエンドに埋め込むというアプローチは、セキュリティ上の重大なリスクがあるため避けるべきだと思います。
やってみた
準備
まずは環境を整えるところから。Auth0とAWSの両方の設定が必要なんですが、思ったより簡単でした。
Auth0のセットアップを以下の手順で行います。
-
Auth0テナントの作成
- Auth0のダッシュボードにアクセス
- 新しいテナントを作成する場合は、右上のテナント切り替えメニューから「Create tenant」を選択
-
アプリケーションの作成
- 左メニューの「Applications」をクリック
- 「Create Application」ボタンをクリック
- アプリケーション名を入力(例:S3 Upload Demo App)
- アプリケーションタイプで「Single Page Application」を選択
- 「Create」をクリック
-
アプリケーションの設定
- 「Settings」タブで以下の項目を設定
- Allowed Callback URLs: http://localhost:3000
- Allowed Logout URLs: http://localhost:3000
- Allowed Web Origins: http://localhost:3000
- Application URIs セクションまで下にスクロールし、設定を保存
- 「Basic Information」セクションのApplication URIs から「Domain」と「Client ID」をメモ
- 「Settings」タブで以下の項目を設定
これらの情報は後ほどAWS側の設定で使用します。
実装
それでは実際の実装に入っていきます。
AWS側の設定
- OpenID Connect プロバイダーの作成
プロバイダーを追加から追加する。
プロバイダのURLと対象者を入力する。
(それぞれ、Auth0のURLとクライアントIDを入力します)
- IAMロールの作成
ウェブアイデンティティを選択し、アイデンティティプロバイダーとAudienceで先ほど追加したプロバイダーを選択する。
今回はS3のFullAccessポリシーをアタッチする。
エンティティが自動的に適用されていることを確認。
クライアントサイドの実装
フロントエンドの実装は、サンプルアプリをもとに実装しました。
-
必要なパッケージをインストール
yarn add @aws-sdk/client-s3 @aws-sdk/credential-providers
-
AWS認証情報の取得とS3アクセス
Auth0のトークンを使ってAWSの認証情報を取得する。// api-server.js const express = require("express"); const cors = require("cors"); const morgan = require("morgan"); const helmet = require("helmet"); const { auth } = require("express-oauth2-jwt-bearer"); const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3"); const { fromWebToken } = require("@aws-sdk/credential-providers"); const authConfig = require("./src/auth_config.json"); const app = express(); const port = process.env.API_PORT || 3001; const appPort = process.env.SERVER_PORT || 3000; const appOrigin = authConfig.appOrigin || `http://localhost:${appPort}`; if ( !authConfig.domain || !authConfig.audience || authConfig.audience === "https://auth0-genai-chat-api" ) { console.log( "Exiting: Please make sure that auth_config.json is in place and populated with valid domain and audience values" ); process.exit(); } app.use(morgan("dev")); app.use(helmet()); app.use(cors({ origin: appOrigin })); const checkJwt = auth({ audience: authConfig.audience, issuerBaseURL: `https://${authConfig.domain}/`, algorithms: ["RS256"], }); app.get("/api/external", checkJwt, (req, res) => { res.send({ msg: "Your access token was successfully validated!", }); }); app.get("/api/buckets", checkJwt, async (req, res) => { try { const token = req.auth.token; const credentials = fromWebToken({ roleArn: 'arn:aws:iam::YOUR_ACCOUNT_ID:role/Auth0S3Role', // AWSで作成したロールのARN, webIdentityToken: token, roleSessionName: 'auth0-session' }); const s3Client = new S3Client({ region: "ap-northeast-1", credentials: await credentials() }); const command = new ListBucketsCommand({}); const response = await s3Client.send(command); res.json(response.Buckets || []); } catch (error) { console.error("Error fetching buckets:", error); res.status(500).json({ error: error.message }); } }); app.listen(port, () => console.log(`API Server listening on port ${port}`));
-
API呼び出し場所の実装
// src/components/S3BucketList.js import React, { useEffect, useState } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; const S3BucketList = () => { const [buckets, setBuckets] = useState([]); const [error, setError] = useState(null); const { getAccessTokenSilently } = useAuth0(); useEffect(() => { const fetchBuckets = async () => { try { const token = await getAccessTokenSilently(); const response = await fetch('http://localhost:3001/api/buckets', { headers: { Authorization: `Bearer ${token}` } }); if (!response.ok) { throw new Error('Failed to fetch buckets'); } const data = await response.json(); console.log('S3 Buckets:', data); setBuckets(data || []); } catch (err) { console.error('Error fetching buckets:', err); setError(err.message); } }; fetchBuckets(); }, [getAccessTokenSilently]); if (error) { return <div>Error: {error}</div>; } return ( <div> <h3>S3 Buckets</h3> {buckets.length === 0 ? ( <p>No buckets found</p> ) : ( <ul> {buckets.map(bucket => ( <li key={bucket.Name}> {bucket.Name} (Created: {new Date(bucket.CreationDate).toLocaleDateString()}) </li> ))} </ul> )} </div> ); }; export default S3BucketList;
動作確認
動作確認をしていきます。
- サーバーの起動
yarn api-server
- アプリの起動
yarn dev
- 画面の確認
バケット名を取得できていることを確認できました。
(バケット名は隠しています)
さいごに
今回のAuth0とAWSのOIDC連携ですが実際にやってみると意外とスムーズに実装できました。
個人的に気に入ったポイントをいくつか紹介すると...
-
セキュリティ面が素晴らしい
- 固定のIAM認証情報を使わなくて済む
- 必要な権限だけを付与できる
- トークンベースで安全
-
開発がスムーズ
- Auth0の使い勝手の良さはそのまま
- フロントエンド、バックエンドの実装がシンプル
また、今回はバケット一覧の取得のみだったため、ユーザーごとに読み取れる、または、更新できるバケットの権限を管理するなどを行うことも可能です。
本記事がAuth0とAWSの外部ID連携を検討中、利用中の方のご参考になっていれば幸いです。