AWS IoTとCognitoを組み合わせて２種類の認証をしてみる

# はじめに

今年も始まりました[ACCESSアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/access)。最初の投稿をさせていただきます、@ufoo68です。

今回は、普段あまり使わない[AWS IoT](https://aws.amazon.com/jp/iot/)のことについて書いてみようと思います。

# 今回のネタ

AWS IoTを用いるとデバイスの認証を簡単に実現できますが、例えば**スマホやPCなどのUI画面からデバイスを制御する場合**のそれぞれの認証ってどう実装するのだろう？と思ったので調べてみました。[こういう公式ページ](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/cognito-identities.html)が見つかりましたが具体的な実現方法がわからなかったので、実装してみることにしました。

ちなみに今回も[AWS CDK](https://aws.amazon.com/jp/cdk/)を使ってインフラをコードで管理したいと思います。

# システム構成など

実現にしたいものはこんな感じのものです↓。

![iot cognito.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209689/236a61a5-661e-404f-478b-d7b856b45226.jpeg)


1. メッセージ送信のためのUIアプリがあり、Cognitoで認証されたユーザーのみが送信可能
2. 証明書で認証されたデバイスのみがメッセージの受信可能

# IoTの構築

まずはIoTに関するシステムを構築してみます。事前に[マネジメントコンソール](https://aws.amazon.com/jp/console/)で証明書を発行します。このとき証明書のARNをCDKのコードに直書きはしたくなかったので、[AWS Systems Manager パラメータストア](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)を使ってやり取りを行いました。以下のように[AWS CLI](https://aws.amazon.com/jp/cli/)を実行します。

```
aws ssm put-parameter --name "iot-cert" --type String --value {{arn}}
```

IoT側はこのように実装しました。

```typescript:lib/iot-stack.ts
import * as cdk from '@aws-cdk/core'
import * as iot from '@aws-cdk/aws-iot'
import * as ssm from '@aws-cdk/aws-ssm'

export class IotStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const projectName = 'iot_cognito'
    const policyName = `${projectName}_policy`
    const iotPolicy = new iot.CfnPolicy(this, 'IotPolicy', {
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Effect: 'Allow',
            Action: 'iot:*',
            Resource: '*',
          },
        ],
      },
      policyName,
    })

    const thingName = `${projectName}_thing`
    const iotThing = new iot.CfnThing(this, 'IotThing', { thingName })
    const ioTCertificateArn = ssm.StringParameter.fromStringParameterAttributes(this, 'IotCertArn', {
      parameterName: 'iot-cert-arn',
    }).stringValue

    const iotPolicyPrincipalAttachment = new iot.CfnPolicyPrincipalAttachment(
      this,
      'IotPolicyPrincipalAttachment',
      {
        policyName,
        principal: ioTCertificateArn,
      }
    )
    iotPolicyPrincipalAttachment.addDependsOn(iotPolicy)

    const iotThingPrincipalAttachment = new iot.CfnThingPrincipalAttachment(
      this,
      'IotThingPrincipalAttachment',
      {
        thingName,
        principal: ioTCertificateArn,
      }
    )
    iotThingPrincipalAttachment.addDependsOn(iotThing)
  }
}

```

やっていることは単純で、事前に発行した証明書に**Thing**と**Policy**を紐付けているだけです。

# Cognitoの構築

これが若干面倒でした。

```typescript:lib/cognito-stack.ts
import * as cdk from '@aws-cdk/core'
import * as cognito from '@aws-cdk/aws-cognito'
import * as iam from '@aws-cdk/aws-iam'

export class CognitoStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const userPool = new cognito.UserPool(this, 'UserPool', {
      signInAliases: {
        email: true,
      },
      selfSignUpEnabled: true,
    })

    const userPoolClient = new cognito.UserPoolClient(this, 'UserPoolClient', {
      userPool,
      authFlows: {
        userPassword: true,
        adminUserPassword: true,
        userSrp: true,
      }
    })

    const identityPool = new cognito.CfnIdentityPool(this, 'IdentityPool', {
      allowUnauthenticatedIdentities: true,
      cognitoIdentityProviders: [{
        clientId: userPoolClient.userPoolClientId,
        serverSideTokenCheck: false,
        providerName: userPool.userPoolProviderName,
      }]
    })

    const unAuthRole = new iam.Role(this, 'UnAuthRole', {
      assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
        StringEquals: {
          ['cognito-identity.amazonaws.com:aud']: identityPool.ref,
        },
        ['ForAnyValue:StringLike']: {
          ['cognito-identity.amazonaws.com:amr']: 'unauthenticated'
        },
      }, 'sts:AssumeRoleWithWebIdentity'),
    })

    const authRole = new iam.Role(this, 'AuthRole', {
      assumedBy: new iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
        StringEquals: {
          ['cognito-identity.amazonaws.com:aud']: identityPool.ref,
        },
        ['ForAnyValue:StringLike']: {
          ['cognito-identity.amazonaws.com:amr']: 'authenticated'
        },
      }, 'sts:AssumeRoleWithWebIdentity'),
      managedPolicies: [iam.ManagedPolicy.fromManagedPolicyArn(this, 'AWSIoTFullAccess', 'arn:aws:iam::aws:policy/AWSIoTFullAccess')]
    })
    new cognito.CfnIdentityPoolRoleAttachment(this, 'IdentitiyRole', {
      identityPoolId: identityPool.ref,
      roles: {
        authenticated: authRole.roleArn,
        unauthenticated: unAuthRole.roleArn,
      },
    })
  }
}
```

まずは**ユーザープール**をつくり、**ユーザープールクライアント**、**IDプール**を作成します。この**IDプール**がIoT CoreとCognitoを連携させるために必要なものになります。
`unAuthRole`と`authRole`を定義して、認証されたユーザーに`authRole`を付与してAWS IoTの操作権限を渡します。

ここまでのコード全般は[こちら](https://github.com/ufoo68/iot-cognito)で公開しています。

# AWS IoTとCognitoの紐付け

この作業は、Cognitoで作成したユーザーのアイデンティティIDをAWS IoT Coreのポリシーに紐付けることをします。なのでどうしても手作業が生じるのでCDKでの実装は難しそうでした。

ここでユーザー作成とメッセージ送信用のUIを作ってしまったほうが楽だったので、[こちら](https://dev.classmethod.jp/articles/aws-iot-cognito-auth/)を参考に先にUIを作りました。さっと作りたかったので`create-react-app`を使いました。

```
create-react-app iot-cognito-ui
```

Cognito認証とデータのパブリッシュを簡単に使うための[aws-amplify](https://www.npmjs.com/package/aws-amplify)ライブラリをインストールします。

```
yarn add aws-amplify aws-amplify-react
```

コード全般は[こちら](https://github.com/ufoo68/iot-cognito-ui)で公開しています。Amplifyを使いましたが今回はライブラリの使用のみで特にデプロイはせずにローカルでの実行を行いました。`withAuthenticator()`を使うとこんな感じ↓のサインアップ・サインイン画面を勝手に作ってくれるので今回はこれでユーザー作成をしてみます。

![signup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209689/8ba93607-2dd6-dde1-62b5-5fe0208b0f3c.png)

ユーザー作成を行うとアイデンティティIDとうものがIDプールで作成されたことが確認できるので、それをIoT Coreのポリシーに紐付けます。以下のコマンドを実行します。

```
aws iot attach-policy --policy-name {{policy name}} --target {{cognito identity id}}
```

`{{policy name}}`はCDKで作成したものを、`{{cognito identity id}}`はIDプールで作成されたものを当てはめます。
マネジメントコンソール上で、作成したポリシーに新たに`COGNITO ID`と書かれた証明書が追加されていたら成功です。

# 動作確認

サーバー側の実装が完了したので、あとはクライアントアプリを作成して動作確認をしてみようと思います。本当は実デバイスを用意してやってみようと思ったのですが、このサーバー側の構築に手間取って気力を奪われてしまったので、PC上で疑似デバイス用のプログラムを作成してすべてPC上で動きを確認してみようと思います。

まずメッセージを送るためのUIアプリを、先程のcreate-react-appのテンプレートから`App.js`を変更して適当なUIを作成します。

```js:src/App.js
import React, { useState } from 'react'
import './App.css'
import { withAuthenticator } from 'aws-amplify-react'
import Amplify, { PubSub } from 'aws-amplify'
import { AWSIoTProvider } from '@aws-amplify/pubsub'
import cryptoRandomString from 'crypto-random-string'

Amplify.configure({
  Auth: {
    identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID,
    region: process.env.REACT_APP_REGION,
    userPoolId: process.env.REACT_APP_USER_POOL_ID,
    userPoolWebClientId: process.env.REACT_APP_USER_CLIENT_ID,
  }
})

Amplify.addPluggable(new AWSIoTProvider({
  aws_pubsub_region: process.env.REACT_APP_REGION,
  aws_pubsub_endpoint: `wss://${process.env.REACT_APP_PUBSUB_ENDPOINT}/mqtt`,
  clientId: cryptoRandomString({length: 10}),
}))

const App = () => {
  const [message, setMessage] = useState('')
  const [topic, setTopic] = useState('')

  const handleChangeTopic = (event) => {
    setTopic(event.target.value)
  }

  const handleChangeMessage = (event) => {
    setMessage(event.target.value)
  }

  const handlePublish = () => {
    PubSub.publish(topic, message)
    console.log('send')
  }

  return (
    <div className="App">
      <div>AWS IoT test UI</div>
      <div>
        <label>TOPIC:
          <input type="text" onChange={handleChangeTopic}></input>
        </label>
      </div>
      <div>
        <label>MESSAGE:
          <input type="text" onChange={handleChangeMessage}></input>
        </label>
      </div>
      <button onClick={handlePublish}>publish</button>
    </div>
  )
}

export default withAuthenticator(App)
```

とりあえず**topic名とメッセージ内容をtest**で送信してみます。

![publish.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209689/0b894cf3-a2f8-6ee1-da05-f640299c2a65.png)

受信用の疑似デバイスのコードも作成します（githubは[こちら](https://github.com/ufoo68/iot-cognito-device)）。このプログラムでは、マネジメントコンソールで発行した証明書を用いて認証を行っています。

```js:index.js
import awsIot from 'aws-iot-device-sdk'
import dotenv from 'dotenv'

dotenv.config()

const device = new awsIot.device({
  keyPath: process.env.KEY_PATH,
  certPath: process.env.CERT_PATH,
  caPath: process.env.CA_PATH,
  clientId: process.env.CLIENT_ID,
  host: process.env.HOST,
})

device
  .on('connect', () => {
    console.log('connect')
    device.subscribe(process.argv[2])
  })

device
  .on('message', (topic, payload) => {
    console.log('message', topic, payload.toString())
  })
```

こちらも同様に**test**というトピック名をSubscribeするように実行します(`process.argv[2]`でコマンド引数でトピック名を渡します)。

```
node index test
```

するとちゃんとUIから送ったメッセージを受信することができました。

```
message test "test"
```

# さいごに

最終的な成果物としては、単純にウェブアプリから送ったメッセージを受信するだけのものになりましたが、お互いに別々の形での認証を行った上でAWS IoT Coreを介したメッセージのやり取りを実現することができました。
正直手作業が多々あったのでCDKでのインフラ管理が完全にできていないのが残念ではありますが、それは今後の課題ということで今回の投稿を終了したいと思います。

明日は@KensukeTakaharaさんの投稿です。お楽しみに！

# 参考にしたサイト

- [Cognitoを使ってブラウザアプリからAWS IoTへのPub/Sub権限を制御する](https://dev.classmethod.jp/articles/aws-iot-cognito-auth/)
- [AWS CognitoとAWS IoTを連携させてみる](https://qiita.com/poruruba/items/34d4d7247d152b1ec933)
- [AWS IoT Analytics を CDK で構築してみた](https://dev.classmethod.jp/articles/aws-iot-analytics-sample-cdk/)
- [Raspberry PiとAWS IoTでLチカしてみた](https://dev.classmethod.jp/articles/raspberry-pi-aws-iot-l-chika/)!