【AWS Amplify】Admin Queriesを利用したcognitoユーザーの作成・削除機能の作成

# はじめに
対象者は以下に当てはまる方です。
- ReactアプリでAmplify（CLI）を利用している
- Cognitoで認証を行っている
- アプリ上でユーザーの作成・削除を行いたい

私はwebアプリ制作素人なのですがタイトルの実現にかなり苦しんだので、同じ涙を減らすためここに記事を残します。
何か間違い等あればコメントお願いします。

# いきなり結論
まず、以下のページを見ながらAmplify CLIを利用してAdmin Queriesを作成します。

https://docs.amplify.aws/react/build-a-backend/auth/admin-actions/

作成すると`amplify/backend/function/AdminQueriesxxxxxxxx`の下に `AdminQueriesxxxxxxxx-cloudformation-template.json`が作成されているので、編集します。
```json:AdminQueriesxxxxxxxx-cloudformation-template.json
...
"Resources": {
  ...,
  "lambdaexecutionpolicy": {
    ...,
    "Properties": {
      ...,
      "PolicyDocument": {
        ...,
        "Statement": [
          ...,
          {
            "Effect": "Allow",
            "Action": [
              ...,
              // 以下を追加
              "cognito-idp:AdminCreateUser",
              "cognito-idp:AdminUpdateUserAttributes",
              "cognito-idp:AdminDeleteUser"
            ]
          }
        ]
      }
    }
  }
}
```


次に、`amplify/backend/function/AdminQueriesxxxxxxxx/src`の下にある`app.js`と`cognitoActions.js`を編集し、以下のコードを追加します。

```javascript:app.js
const {
  createUser,
  deleteUser,
} = require('./cognitoActions');

// ユーザー作成
app.post('/createUser', async (req, res, next) => {
  if (!req.body.username || !req.body.attributes) {
    const err = new Error('username and attributes are required');
    err.statusCode = 400;
    return next(err);
  }

  try {
    const response = await createUser(req.body.username, req.body.attributes);
    res.status(200).json(response);
  } catch (err) {
    next(err);
  }
});

const {
  deleteUser,
} = require('./cognitoActions');

// ユーザー削除
app.post('/deleteUser', async (req, res, next) => {
  if (!req.body.username) {
    const err = new Error('username is required');
    err.statusCode = 400;
    return next(err);
  }

  try {
    const response = await deleteUser(req.body.username);
    res.status(200).json(response);
  } catch (err) {
    next(err);
  }
});
```

```javascript:cognitoActions.js
const {
  AdminCreateUserCommand,
  AdminDeleteUserCommand,
} = require('@aws-sdk/client-cognito-identity-provider');

// ユーザー作成
async function createUser(username, attributes) {
  const input = {
    UserPoolId: userPoolId,
    Username: username,
    UserAttributes: attributes,
  };
  
  console.log(`Attempting to create user: ${username}`);

  try {
    await cognitoIdentityProviderClient.send(new AdminCreateUserCommand(input));
    return {
      message: `Created ${username}`,
    };
  } catch (err) {
    console.log(err);
    throw err;
  }
}

// ユーザー削除
async function deleteUser(username) {
  const input = {
    UserPoolId: userPoolId,
    Username: username,
  };
  
  console.log(`Attempting to delete user: ${username}`);

  try {
    await cognitoIdentityProviderClient.send(new AdminDeleteUserCommand(input));
    return {
      message: `Deleted ${username}`,
    };
  } catch (err) {
    console.log(err);
    throw err;
  }
}

module.exports = {
  createUser,
  deleteUser,
};
```


上記コードを追加したら、`amplify push`でデプロイしてください。
最後に、作成したAPIにリクエストを送るコードを作成します。
```typescript:request.ts
import { post } from 'aws-amplify/api';
import { fetchAuthSession } from 'aws-amplify/auth';

// ユーザー作成
export async function createUser( { email }: { email: string } ) {
  const authSession = (await fetchAuthSession()).tokens;
  const apiName = 'AdminQueries';
  const path = '/createUser';
  const userAttributes = [
    { "Name": "email", "Value": email },
  ]

  const options = {
    body: {
      "username": email,
      'attributes': userAttributes
    },
    headers: {
      'Content-Type' : 'application/json',
      Authorization: `${authSession?.accessToken.toString()}`
    }
  }

  try {
    const restOperation = post({ 
      apiName: apiName,
      path: path,
      options: options
    });
    const response = await restOperation.response;
    const data = await response.body.json();
    console.log('Admin Queries (createUser) call succeeded: ', data);
  } catch (e) {
    console.log('Admin Queries (createUser) call failed: ', e);
  }
};

// ユーザー削除
export async function deleteUser({ email }: { email: string }) {
  const authSession = (await fetchAuthSession()).tokens;
  const apiName = 'AdminQueries';
  const path = '/deleteUser';

  const options = {
    body: {
      "username": email,
    },
    headers: {
      'Content-Type' : 'application/json',
      Authorization: `${authSession?.accessToken.toString()}`
    }
  }

  try {
    const restOperation = post({ 
      apiName: apiName,
      path: path,
      options: options
    });
    const response = await restOperation.response;
    const data = await response.body.json();
    console.log('Admin Queries (deleteUser) call succeeded: ', data);
  } catch (e) {
    console.log('Admin Queries (deleteUser) call failed: ', e);
  }
};
```

