Amplify AuthのUser groupsによるマルチテナントS3アクセス制御
Qiita 「AWS Amplify Advent Calendar 2021」 22日目の記事です。
やりたいこと
- Amplify AuthのUser groupsで、マルチテナント環境でのCognito + S3のアクセス制御をやる
- ユーザーは複数テナントに所属可能で、所属した全てのテナントに対応したS3にアクセスできる
- S3へはフロントエンド(React)から直接アクセスする
結論
Amplifyだけでは、やりたいことの実現方法がわかりませんでした。
IAM Roleが複数付与されないので、複数テナントに所属した場合だと対応した全てのS3にはアクセスできません。
Amplifyのみに頼らずに、AWS SDK(v2)を使ってRoleを切り替えることで、複数テナントのS3へのアクセスに対応しました。(v3でも同様に実装できると思いますが未確認です)
当初想定
ユーザーを複数グループに所属させれば、所属したグループに対応した権限を全て得られる。
Amplify AuthのUser groupsとAmplify Storageを使えば余裕。
・・・と思っていました。
試したこと
- Amazon Cognito userpoolに複数グループを作る
- それぞれのグループに対応したS3へのアクセス権を持つ、別々のIAM Roleを設定する
- 同一ユーザーを複数グループに所属させる
動いてほしかった動作
- フロントエンド(React)からAmplify Authでログインすると複数Roleが適用され、Amplify Storageで所属した全てのグループに対応したS3にアクセスできる
実態
403 Forbidden
なんで?
実際の仕様
-
1つのグループにのみ所属した場合
-
cognito:preferred_role
に所属したグループのロールが設定される - AmplifyのAuth機能を使うと、
cognito:preferred_role
が使用される - つまり所属したグループのS3にアクセスできる
-
-
複数のグループに所属し、優先順位(
precedence
)が同じ場合または優先順位を設定しなかった場合-
cognito:preferred_role
は設定されない(cognito:roles
には所属する複数のロールが設定される) - AmplifyのAuth機能を使うと、
cognito:preferred_role
を見に行くが設定されていないので、どのロールも適用されない - つまり複数グループに所属すると、全てのS3にアクセスできなくなる
-
-
複数のグループに所属し、Cognitoのグループに優先順位(
precedence
)をつけ、最も優先順位が強い(precedence
が一番小さい)グループが1つの場合- 対応するロールが
cognito:preferred_role
に設定される - AmplifyのAuth機能を使うと、
cognito:preferred_role
の1つのみが適用される - つまり複数グループに所属しても、1つのS3しかアクセスできない
- 対応するロールが
つまり、、、
- Amplifyでは同一ユーザーに2つ以上のロールを適用できない!!
グループの存在意義ってなんですか?
先人たちのissue
複数の嘆きの声が上がっているが、基本無視されています。
- https://github.com/aws-amplify/amplify-js/issues/1407
- https://github.com/aws-amplify/amplify-js/issues/2390#issuecomment-624594960
ロールの切替を実装する
ということで、Amplifyでは、複数グループに所属しても、複数のRoleを適用することはできません。
しかし、aws-sdkで頑張ることで、複数のRoleの同時適用はできませんが、状況に応じてRoleを切り替えることができました。
方針
Amazon Cognitoのドキュメントに下記の記載があります。
Use the GetCredentialsForIdentity CustomRoleArn parameter if it is set and it matches a role in the cognito:roles claim. If this parameter doesn't match a role in cognito:roles, deny access.
つまり、GetCredentialsForIdentity
関数を使えば、cognito:roles
内のRoleに切り替えることができます。
先人が残した記録
Cognitoの設定変更
-
GetCredentialsForIdentity
を使用してroleを変更するには、CognitoのIDプールの設定でロールの切り替えを許可する必要がある- ユーザープールではなくIDプールの設定なので注意!!
- idプールの編集 -> 認証プロバイダー -> cognito で下記のように「トークンからロールを選択する」に設定変更する
書いたコード
Amplifyでログイン処理を実施することを前提にしています。
ログイン後にAmplify Authから取得した情報(Credentials)を使いGetCredentialsForIdentity
でロールを切り替え、aws-sdkでS3にアクセスします。
Amplify Storageではなくaws-sdkを使う理由は、ロール切り替え後のCredentialsをAmplifyにセットし直す手段が分からなかった為です。
注意点として、AmplifyやGetCredentialsForIdentity
ではCognitoの認証情報であるAWS.CognitoIdentity.Credentials
が使われますが、aws-sdkではAWSの認証情報であるAWS.Credentials
が必要です。型が違うので直接投げ込むとエラーになるので変換してあげる必要があります。
- Role切り替え用の関数
const switchRoles = async(user, region, identityId, roleArn, cognitoArn) => {
//getCredentialsForIdentityで、切り替え後のロールのアクセスキーを取得
const cognitoIdentity = new AWS.CognitoIdentity({ apiVersion: '2014-06-30', region });
const params = {
IdentityId: identityId,
CustomRoleArn: roleArn,
Logins: {
[cognitoArn]: user
.getSignInUserSession()
.getIdToken()
.getJwtToken(),
},
};
const credentialData = await cognitoIdentity.getCredentialsForIdentity(params).promise()
//AWS-SDKのロールチェンジ
AWS.config.update({
credentials: new AWS.Credentials(
credentialData.Credentials.AccessKeyId,
credentialData.Credentials.SecretKey,
credentialData.Credentials.SessionToken
),
region: region
});
return
}
- ロール切り替えの実行
// Amprifyでログインして必要な情報を取得
const user = await Auth.signIn("****", "****");
const credentials = await Auth.currentUserCredentials();
//指定したいroleのARN
const roleArn = "arn:aws:iam::xxxxxxxxxxxx:role/xxxxxxxxxx"
// aws-sdkのroleを切り替える
await switchRoles(
user, //amplify user
awsconfig.aws_project_region, // region
credentials.identityId, //identityID
roleArn, //適用するroleのarn
`cognito-idp.${awsconfig.aws_project_region}.amazonaws.com/${awsconfig.aws_user_pools_id}` //ユーザープールのID(書き方がarnでないので注意)
);
- S3へアクセス(署名付きURLを取得)
const s3 = new AWS.S3({'region':awsconfig.aws_project_region});
const url = s3.getSignedUrl( 'getObject', {Bucket: 'xxxx', Key: 'xxxxx.png'})
これで、switchロールした権限でS3にアクセスできるようになりました。
ひとまず解決です!!
ですが、、、
Credentialsが自動で更新されない問題
問題点
- 前述の方法では、Credentialsを自力で作っている
- 当然有効期限がある
- 1時間である
- 当然有効期限がある
- 1時間経つとS3にアクセスできなくなるのは、ちょっとしょぼい
- Credentialsを自動でrefreshさせたいよね
ということで、CredentialProviderChainを使って、Credentialsが自動でrefreshされるようにします。
先人が残した記録
こちらの記事を参考にしてますので、内容を把握したい方は参照してください。
2022/03/09追記 : 記事、消えてますね。。。どうしよう。。。貴重な情報だったのに。。。
書いたコード
前提としてAmplify Authによるログイン処理は実装済とします。
-
AWS.Credentials
を継承してロールの切り替えとCredentialsの更新を実装したクラス
import Amplify, { Auth } from 'aws-amplify';
import AWS from 'aws-sdk';
import awsconfig from '@/aws-exports';
Amplify.configure(awsconfig);
// Amplify Auth から AWS.Credentialsを取得するクラス(credentialsのrefreshに対応)
export class AmplifyAuthCredentials extends AWS.Credentials {
roleArn: string = "";
constructor(accessKeyId: string, secretAccessKey: string, sessionToken: string) {
super(accessKeyId, secretAccessKey, sessionToken);
}
//実質こっちがコンストラクタ(await使いたい)
static async init(roleArn: string): Promise<AmplifyAuthCredentials> {
const credentials = await AmplifyAuthCredentials.getSwitchRoleCredentials(roleArn);
if (credentials == null) {
return new AmplifyAuthCredentials('', '', '');
}
//AWS.CognitoIdentity.CredentialsからAWS.Credentialsを作る
const obj = new AmplifyAuthCredentials(
credentials.AccessKeyId,
credentials.SecretKey,
credentials.SessionToken,
);
obj.expireTime = credentials.Expiration;
//obj.expireTime = new Date(); //有効期限を書き換える時に使用(テスト用を想定)
await obj.setRoleArn(roleArn);
return obj
}
//SwitchRole後のクレデンシャル(AWS.CognitoIdentity.Credentials)を取得
static async getSwitchRoleCredentials(roleArn: string): Promise<AWS.CognitoIdentity.Credentials | null> {
try {
// switch role
const cognitoIdentity = new AWS.CognitoIdentity({ apiVersion: '2014-06-30', region: awsconfig.aws_project_region });
const user = await Auth.currentUserPoolUser();
// 該当のroleに切り替える権限がないとき、return null
const payload = user.signInUserSession.idToken.payload;
if (!payload['cognito:roles']?.includes(roleArn)) { return null; }
const currentCredentials = await Auth.currentUserCredentials();
const cognitoArn = `cognito-idp.${awsconfig.aws_project_region}.amazonaws.com/${awsconfig.aws_user_pools_id}`;
const params = {
IdentityId: currentCredentials.identityId,
CustomRoleArn: roleArn,
Logins: {
[cognitoArn]: user
.getSignInUserSession()
.getIdToken()
.getJwtToken(),
},
};
//getCredentialsForIdentityで、切り替え後のロールのCredentialsを取得
const credentials = (await cognitoIdentity.getCredentialsForIdentity(params).promise()).Credentials;
return credentials;
} catch (error) {
return Promise.reject(error);
}
}
// Roleを外部から設定する為の関数
async setRoleArn(roleArn: string): Promise<void> {
this.roleArn = roleArn;
await this.refreshPromise();
}
// AWS.Credentialsから継承(オーバーライド)
refresh(callback) {
this.setCredentials().then(() => callback()).catch(callback);
}
// AWS.Credentialsから継承(オーバーライド)
async refreshPromise() {
return this.setCredentials();
}
// AWS.Credentialsから継承(オーバーライド)
async setCredentials() {
try {
// role切り替え後のCredentialsを取得
const credentials = await AmplifyAuthCredentials.getSwitchRoleCredentials(this.roleArn)
if (credentials == null) { return; }
//アクセスキーをセット
this.accessKeyId = credentials.AccessKeyId;
this.secretAccessKey = credentials.SecretKey;
this.sessionToken = credentials.SessionToken;
this.expired = false;
this.expireTime = credentials.Expiration;
// 有効期限の変更(テスト時の利用を想定)
// var date = new Date();
// date.setSeconds(date.getSeconds() + 30);
// this.expireTime = date;
} catch (error) {
return Promise.reject(error);
}
}
}
- 上記クラスを使って、aws-sdkのロールを切り替える処理
- Amplify Authでログイン後、S3へのアクセス前に実行
// ロールのArn
const roleArn = "arn:aws:iam::xxxxxxxxxxxx:role/xxxxxx"
// aws-sdkのroleを切り替える
const credentials = await AmplifyAuthCredentials.init(roleArn);
AWS.config.update({
credentialProvider: new AWS.CredentialProviderChain([
() => credentials,
]),
region: awsconfig.aws_project_region
});
- S3にアクセス(署名付きURLを取得)する処理
const s3 = new AWS.S3({ region: "ap-northeast-1" })
const url = await s3.getSignedUrlPromise('getObject', { Bucket: 'bucketName', Key: 'fileName.png' })
注意点
S3から署名付きURLを取得する時に、、、
- 同期関数
getSignedUrl
をcallbackなしで使用すると、S3へのアクセスとCredentialsの更新が非同期で動き、Credentialsの更新よりも前にS3にアクセスしようとするので失敗します- callbackありで使用すれば、成功します
- 非同期関数
getSignedUrlPromise
を使用すれば、Credentialsの更新後にS3にアクセスするので、成功します
ですので非同期関数を使ってください。
AWSさん、同期関数のgetSignedUrl
バグってませんか?
最後に
- もっとスマートなやり方を知っている方がいれば、教えて下さい
- 作ってからAWS SDKはv3を使えばよかったと反省したが、もう遅い
その他参考ドキュメント
-
AWSドキュメント : ロールベースのアクセスコントロール
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/role-based-access-control.html -
AWSドキュメント : グループベースのマルチテナンシー
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/group-based-multi-tenancy.html- 何か後ろめたさがあるのでしょうか?とても内容が薄いですね☆
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion