Cognito User Pool の DeveloperOnlyAttribute って何？


## 結論

管理者向け API でしか読み取れないユーザー属性っぽいです。
クライアント向け API では読み書きできません。管理者向け API でも書き込みはできません。

### 2020/08/19 追記

いつの間にか API Reference に以下の記載が追加されていました。

SchemaAttributeType - Amazon Cognito Identity Provider
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SchemaAttributeType.html

> We recommend that you use WriteAttributes in the user pool client to control how attributes can be mutated for new use cases instead of using DeveloperOnlyAttribute.

アプリクライアント毎に設定できる WriteAttibutes を使うことを推奨しているようです。

追記おわり

## どういう事？

CloudFormation の Cognito User Pool リファレンスを眺めていた所、個人的に見慣れないオプションを発見しました。

Amazon Cognito UserPool SchemaAttribute - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-schemaattribute.html

> DeveloperOnlyAttribute
>     属性タイプが開発者のみであるかを指定します。
>     Type: Boolean
>     Required: No

開発者のみとは？
気になるので Cognito 側の API リファレンスを確認してみます。


SchemaAttributeType - Amazon Cognito Identity Provider
https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_SchemaAttributeType.html

> DeveloperOnlyAttribute
>     Specifies whether the attribute type is developer only.
>     Type: Boolean
>     Required: No

属性タイプが developer only か指定する的な。
この文脈で言う developer とは一体・・・

## DeveloperOnlyAttribute を有効化した User Pool を作ってみる

DeveloperOnlyAttribute を有効化した `myattribute` というカスタム属性を定義して User Pool を作ってみます。
勉強も兼ねて CloudFormation 使っていきます。

```myuserpool.yaml
Resources:
  DeveloperOnlyAttributeEnabledCognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: DeveloperOnlyAttributeEnabledCognitoUserPool
      Schema: 
        - Name: myattribute
          AttributeDataType: String
          DeveloperOnlyAttribute: True
```

CloudFormation テンプレートを作成したら、 AWS CLI からデプロイ

```bash
$ aws cloudformation deploy --template-file ./myuserpool.yaml --stack-name developer-only-attribute-enabled-cognito-user-pool

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - developer-only-attribute-enabled-cognito-user-pool
```

出来上がった User Pool の設定を確認してみます。

```bash
$ aws cognito-idp describe-user-pool --user-pool-id ap-northeast-1_xxx
{
    "UserPool": {
        "Id": "ap-northeast-1_xxx",
        "Name": "DeveloperOnlyAttributeEnabledCognitoUserPool",
        "Policies": {
            "PasswordPolicy": {
                "MinimumLength": 8,
                "RequireUppercase": true,
                "RequireLowercase": true,
                "RequireNumbers": true,
                "RequireSymbols": true,
                "TemporaryPasswordValidityDays": 7
            }
        },
        "LambdaConfig": {},
        "LastModifiedDate": 1565706803.41,
        "CreationDate": 1565706803.41,
        "SchemaAttributes": [
            {
                "Name": "sub",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": false,
                "Required": true,
                "StringAttributeConstraints": {
                    "MinLength": "1",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "given_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "family_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "middle_name",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "nickname",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "preferred_username",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "profile",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "picture",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "website",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "email",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "email_verified",
                "AttributeDataType": "Boolean",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false
            },
            {
                "Name": "gender",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "birthdate",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "10",
                    "MaxLength": "10"
                }
            },
            {
                "Name": "zoneinfo",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "locale",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "phone_number",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "phone_number_verified",
                "AttributeDataType": "Boolean",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false
            },
            {
                "Name": "address",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "StringAttributeConstraints": {
                    "MinLength": "0",
                    "MaxLength": "2048"
                }
            },
            {
                "Name": "updated_at",
                "AttributeDataType": "Number",
                "DeveloperOnlyAttribute": false,
                "Mutable": true,
                "Required": false,
                "NumberAttributeConstraints": {
                    "MinValue": "0"
                }
            },
            {
                "Name": "dev:custom:myattribute",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": true,
                "Mutable": false,
                "Required": false,
                "StringAttributeConstraints": {}
            }
        ],
        "VerificationMessageTemplate": {
            "DefaultEmailOption": "CONFIRM_WITH_CODE"
        },
        "MfaConfiguration": "OFF",
        "EstimatedNumberOfUsers": 0,
        "EmailConfiguration": {
            "EmailSendingAccount": "COGNITO_DEFAULT"
        },
        "UserPoolTags": {},
        "AdminCreateUserConfig": {
            "AllowAdminCreateUserOnly": false,
            "UnusedAccountValidityDays": 7
        },
        "Arn": "arn:aws:cognito-idp:ap-northeast-1:123456789012:userpool/ap-northeast-1_xxx"
    }
}
```

情報が盛り沢山ですが、今回着目するのは次の箇所です。

```json
            {
                "Name": "dev:custom:myattribute",
                "AttributeDataType": "String",
                "DeveloperOnlyAttribute": true,
                "Mutable": false,
                "Required": false,
                "StringAttributeConstraints": {}
            }
```

`myattribute` というカスタム属性が作成されて、 `DeveloperOnlyAttribute` が `true` になってますね。
普通のカスタム属性は `custom:attributename` のような形式ですが、 `dev:custom:attributename` のように、 `dev:` がプレフィックスとして追加されています。


## ユーザーを作ってみる

まず、ユーザー作るのにアプリクライアントが必要なので作ります。

```bash
$ aws cognito-idp create-user-pool-client --user-pool-id ap-northeast-1_xxx --client-name myappclient --explicit-auth-flows "ADMIN_NO_SRP_AUTH"
{
    "UserPoolClient": {
        "UserPoolId": "ap-northeast-1_xxx",
        "ClientName": "myappclient",
        "ClientId": "6c0fdunb8s2ijns0ue2lfr8iia",
        "LastModifiedDate": 1565707946.052,
        "CreationDate": 1565707946.052,
        "RefreshTokenValidity": 30,
        "ExplicitAuthFlows": [
            "ADMIN_NO_SRP_AUTH"
        ],
        "AllowedOAuthFlowsUserPoolClient": false
    }
}
```

これでユーザー作成の準備ができたので、カスタム属性を設定して sign up してみます。

```bash
$ aws cognito-idp sign-up --client-id 6c0fdunb8s2ijns0ue2lfr8iia --username sampleuser --password 'Passw@rd1234' --user-attributes Name=dev:custom:myattribute,Value=hogehoge
{
    "UserConfirmed": false,
    "UserSub": "bb64ae42-b532-4182-8efa-efc67662a345"
}
```

で、後で認証するため confirm しておきます。

```bash
$ aws cognito-idp admin-confirm-sign-up --user-pool-id ap-northeast-1_xxx --username sampleuser
```

`admin-confirm-sign-up` コマンドは、成功時に特に出力は無いので `admin-get-user` コマンドでユーザー状況を確認します。

```bash
$ aws cognito-idp admin-get-user --user-pool-id ap-northeast-1_xxx --username sampleuser
{
    "Username": "sampleuser",
    "UserAttributes": [
        {
            "Name": "sub",
            "Value": "bb64ae42-b532-4182-8efa-efc67662a345"
        },
        {
            "Name": "dev:custom:myattribute",
            "Value": "hogehoge"
        }
    ],
    "UserCreateDate": 1565708213.268,
    "UserLastModifiedDate": 1565708247.72,
    "Enabled": true,
    "UserStatus": "CONFIRMED"
}
```

`UserStatus` が `CONFIRMED` になってれば OK


## 認証して、カスタム属性を取得してみる

`ADMIN_NO_SRP_AUTH` を使ってサクッと認証します。

```bash
$ aws cognito-idp admin-initiate-auth --user-pool-id ap-northeast-1_xxx --client-id 6c0fdunb8s2ijns0ue2lfr8iia --auth-flow ADMIN_NO_SRP_AUTH --auth-parameters USERNAME=sampleuser,PASSWORD=Passw@rd1234
{
    "ChallengeParameters": {},
    "AuthenticationResult": {
        "AccessToken": "xxx",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": "yyy",
        "IdToken": "zzz"
    }
}
```

ここでレスポンスされる `AccessToken` を使っていきます。


## クライアント向け API でカスタム属性を読み書きしてみる

Cognito User Pool には、IAM のクレデンシャルを使って API 呼び出しを行う管理者向け API と、認証時に発行されるアクセストークンを使って API 呼び出しを行うクライアント向け API があります。

リソースのアクセス許可 - Amazon Cognito
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/resource-permissions.html#amazon-cognito-signed-versus-unsigned-apis

> 署名された API と署名されていない API
> AWS 認証情報で署名された API は、IAM ポリシーで制限することができます。次の Cognito API は署名されていないため、IAM ポリシーで制限することはできません。

署名されていない API を使って、カスタム属性の読み書きを試してみます。

```bash
$ aws cognito-idp get-user --access-token xxx
{
    "Username": "sampleuser",
    "UserAttributes": [
        {
            "Name": "sub",
            "Value": "bb64ae42-b532-4182-8efa-efc67662a345"
        }
    ]
}
```

`UserAttributes` に `dev:custom:myattribute` が含まれていません。
クライアント向け API で無理やり `dev:custom:myattribute` を変更しようとしてみます。

```bash
$ aws cognito-idp update-user-attributes --user-attributes Name=dev:custom:myattribute,Value=fugafuga --access-token xxx

An error occurred (InvalidParameterException) when calling the UpdateUserAttributes operation: Invalid user attributes: custom:myattribute: Attribute cannot be updated.
```

ズバリなエラーメッセージですね。
カスタム属性として `custom:myattribute` は存在しているようですが、更新はできないようです。

削除も試してみます。

```bash
$ aws cognito-idp delete-user-attributes --user-attribute-names dev:custom:myattribute --access-token xxx

An error occurred (InvalidParameterException) when calling the DeleteUserAttributes operation: Invalid user attributes: user.dev:custom:myattribute: Attribute cannot be updated.
```

`user.dev:custom:myattribute` 属性は存在するようですが、削除も含めた更新ができないようです。

## 管理者向け API でカスタム属性を読み書きしてみる

AWS 認証情報での署名が必要な、管理者向け API で色々ためしてみます。
既にユーザー作成時に試してますが、改めて admin-get-user でユーザー情報を取得してみます。

```bash
$ aws cognito-idp admin-get-user --user-pool-id ap-northeast-1_xxx --username sampleuser
{
    "Username": "sampleuser",
    "UserAttributes": [
        {
            "Name": "sub",
            "Value": "bb64ae42-b532-4182-8efa-efc67662a345"
        },
        {
            "Name": "dev:custom:myattribute",
            "Value": "hogehoge"
        }
    ],
    "UserCreateDate": 1565708213.268,
    "UserLastModifiedDate": 1565708247.72,
    "Enabled": true,
    "UserStatus": "CONFIRMED"
}
```

`dev:custom:myattribute` 属性の情報が参照できています。
では、次は更新を試してみます。

```bash
$ aws cognito-idp admin-update-user-attributes --user-pool-id ap-northeast-1_xxx --username sampleuser --user-attributes Name=dev:custom:myattribute,Value=fugafuga

An error occurred (InvalidParameterException) when calling the AdminUpdateUserAttributes operation: Invalid user attributes: custom:myattribute: Attribute cannot be updated.
```

管理者向け API でも属性の更新ができないようです。
一応、管理者向け API での削除も試してみます。

```bash
$ aws cognito-idp admin-delete-user-attributes --user-pool-id ap-northeast-1_xxx --username sampleuser --user-attribute-names dev:custom:myattribute

An error occurred (InvalidParameterException) when calling the AdminDeleteUserAttributes operation: Invalid user attributes: user.dev:custom:myattribute: Attribute cannot be updated.
```

というわけで削除もできません。


## 検証結果まとめ、結論

検証結果をまとめます。

|  | 読み取り | 更新 | 削除 |
|---|---|---|---|
| クライアント API <br> (get-user, update-attribute, delete-attribute)  | できない  | できない | できない |
| 管理者向け API <br> (admin-get-user, admin-update-attribute, admin-delete-attribute)  | できる  | できない | できない |

というわけで、クライアント向け API では読み書きできず、管理者向け API では読み込み可能であるものの書き込みはできない状態でした。
つまり DeveloperOnlyAttribute は管理者向け API でのみ読み込むことが出来る属性と言えそうです。
DeveloperOnlyAttribute の Developer とは、 IAM のクレデンシャルを持つ開発者というようなニュアンスっぽいですね。

また、公式フォーラムに以下のような情報がありました。

AWS Developer Forums: DeveloperOnlyAttribute - what is the effect of the property? 
https://forums.aws.amazon.com/thread.jspa?threadID=267841&tstart=0

> This for the attributes that developer doesn't want to expose to the end users.

DeveloperOnlyAttribute はエンドユーザーに見せたくない属性に使う設定のようです。
個人的に具体的なユースケースが思い浮かびませんが・・・

ちなみに、検証で作成したリソースの後片付けは CloudFormation で作成したので、スタックを削除すれば OK です

```bash
$ aws cloudformation delete-stack --stack-name developer-only-attribute-enabled-cognito-user-pool
```

以上、 DeveloperOnlyAttribute について調べてみたメモでした。
こういう用途で DeveloperOnlyAttribute 使ってるとか、検証内容について指摘等あれば是非コメントください。