Cognitoを使ってブラウザアプリからAWS IoTへのPub/Sub権限を制御する
はじめに
サーバーレス開発部@大阪の岩田です。 前回のブログでAWS IoTのカスタムオーソライザーを用いた、ブラウザアプリの権限管理に挑戦して撃沈しました。
今回はベタにCognitoと連携した権限管理に挑戦したいと思います。
やりたい事
今回の目標は下記のようなシナリオを実現する事です。
- Cognitoを利用してブラウザアプリにログインする
- ブラウザアプリにログインしている場合、対象ユーザーが所有するデバイスに関連するトピックにPublish・Subscribeできるようにする
- ブラウザアプリにログインしていない場合や、対象ユーザーが所有するデバイスに関連しないトピックへのPublish・Subscribeは拒否する
必要な設定
今回のシナリオを実現するに当たって、下記の設定が必要になります。
- Cognito認証済みロールにAWS IoTの操作を許可するポリシーをアタッチ
- CognitoアイデンティティIDに対して、AWS IoTの適切なポリシーをアタッチ
- AWS IoTのモノにCognitoアイデンティティIDをアタッチ
それでは、1つづつ順を追って環境を構築していきます!!
必要なAWSリソースの作成
まず下記のCFnテンプレートから必要なAWSリソースを作成します。
AWSTemplateFormatVersion: '2010-09-09' Description: Create Cognito User Pool Resources: CognitoUserPoolMyUserPool: Type: "AWS::Cognito::UserPool" Properties: AdminCreateUserConfig: AllowAdminCreateUserOnly: false UnusedAccountValidityDays: 7 AutoVerifiedAttributes: - email Policies: PasswordPolicy: MinimumLength: 8 RequireLowercase: false RequireNumbers: true RequireSymbols: false RequireUppercase: false Schema: - AttributeDataType: "String" DeveloperOnlyAttribute: false Mutable: true Name: "email" StringAttributeConstraints: MaxLength: "2048" MinLength: "0" Required: true - AttributeDataType: "String" DeveloperOnlyAttribute: false Mutable: true UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: ClientName: Fn::Join: - "" - - Ref: AWS::StackName - UserPoolClient GenerateSecret: false RefreshTokenValidity: 7 UserPoolId: Ref: CognitoUserPoolMyUserPool MyIdentifyPool: Type: "AWS::Cognito::IdentityPool" Properties: IdentityPoolName: "MyIdentifyPool" AllowUnauthenticatedIdentities: true CognitoIdentityProviders: - ClientId: Ref: UserPoolClient ProviderName: Fn::Join: - "" - - "cognito-idp." - Ref: "AWS::Region" - ".amazonaws.com/" - Ref: CognitoUserPoolMyUserPool ServerSideTokenCheck: false CognitoUnAuthRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Federated: - "cognito-identity.amazonaws.com" Action: - "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref MyIdentifyPool ForAnyValue:StringLike: "cognito-identity.amazonaws.com:amr": "unauthenticated" CognitoAuthRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - "arn:aws:iam::aws:policy/AWSIoTFullAccess" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Federated: - "cognito-identity.amazonaws.com" Action: - "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref MyIdentifyPool ForAnyValue:StringLike: "cognito-identity.amazonaws.com:amr": authenticated CognitoRoleMapping: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref MyIdentifyPool Roles: authenticated: !GetAtt CognitoAuthRole.Arn unauthenticated: !GetAtt CognitoUnAuthRole.Arn IoTPolicy: Type: AWS::IoT::Policy Properties: PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "iot:*" Resource: - "*" IoTThing1: Type: AWS::IoT::Thing Properties: AttributePayload: Attributes: myAttributeA: "MyAttributeValueA" myAttributeB: "MyAttributeValueB" myAttributeC: "MyAttributeValueC" Outputs: UserPoolId: Description: "User Poll ID" Value: Ref: CognitoUserPoolMyUserPool UserPoolClient: Description: "User Pool Client ID" Value: Ref: UserPoolClient MyIdentifyPool: Description: "Identify Pool ID" Value: Ref: MyIdentifyPool IoTPolicy: Description: "AWS IoT Policy" Value: Ref: IoTPolicy IoTThing1: Description: "AWS IoT Thing1" Value: Ref: IoTThing1
必要なCognitoユーザープール、 フェデレーティッドアイデンティティ、AWS IoTのモノやポリシーが作成されます。
ポイントとして
CognitoAuthRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - "arn:aws:iam::aws:policy/AWSIoTFullAccess"
の部分で、Cognito認証済みロールに対してAWS IoTへのフルアクセス権を付与しています。
動作確認用Reactアプリの作成
動作確認にはReactとAWS Amplifyを使って作成した簡易なアプリを使います。 AWS IoTでCognito認証を使う場合はHTTPリクエストにSigv4の署名を付ける必要があるのですが、AWS Amplifyを使えば勝手にやってくれるのでラクチンです。
バージョン等
検証に用いた環境の各種バージョンは下記の通りです。
- Node.js:v8.10.0
- aws-amplify:1.0.11
- aws-amplify-react:2.0.1
- react:16.5.0
- react-dom:16.5.0
- react-scripts:1.1.5
Reactアプリの作成
まずcreate-react-app
でアプリのひな形を作成し、npm install --save aws-amplify aws-amplify-react
で追加のライブラリを導入します。
ライブラリが導入できたら、App.jsを下記のように修正します。 ハイライト箇所はCloudFormationの出力を参考に、自分の環境に合わせて修正して下さい。
import React, { Component } from 'react'; import './App.css'; import { withAuthenticator } from 'aws-amplify-react'; import Amplify, { PubSub } from 'aws-amplify'; import { AWSIoTProvider } from '@aws-amplify/pubsub'; Amplify.configure({ Auth: { identityPoolId: 'us-east-1:xxxxxxxxxx', region: 'us-east-1', userPoolId: 'us-east-1_xxxxxxxxxx', userPoolWebClientId: 'xxxxxxxxxx', } }); Amplify.addPluggable(new AWSIoTProvider({ aws_pubsub_region: 'us-east-1', aws_pubsub_endpoint: 'wss://xxxxxxxxxx.iot.us-east-1.amazonaws.com/mqtt', clientId: 'xxxxxxxxxxxxxxxxxxxx' //AWS IoTに作成したモノの名前 })) class App extends Component { constructor(props) { super(props); this.state = { messages: [], topic: '' } this.handleSubscribe = this.handleSubscribe.bind(this) this.handlePublish = this.handlePublish.bind(this) this.handleChangeTopic = this.handleChangeTopic.bind(this) this.handleReceive = this.handleReceive.bind(this) } render() { return ( <div className="App"> <label>TOPIC: <input type="text" onChange={this.handleChangeTopic}></input> </label><br/> <button onClick={this.handlePublish}>publish</button> <button onClick={this.handleSubscribe}>subscribe</button> <MessageList messages={this.state.messages} /> </div> ) } handlePublish(){ PubSub.publish(this.state.topic, 'pub message from browser!!') } handleSubscribe(topic){ PubSub.subscribe(this.state.topic).subscribe({ next: this.handleReceive, error: error => console.error(error), close: () => console.log('Done'), }) } handleChangeTopic(event) { this.setState({topic: event.target.value}) } handleReceive(msg){ const messages = this.state.messages messages.push(JSON.stringify(msg.value)) this.setState({messages: messages}) } } class MessageList extends React.Component { render() { let id = 1 return ( <ul> {this.props.messages.map(message => { id += 1 return ( <li key={id}> <div> {message} </div> </li> ) })} </ul> ) } } export default withAuthenticator(App)
動作確認
準備ができたので、実際に動作確認していきます。
npm start
でアプリを起動し、サインアップ・サインイン後に、適当なトピックを入力してpublishしてみます。
コンソールにエラーメッセージが表示され失敗しました。 Cognito認証済みロールへの権限付与に加えて、サインアップ時に作成されたCognitoアイデンティティIDに対して権限設定が必要なためです。
CognitoアイデンティティIDに対して、AWS IoTの適切なポリシーをアタッチ
2018年9月現在、マネジメントコンソールからの操作がサポートされていないので、AWS CLIを利用して下記のコマンドでポリシーをアタッチします。
aws iot attach-policy --policy-name <作成したポリシー名> --target <作成されたCognitoアイデンティティID>
CognitoアイデンティティIDはマネジメントコンソールのIDブラウザ等から確認可能です。
なお、アタッチ作業自体はマネジメントコンソールから行えませんが、CLIからアタッチした後はマネジメントコンソールから参照できるようになります。
ポリシーアタッチ後に改めてブラウザアプリから動作確認します。
成功です!!
Pub・Sub可能なトピックを対象ユーザーが所有するデバイスに関連するトピックに制限
このままだと、何でもかんでもPub・Subできてしまうので、権限を絞っていきます。
作成したAWS IoTのポリシーを下記のように修正します。
{ "Version": "2012-10-17", "Statement": [ { "Condition": { "Bool": { "iot:Connection.Thing.IsAttached": [ "true" ] } }, "Action": [ "iot:Connect" ], "Resource": [ "*" ], "Effect": "Allow" }, { "Action": [ "iot:Publish", "iot:Receive" ], "Resource": [ "arn:aws:iot:us-east-1:xxxxxxxxxx:topic/${iot:Connection.Thing.ThingName}/*" ], "Effect": "Allow" }, { "Action": [ "iot:Subscribe" ], "Resource": [ "arn:aws:iot:us-east-1:xxxxxxxxxx:topicfilter/${iot:Connection.Thing.ThingName}/*" ], "Effect": "Allow" } ] }
ポイントとしてポリシー変数iot:Connection.Thing.IsAttached
とiot:Connection.Thing.ThingName
を使用して制御を行なっています。
それぞれのポリシー変数の説明をAWSのドキュメントから引用します。
iot:Connection.Thing.IsAttached
ポリシーが評価されているモノに、証明書または Amazon Cognito ID がアタッチされている場合は、true に解決されます。
このポリシー変数を使う事で、サインインしていないユーザーの接続を拒否します。
iot:Connection.Thing.ThingName
これは、ポリシーが評価されているモノの名前に解決されます。モノ名は、MQTT/WebSocket 接続のクライアント ID に設定されます。このポリシー変数は、MQTT または MQTT over WebSocket プロトコルに接続するときにのみ使用できます。
モノのポリシー変数
Cognito認証を使ってAWS IoTに接続した場合は、${iot:Connection.Thing.ThingName}
の部分がMQTTのクライアントIDに置きかわります。
このポリシー変数を使う事で、<対象ユーザーが所有するモノの名前>/
から始まるトピックにのみPub・Subを許可するように制御します。
動作確認
トピック名の指定を<作成したAWS IoTのモノの名前>/
に変更し、改めて動作確認してみます。
失敗しました。 今度はCognitoアイデンティティIDとAWS IoTのモノを紐付けてやる必要があります。
AWS IoTのモノにCognitoアイデンティティIDをアタッチ
AWS CLIで下記のコマンドでポリシーをアタッチします。
aws iot attach-thing-principal --thing-name <ユーザーに紐付けるモノの名前> --principal <対象ユーザーのCognitoアイデンティティID>
これでAWS IoTのモノとCognitoアイデンティティIDが紐付きました。
MQTTのクライアントIDはブラウザ側で自由に設定可能な項目ですが、こうやってCognitoアイデンティティIDと紐付けておく事で、セキュリティが担保されます。
改めて動作確認してみます。
成功です! 念のためマネジメントコンソールの方で同じトピックをSubscribeしつつ、ブラウザからパブリッシュしてみます
マネジメントコンソール側でもブラウザから正常にPublish出来ている事が確認できます。
最後に、別のユーザーでサインインし直して、同じトピックに対してPub・Subしてみます。
想定通り失敗しました!! これで設定完了です。
まとめ
Cognitoを利用したAWS IoTへの権限管理について調べてみました。
- ポリシーをアタッチすべき箇所が多いことと
- ポリシー変数の挙動を理解していないと思うような権限制御が行えない
というところに苦戦しました。
実案件に利用する際は、CognitoアイデンティティIDに対するAWS IoTのポリシーのアタッチと、AWS IoTのモノに対するCognitoアイデンティティIDのアタッチをどのように実現するか、業務フローの検討や周辺アプリの作り込みが重要になりそうです。
誰かの参考になれば幸いです。