以上です。


# 余談: 苦しみの軌跡
### 【前編】APIが繋がらない

作成していたwebアプリで管理者ユーザーが他のユーザーを管理する必要があり、Amplifyで何かないか探したところAdmin Queriesを見つけました。

https://docs.amplify.aws/react/build-a-backend/auth/admin-actions/

とりあえず公式のドキュメントに従って作ったはいいものの、動かない。
そう、**公式に書いてあるコードでは動きません**。（なんで？）

ブラウザの開発者ツールを見ると以下のようなエラーが出ていました。
`
Access to fetch at 'https://~' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
`
`
POST https://~ net::ERR_FAILED 401 (Unauthorized)
`

とりあえず「Access-Control-Allow-Origin」とかいうヘッダーが無いらしいしCORSポリシーがどうのこうの言われているので、CORSについて調べてみました。
すると確かにヘッダーにAccess-Control-Allow-Origin等付ける必要があるらしいが、付けるのはAPI側な雰囲気。

APIのコードを確認してみると、
```javascript:app.js
// Enable CORS for all methods
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  next();
});
```
付いてね…？

ここで丸一日くらい悩みました。

答えを言うとエラー文の`POST ~ (unauthorized)`が本質で、Access-Control-Allow-Originが無いとか言われているのは返ってきたエラーのレスポンスにヘッダーが無かったからでした。
これはAWSコンソール（web）のAPI GatewayからAdminQueriesを選択し、「ゲートウェイのレスポンス」から401のエラーにヘッダーをつけることで解決できます。

エラーは減ったけれど結局繋がらないのは解決していません。
unauthorizedということは認証がうまくいっていないので、ヘッダーに追加されている
``
Authorization: `${(await fetchAuthSession()).tokens.accessToken.payload}`
``
を弄って
``
Authorization: `${await fetchAuthSession()).tokens?.accessToken.toString()}`
``
にしたら何とかなりました。

### 【後編】ユーザー作成処理の書き方がわからん
[この記事](https://dev.classmethod.jp/articles/amplify-admin-queries-api-custom/)を参考に作業してたんですが、如何せん2021年の記事なので微妙に中身のコードが違う…
`cognitoActions.js`内で`AdminAddUserToGroupCommand`とかrequireで入れてたので、「AdminCreateUserCommand」とか無いかな～と思い検索。

ありました。ドンピシャで。

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/command/AdminCreateUserCommand/

コードサンプルもあったのでこれを読みながらすんなり実装できました。同じコマンドで初期パスワード再発行もできるみたいです。

以下のページに使用可能なコマンドが並んでいるので、これを読んで使えば割と何でもできる気がします。
（admin actionsのページに直リンクとかあったらいいのに…　見落としてるだけだったらごめんなさい）

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/cognito-identity-provider/

これを読みながら試行錯誤していた時でした。

コンソールのLambdaにも同じコードが存在しているので、テストが楽という理由でそちら（コンソール上）のコードを変更し保存しました。
その後ローカルにコードを編集しAmplify CLIでpushしようとしたところ、**エラーでpushもpullもできなくなりました**。
一度削除してリカバリーみたいなのも行ったんですが、エラーは起き続き…

最終的に**リソースを全削除して１から作り直す羽目になりました**。

ちゃんとわかっていませんが、おそらくAmplify CLIで作成したLambdaのコードをコンソール上で変更するとこうなるようです。
（環境変数の設定やVPCの設定などは問題ありませんでした。）

**Amplify CLIで作成したLambdaのコードはコンソール上で弄らないことをお勧めします。**

皆様、くれぐれもお気を付けください…。


以上、お役に立てれば幸いです。