Page updated Jan 16, 2024

GraphQL Transformer @auth identity claim changes

The Amplify CLI is changing the default owner value in GraphQL APIs in v8.1.0. Previously, the API stored only the username by default. In this next release behind a feature flag, and in an upcoming major CLI release, the GraphQL Transformer will store a user's unique sub and username.

What is changing?

In the Amplify CLI >= v8.0.3 owner-based @auth rule uses "username" as the default identity claim from a JSON Web Token from AWS Cognito and stores it under the owner field in your app's DynamoDB table. This means the owner field of a record will be queried and populated with a user's username. The Amplify CLI GraphQL Transformer is changing the default identityClaim to use a combination of sub - the unique ID (uuid) given by Cognito in your User Pool - and username from the JWT token with a delimiter of ::.

What are the breaking changes?

The sort key fields for your @primaryKeys can no longer be part of an owner-based authorization's ownerField. This also applies to the auto-generated owner field when you add owner-based authorization @auth(rules: [{ allow: owner }]). Here are two primary examples that don't support this:

## NOT supported schema example 1 ## type Item @auth(rules: [{ allow: owner }]) @model { id: ID! @primaryKey(sortKeyFields: ["owner"]) owner: String ## "owner" is an auto-generated field of the owner-based authorization rule }
1## NOT supported schema example 1 ##
2type Item @auth(rules: [{ allow: owner }]) @model {
3 id: ID! @primaryKey(sortKeyFields: ["owner"])
4 owner: String ## "owner" is an auto-generated field of the owner-based authorization rule
5}
## NOT supported schema example 2 ## type Item @auth(rules: [{ allow: owner, ownerField: "user" }]) @model { id: ID! @primaryKey(sortKeyFields: ["owner"]) user: String ## "user" is the designated "ownerField" of the owner-based authorization rule }
1## NOT supported schema example 2 ##
2type Item @auth(rules: [{ allow: owner, ownerField: "user" }]) @model {
3 id: ID! @primaryKey(sortKeyFields: ["owner"])
4 user: String ## "user" is the designated "ownerField" of the owner-based authorization rule
5}

If your table requires this configuration, it is recommended to set the identityClaim to username.

Additionally, if you want to continue making queries with the owner field as the secondary query parameter, consider using the @index directive instead. Using the mentioned example, you can set up a query as the following:

type Todo @auth(rules: [{ allow: owner, ownerField: "user" }]) { listId: ID! @primaryKey @index(name: "byUser", sortKeyFields: ["user"], queryField: "todoByUser") user: String! }
1type Todo @auth(rules: [{ allow: owner, ownerField: "user" }]) {
2 listId: ID!
3 @primaryKey
4 @index(name: "byUser", sortKeyFields: ["user"], queryField: "todoByUser")
5 user: String!
6}

You will be able to query your Todo by user with the following query:

query byUser($user: String!) { todoByUser(user: $user) { items { listId user } nextToken } }
1query byUser($user: String!) {
2 todoByUser(user: $user) {
3 items {
4 listId
5 user
6 }
7 nextToken
8 }
9}

Learn more about configuring the @index directive here.

There are no other breaking changes to your GraphQL API when using the new default identity claim. The resolvers will store these values in your DynamoDB tables in the format of <sub>::<username>, and return username to the client code by default.

While the other directives will work as they are currently expected to function (i.e. @searchable, @function), using custom queries to your databases may need to be changed. For example, if you are performing a query with the filter parameter, you will need to change it from using the filter query with just 1 argument to using an or conditional statement like the following:

query MyQuery { listPrivateNotes( filter: { or: [{ owner: { contains: "::user1" } }, { owner: { eq: "user1" } }] } ) { nextToken items { id content } } }
1query MyQuery {
2 listPrivateNotes(
3 filter: {
4 or: [{ owner: { contains: "::user1" } }, { owner: { eq: "user1" } }]
5 }
6 ) {
7 nextToken
8 items {
9 id
10 content
11 }
12 }
13}

Another example of changing your queries is if you are using OpenSearch for a custom query like the one below, you will need your queries to account for the format of the stored owner field with a matching operation. See supported search operations. Here's an example of using the match operation:

query SearchAndSort { searchStudents(filter: or: [{ owner: { match: "*::user1" } }, { owner: { eq: "user1" } }], ) { items { name id } } }
1query SearchAndSort {
2 searchStudents(filter:
3 or: [{
4 owner: { match: "*::user1" }
5 }, {
6 owner: { eq: "user1" }
7 }],
8 ) {
9 items {
10 name
11 id
12 }
13 }
14}

How do I migrate my GraphQL API?

Automatic changes the CLI will make for new apps

If you are creating a new Amplify project, your app's GraphQL API will be created using the "sub::username" identity claim, and no further action is required from you. Your database records will store the uuid and username in the owner field in the format of <sub>::<username>, and the API will return <username>.

If you would like to use username for the identity claim without storing sub, specify in your schema the following:

type Todo @model @auth( rules: [ { allow: owner identityClaim: "username" # explicit use of username } ] ) { id: ID! title: String! }
1type Todo
2 @model
3 @auth(
4 rules: [
5 {
6 allow: owner
7 identityClaim: "username" # explicit use of username
8 }
9 ]
10 ) {
11 id: ID!
12 title: String!
13}

Manual changes to continue using "username" for existing apps

If you have an existing schema that uses owner-based @auth with an implicit identity claim rule on a type or field similar to this:

type Todo @model @auth( rules: [ { allow: owner # no explicit identity claim } ] ) { id: ID! title: String! }
1type Todo
2 @model
3 @auth(
4 rules: [
5 {
6 allow: owner # no explicit identity claim
7 }
8 ]
9 ) {
10 id: ID!
11 title: String!
12}

Your GraphQL schema is using the default identity claim "username" and the Amplify CLI GraphQL Transformer is using the default value to generate your VTL files. Therefore, your schema is read as:

type Todo @model @auth( rules: [ { allow: owner identityClaim: "username" # explicit identity claim } ] ) { id: ID! title: String! }
1type Todo
2 @model
3 @auth(
4 rules: [
5 {
6 allow: owner
7 identityClaim: "username" # explicit identity claim
8 }
9 ]
10 ) {
11 id: ID!
12 title: String!
13}

The Amplify CLI is changing the default to "sub::username" in v9.0.0, so if your identityClaim is not explicitly defined, the transformers will use "sub::username" unless you have set "username" to your schema as shown above.

In the initial release, this functionality will be behind a feature flag, useSubUsernameForDefaultIdentityClaim, with false as the default value. Setting the feature flag to true enables the transformers to use the new identity claim; however, it is recommended to explicitly state your identity claim moving forward as this feature flag is only temporary (as shown above).

Manual changes to use "sub::username" for existing apps

If you wish to migrate your VTL files before the changes in v9.0.0, set your identityClaim to "sub::username".

type Todo @model @auth( rules: [ { allow: owner identityClaim: "sub::username" # set your identity claim } ] ) { id: ID! title: String! }
1type Todo
2 @model
3 @auth(
4 rules: [
5 {
6 allow: owner
7 identityClaim: "sub::username" # set your identity claim
8 }
9 ]
10 ) {
11 id: ID!
12 title: String!
13}

Keep in mind that if you have existing owner records in your database and owner-reliant code in your code base, these changes will not migrate your data. The resolvers are backwards compatible, so your API will still be compatible with the former contract that uses username for the owner field. In other words, when using the sub::username identity claim, your resolvers will authorize both username and sub values from the record that match the JWT, but the resolvers will write <sub>::<username> when no owner field input is specified.

What is the timeline?

v8.1.0 of the Amplify CLI is introducing the feature flag, useSubUsernameForDefaultIdentityClaim, that will allow developers to opt into the sub::username default. New Amplify projects created will already be opted into the feature flag, but developers that want to use the new default will have to opt in. For existing Amplify projects, that do not have the feature flag, useSubUsernameForDefaultIdentityClaim will default to false.

The Amplify CLI team will release v9.0.0, with the feature flag removed, and developers will need to manually set "username" to their schemas to maintain their former API contract, or the resolvers will start to store sub::username in their databases.

Check for invalid owner identity claims

Amplify CLI v10.0.0 is adding stricter checks to ensure that valid owner identity claims exist in the customer's JWT token before storing these values in the owner field. Previously, if the developer-specified owner identity claim does not exist in the customer's JWT token, a default none value was stored in the owner field.

Amplify CLI v10.0.0 and above, if the developer-specified identity claim does not exist in the customer's JWT token, the customer will be not be authorized to access the record.

Since you cannot determine the owner from the previously stored default none value in the owner field, any requests to access these records will not be authorized.

Consider the following schema with custom owner identity claim:

type Todo @model @auth(rules: [{ allow: owner, identityClaim: "userID" }]) { id: ID! title: String! }
1type Todo @model @auth(rules: [{ allow: owner, identityClaim: "userID" }]) {
2 id: ID!
3 title: String!
4}

The developer-specified owner identity claim i.e userID should be present in the customer's JWT token for them to be authorized.

Consider the following schema with default owner identity claim:

type Todo @model @auth(rules: [{ allow: owner }]) { id: ID! title: String! }
1type Todo @model @auth(rules: [{ allow: owner }]) {
2 id: ID!
3 title: String!
4}

The claims sub and username should be present in the customer's JWT token for them to be authorized.