Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amplify API Unathorized error Error #3677

Closed
dan-codes1 opened this issue May 6, 2024 · 29 comments
Closed

Amplify API Unathorized error Error #3677

dan-codes1 opened this issue May 6, 2024 · 29 comments
Assignees
Labels
api Issues related to the API category bug Something isn't working

Comments

@dan-codes1
Copy link

dan-codes1 commented May 6, 2024

Describe the bug

I have been experiencing some graphql api bugs since I upgraded my amplify version from 2.26.1 to 2.29.2.
The error occurs when I try to asynchronously load the lazy attibutes in my structs. E.g., take these schemas:

type Message @model @auth(rules: [{allow: private, operations: [read]}, {allow: owner, operations: [create, update, delete]}]) {
    id: ID!
    type: MessageType!
    text: String
    sentAt: AWSDateTime!
    isRead: Boolean!
    sender: User! @hasOne
    space: MessageSpace! @hasOne
    editedAt: AWSDateTime
    modelString: String! @index(name: "messagesByDate", queryField: "messagesByDate", sortKeyFields: ["sentAt"])
    modelStatus: ModelStatus!
}

type MessageSpace @model @auth(rules: [{allow: private, operations: [create, read, update]}]) {
    id: ID!
    userIds: [String!]!
    type: MessageSpaceType!
    lastSentMessageId: String
    hasMessages: Boolean!
    lastModified: AWSDateTime!
    messages: [Message] @hasMany
    modelString: String! @index(name: "messagesSpacesByDate", queryField: "messagesSpacesByDate", sortKeyFields: ["lastModified"])
    modelStatus: ModelStatus!
}

When I try to load the space attribute from the message, it throws this error:

GraphQLResponseError<Optional<MessageSpace>>: GraphQL service returned a successful response containing errors: [Amplify.GraphQLError(message: "Not Authorized to access getMessageSpace on type Query", locations: Optional([Amplify.GraphQLError.Location(line: 2, column: 3)]), path: Optional([Amplify.JSONValue.string("getMessageSpace")]), extensions: Optional(["errorInfo": Amplify.JSONValue.null, "data": Amplify.JSONValue.null, "errorType": Amplify.JSONValue.string("Unauthorized")]))]
Recovery suggestion: The list of `GraphQLError` contains service-specific messages

This is the code and api I use:

let request = try await Amplify.API.query(request: .get(Message.self, byId: id, authMode: .amazonCognitoUserPools))
let message = try request.get() 
let sender = try await message.sender
let space = try await message.space

It throws an error when I try to get the space but not the sender. This only started when I upgraded my amplify project, and started using the new multi auth apis in: #3630

This is the user schema for the sender attribute

type User @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update, delete]}]) {
    id: ID!
    version: Int!
}

Steps To Reproduce

  1. Try the code above in the previous amplify version 2.26.1, using the old multi auth method (providing the api names)
  2. If that works, try it with amplify version 2.29.2. with multi auth feat(api): add authorizationMode to GraphQLRequest #3630
  3. Does it work?

Expected behavior

To not throw an error and work as it did with the previous apis and version

Amplify Framework Version

2.29.2.

Amplify Categories

API

Dependency manager

Swift PM

Swift version

5.9

CLI version

12.11.1

Xcode version

15

Relevant log output

<details>
<summary>Log Messages</summary>


INSERT LOG MESSAGES HERE
```

Is this a regression?

Yes

Regression additional context

No response

Platforms

iOS

OS Version

15

Device

iPhone 15

Specific to simulators

no

Additional context

No response

@ruisebas ruisebas added bug Something isn't working api Issues related to the API category labels May 6, 2024
@ruisebas
Copy link
Member

ruisebas commented May 6, 2024

Hi @dan-codes1 , thanks for opening this issue.

We've recently fixed a network-related issue that was causing Authentication problems in the latest version 2.33.3.
Would you mind please upgrading to this version and verifying if it also addresses your issue?

Thanks!

@dan-codes1
Copy link
Author

Hi, I just updated to the latest version but the bug still persists

@dan-codes1
Copy link
Author

Just to note: Though I gave a specific example using the Message and MessageSpace models, the same unauthorized error occurs with other models in the app.

@lawmicha lawmicha self-assigned this May 7, 2024
@lawmicha lawmicha added the pending-triage Issue is pending triage label May 7, 2024
@lawmicha
Copy link
Member

lawmicha commented May 7, 2024

Hi @dan-codes1, thanks for trying out the new authMode parameter. The authMode determines which interceptor to use. In your example, the request with user pool auth should have the token added to the request Authorization header. Could you breakpoint into this line and take a look at the final request before it is sent to AppSync?

https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift#L80

Then po finalRequest and let us know if you are able to confirm the request is updated with the token. I'm assuming the user is signed in before making the request.

@lawmicha
Copy link
Member

lawmicha commented May 7, 2024

are there any more logs related to the request with verbose log level enabled? Amplify.Logging.logLevel = .verbose before Amplify.configure()

@dan-codes1
Copy link
Author

dan-codes1 commented May 8, 2024

Hi,

The user is signed in. This is the log output for the request:

Starting mutation 3C303F34-16F7-46E9-AA58-362B7F0DD52B
{
  "query" : "mutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n    appVersion\n    bioAdress\n    bioLanguage\n    biography\n    biographyTitle\n    countryCode\n    createdAt\n    currentDevice\n    dateOfBirth\n    deviceTypes\n    email\n    emailConfirmed\n    favouriteArtiste\n    favouriteMusicGenre\n    favoutieShowAsAKid\n    favoutiteSong\n    firstName\n    funFact\n    hobby\n    identityVerified\n    image {\n      id\n      index\n      s3Key\n      __typename\n    }\n    isConfirmed\n    isPremium\n    isSubscribed\n    lastName\n    middleName\n    mobile\n    mobileNumberConfirmed\n    modelStatus\n    modelString\n    pets\n    profilePhotoStandardUrl\n    profilePhotoThumbnailUrl\n    rating\n    staus\n    subscriptionEndDate\n    subscriptionId\n    subscriptionRenewedDate\n    subscriptionStartDate\n    subscriptionType\n    timeZone\n    timeZoneIdentifier\n    updatedAt\n    userType\n    verifiedDOB\n    verifiedFirstName\n    verifiedLastName\n    version\n    __typename\n  }\n}",
  "variables" : {
    "input" : {
      "countryCode" : "CA",
      "favoutiteSong" : null,
      "pets" : null,
      "favoutieShowAsAKid" : null,
      "verifiedFirstName" : null,
      "timeZone" : "MDT",
      "lastName" : "Codes",
      "userType" : "GUEST",
      "deviceTypes" : [
        "IOS"
      ],
      "emailConfirmed" : true,
      "appVersion" : "1.0.0",
      "profilePhotoStandardUrl" : null,
      "subscriptionRenewedDate" : null,
      "rating" : null,
      "identityVerified" : false,
      "firstName" : "Dan",
      "currentDevice" : "IOS",
      "image" : null,
      "mobile" : null,
      "mobileNumberConfirmed" : false,
      "subscriptionEndDate" : null,
      "subscriptionId" : null,
      "subscriptionStartDate" : null,
      "subscriptionType" : null,
      "favouriteArtiste" : null,
      "isPremium" : false,
      "hobby" : null,
      "favouriteMusicGenre" : null,
      "bioLanguage" : null,
      "modelString" : "User",
      "verifiedDOB" : null,
      "email" : "blah@blah.com",
      "middleName" : null,
      "dateOfBirth" : "2006-05-07Z",
      "id" : "74c8c408-1021-7018-39d7-70112d10afce",
      "staus" : "ACTIVE",
      "biographyTitle" : null,
      "modelStatus" : "ACTIVE",
      "timeZoneIdentifier" : "America\/Edmonton",
      "funFact" : null,
      "verifiedLastName" : null,
      "isConfirmed" : false,
      "version" : 1,
      "isSubscribed" : false,
      "profilePhotoThumbnailUrl" : null,
      "biography" : null,
      "bioAdress" : null
    }
  }
}
Starting execution for Auth.fetchSessionAPI
Starting execution
Check if authstate configured
Auth state configured
Fetching current state
Session exists, checking validity
Successfully completed execution for Auth.fetchSessionAPI with result:
{
    "AWS Credentials" = "[\"expiration\": 2024-05-08 00:36:00 +0000, \"secretAccessKey\": \"ro*****zG\", \"sessionToken\": \"IQ*****En\", \"accessKey\": \"AS*****ED\"]";
    cognitoTokens = "[\"expiry\": 2024-05-08 00:35:59 +0000, \"idToken\": \"ey*****Yg\", \"accessToken\": \"ey*****gQ\", \"refreshToken\": \"ey*****-g\"]";
    identityId = "us-ea*****fde9e";
    isSignedIn = true;
    userSub = "74c8c*****0afce";
}
Starting network task for mutation 3C303F34-16F7-46E9-AA58-362B7F0DD52B


GraphQLResponseError<User>: GraphQL service returned a successful response containing errors: [Amplify.GraphQLError(message: "Unauthorized on [pets, bioLanguage, rating, favouriteArtiste, biographyTitle, verifiedDOB, favouriteMusicGenre, profilePhotoThumbnailUrl, funFact, verifiedLastName, subscriptionRenewedDate, subscriptionStartDate, bioAdress, image, profilePhotoStandardUrl, subscriptionEndDate, mobile, biography, verifiedFirstName, favoutiteSong, favoutieShowAsAKid, subscriptionType, middleName, subscriptionId, hobby]", locations: Optional([Amplify.GraphQLError.Location(line: 2, column: 3)]), path: Optional([Amplify.JSONValue.string("updateUser")]), extensions: Optional(["errorInfo": Amplify.JSONValue.null, "data": Amplify.JSONValue.null, "errorType": Amplify.JSONValue.string("Unauthorized")]))]
Recovery suggestion: The list of `GraphQLError` contains service-specific messages

@dan-codes1
Copy link
Author

Any updates on this?

@lawmicha
Copy link
Member

lawmicha commented May 9, 2024

I tried to reproduce this with a very simplified schema of just the User model and I was able to create and update the User, the app user is signed in of course. Could you show us the full schema, including the Listing Model (it is missing in your comment above), or can also provide it to us through amplify diagnose --send-report (https://docs.amplify.aws/gen1/javascript/tools/cli/reference/diagnose/) ?

@lawmicha
Copy link
Member

lawmicha commented May 9, 2024

I used a very simply schema, but kept your auth rules

type User @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
    id: ID!
    firstName: String!
}

If you make a mutation request, and put a breakpoint at finalRequest in https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift#L80 you should then be able to run po finalRequest to see the HTTP headers

This is what mine looks like, we are expecting he header Authoriation with token value.

(lldb) po finalRequest
▿ Result<URLRequest, APIError>
  ▿ success : https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql
    ▿ url : Optional<URL>
      ▿ some : https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql
        - _url : https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql
    - cachePolicy : 0
    - timeoutInterval : 60.0
    - mainDocumentURL : nil
    - networkServiceType : __C.NSURLRequestNetworkServiceType
    - allowsCellularAccess : true
    ▿ httpMethod : Optional<String>
      - some : "POST"
    ▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
      ▿ some : 5 elements
        ▿ 0 : 2 elements
          - key : "Cache-Control"
          - value : "no-store"
        ▿ 1 : 2 elements
          - key : "Authorization"
          - value : "xxxxxxxxhyME55eE43Mm1iek5mdGI1WlRZYThweTJnPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJmNGI4YTQ3OC1hMGMxLTcwOGMtMGM4ZC0xNjE5YTdlN2FkMjAiLCxxxxxxxxxxxxxiOiI3ODN1djUwdGh2YzQ1OHBlODI0cWRibWxybiIsIm9yaWdpbl9qdGkiOiJkNDJjNxxxxxtYWM2NxxxxxxudF9pZCI6IjMxYjJiZmNkLTRkOGEtNGEwNi1hZmNkLTQ3ZDg2YzQ1ZGRjZiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE3MTUyODU5MjksImV4cCI6MTcxNTI4OTUyOSwixxxxxtNpHkdtobNhJI4f5rK2VjlhhfyIYsxIYls8YYO97LwDl7cJbtcKsWluCOzHL3JrRwv5M3ncyu7M6opxpTPxzbqogPvECEn8jHrv3f55a9isP3f5PZMytdlF1rDXFNIMP5QrFletOABNGQZJH_-LpTM0I7lQ0fDx09VrEmpIcA6m_v2OKkIpxsi02xgOCom5rLMOQ0Aflnk9XKJ2w9V72sIqJ1ImurQ1i5Q"
        ▿ 2 : 2 elements
          - key : "X-Amz-Date"
          - value : "20240509T163316Z"
        ▿ 3 : 2 elements
          - key : "Content-Type"
          - value : "application/json"
        ▿ 4 : 2 elements
          - key : "User-Agent"
          - value : "lib/amplify-swift#2.33.4"
    ▿ httpBody : Optional<Data>
      ▿ some : 219 bytes
        - count : 219
        ▿ pointer : 0x0000000109832000
          - pointerValue : 4454555648
    - httpBodyStream : nil
    - httpShouldHandleCookies : true
    - httpShouldUsePipelining : false

Also if you want to confirm directly with AppSync to narrow out whether there's a bug on the client side or an issue with your backend, you can navigate to the AppSync console and replay your request with a Cognito User Pool user signed in.

@dan-codes1
Copy link
Author

dan-codes1 commented May 9, 2024

When I create the mutate request, it seems that the final request is being called twice. The first time it uses an api key header and the second time it uses the Authorization header. This is the output:

(lldb) po finalRequest
▿ Result<URLRequest, APIError>
▿ success : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
▿ url : Optional
▿ some : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
- _url : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
- cachePolicy : 0
- timeoutInterval : 60.0
- mainDocumentURL : nil
- networkServiceType : __C.NSURLRequestNetworkServiceType
- allowsCellularAccess : true
▿ httpMethod : Optional
- some : "POST"
▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
▿ some : 4 elements
▿ 0 : 2 elements
- key : "x-api-key"
- value : "da2-ezxxxxxxxxxxxxxxxxx" (redacted)
▿ 1 : 2 elements
- key : "Content-Type"
- value : "application/json"
▿ 2 : 2 elements
- key : "User-Agent"
- value : "lib/amplify-swift#2.33.3"
▿ 3 : 2 elements
- key : "Cache-Control"
- value : "no-store"
▿ httpBody : Optional
▿ some : 1083 bytes
- count : 1083
▿ pointer : 0x000000012700be00
- pointerValue : 4949327360
- httpBodyStream : nil
- httpShouldHandleCookies : true
- httpShouldUsePipelining : false

(lldb) po finalRequest
▿ Result<URLRequest, APIError>
▿ success : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
▿ url : Optional
▿ some : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
- _url : https://a37t7we2ovennolx5jyaoehvqu.appsync-api.us-east-1.amazonaws.com/graphql
- cachePolicy : 0
- timeoutInterval : 60.0
- mainDocumentURL : nil
- networkServiceType : __C.NSURLRequestNetworkServiceType
- allowsCellularAccess : true
▿ httpMethod : Optional
- some : "POST"
▿ allHTTPHeaderFields : Optional<Dictionary<String, String>>
▿ some : 5 elements
▿ 0 : 2 elements
- key : "Authorization"
- value : "eyJraWQiOiJneUt0Mxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" (redacted)
▿ 1 : 2 elements
- key : "X-Amz-Date"
- value : "20240509T164002Z"
▿ 2 : 2 elements
- key : "User-Agent"
- value : "lib/amplify-swift#2.33.3"
▿ 3 : 2 elements
- key : "Cache-Control"
- value : "no-store"
▿ 4 : 2 elements
- key : "Content-Type"
- value : "application/json"
▿ httpBody : Optional
▿ some : 2125 bytes
- count : 2125
▿ pointer : 0x000000012702a400
- pointerValue : 4949451776
- httpBodyStream : nil
- httpShouldHandleCookies : true
- httpShouldUsePipelining : false

I ran po request on: let request = try await Amplify.API.mutate(request: .update(user, authMode: .amazonCognitoUserPools)). This is the result

(lldb) po request
▿ Result<User, GraphQLResponseError>
▿ failure : GraphQLResponseError: GraphQL service returned a successful response containing errors: [Amplify.GraphQLError(message: "Unauthorized on [pets, bioLanguage, rating, favouriteArtiste, biographyTitle, verifiedDOB, favouriteMusicGenre, profilePhotoThumbnailUrl, funFact, verifiedLastName, subscriptionRenewedDate, subscriptionStartDate, bioAdress, image, profilePhotoStandardUrl, subscriptionEndDate, mobile, biography, verifiedFirstName, favoutiteSong, favoutieShowAsAKid, subscriptionType, middleName, subscriptionId, hobby]", locations: Optional([Amplify.GraphQLError.Location(line: 2, column: 3)]), path: Optional([Amplify.JSONValue.string("updateUser")]), extensions: Optional(["errorInfo": Amplify.JSONValue.null, "data": Amplify.JSONValue.null, "errorType": Amplify.JSONValue.string("Unauthorized")]))]
Recovery suggestion: The list of GraphQLError contains service-specific messages
▿ error : 1 element
▿ 0 : GraphQLError
- message : "Unauthorized on [pets, bioLanguage, rating, favouriteArtiste, biographyTitle, verifiedDOB, favouriteMusicGenre, profilePhotoThumbnailUrl, funFact, verifiedLastName, subscriptionRenewedDate, subscriptionStartDate, bioAdress, image, profilePhotoStandardUrl, subscriptionEndDate, mobile, biography, verifiedFirstName, favoutiteSong, favoutieShowAsAKid, subscriptionType, middleName, subscriptionId, hobby]"
▿ locations : Optional<Array>
▿ some : 1 element
▿ 0 : Location
- line : 2
- column : 3
▿ path : Optional<Array>
▿ some : 1 element
▿ 0 : JSONValue
- string : "updateUser"
▿ extensions : Optional<Dictionary<String, JSONValue>>
▿ some : 3 elements
▿ 0 : 2 elements
- key : "errorInfo"
- value : Amplify.JSONValue.null
▿ 1 : 2 elements
- key : "data"
- value : Amplify.JSONValue.null
▿ 2 : 2 elements
- key : "errorType"
▿ value : JSONValue
- string : "Unauthorized"

This is the Listing schema

type Listing @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
id: ID!
publicId: String!
userId: String!
title: String!
description: String!
beds: Int!
bedroom: Int!
bathroom: Float!
placeType: PlaceType!
checkinInstruction: String
checkoutInstruction: String
availabilityIsFlexible: Boolean!
price: Float!
checkInItems: [CheckInItem] @hasmany
checkOutItems: [CheckOutItem] @hasmany
images: [ListingImage!] @hasmany(indexName: "byListing", fields: ["id"])
rating: Float
customRule: String
unavailableDates: [AWSDate!]
unavailableDatesFromBookings: [UnavailableDate] @hasmany
coverImage: ListingImage @hasone
latitude: Float!
longitude: Float!
status: ListingStatus!
adressLabel: String!
addressNumber: String!
street: String!
municipality: String!
neighbourhood: String!
region: String!
subRegion: String!
postalCode: String!
country: String!
host: User! @belongsTo
modelString: String!
bookings: [Booking!] @hasmany
modelStatus: ModelStatus!
version: Int!
}

type CheckInItem @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
id: ID!
index: Int!
title: String
message: String
modelStatus: ModelStatus!
listing: Listing! @belongsTo
version: Int!
}

type CheckOutItem @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
id: ID!
index: Int!
title: String
message: String
modelStatus: ModelStatus!
listing: Listing! @belongsTo
version: Int!
}

type ListingImage @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
id: ID!
listingID: ID! @Index(name: "byListing", sortKeyFields: ["index"])
type: ImageType!
s3Key: String!
index: Int!
listing: Listing! @belongsTo(fields: ["listingID"])
modelStatus: ModelStatus!
modelString: String!
version: Int!
}

type Booking @model @auth(rules: [{allow: private, operations: [read, update]}, {allow: owner, operations: [create, update]}]) {
id: ID!
listing: Listing! @hasone
guest: User! @hasone
host: User! @hasone
created: AWSDateTime!
status: BookingStatus!
statusDateChange: AWSDateTime
acceptanceDate: AWSDateTime
guestCancellationDate: AWSDateTime
hostCancellationDate: AWSDateTime
rejectiondDate: AWSDateTime
paymentDate: AWSDateTime
lastActionBy: UserType!
startDate: AWSDate!
endDate: AWSDate!
listingTitle: String!
unavailableDate: UnavailableDate @hasone
paymentStatus: PaymentStatus
priceBooked: Float!
pricePerNight: Float!
currencyCode: CurrencyCode!
numberOfNights: Int!
totalPaymentAmount: Float
confirmationDate: AWSDateTime
amountPaid: Float
amountRemaining: Float
modelStatus: ModelStatus!
paymentPlatform: PaymentPlatform
modelString: String! @Index(name: "bookingsByDate", queryField: "bookingsByDate", sortKeyFields: ["created"])
version: Int!
}

@lawmicha
Copy link
Member

The block of code within mainAsync() is executed when AWSGraphQLOperation in the operation queue is picked up by a thread. Each Amplify.API.mutate call will add one AWSGraphQLOperation to the queue. It seems unlikely line 80 will get called twice for a single request. Can you make the request again and gather more detail such as logs and more breakpoints?

  1. Can you show us the amplifyconfiguration.json file (redacted)? This will show us what the default auth is configured as. It will let us determine which API Amplify.API.mutate will use when no apiName is provided. It will infer the only GraphQL endpoint in the configuration.
  2. Do you have any special custom interceptors added to the AWSAPIPlugin instance?
  3. Can you show us the code you are making the request with and how are you executing it? Are you testing it through a click of a button or a unit test?
  4. Can you execute it again, with a break point here: https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin%2BGraphQLBehavior.swift#L46 This is the entry point into AWSAPIPlugin from calling Amplify.API.mutate. Does this get called once or twice?
  5. The execution of the operation has a log with identifier here https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift#L41 You can set Amplify.Logging.logLevel = .verbose before Amplify.configure() to see log line. Can you debug into this, does it break once or twice? Do you see one or two logs with different identifiers?
  6. Within this operation, you should be able to also determine whether getEndpointInterceptors() is being called once or twice, and it should also be going into the if let authType = request.authMode as? AWSAUthorizationType block.

@dan-codes1
Copy link
Author

dan-codes1 commented May 11, 2024

Yes, you're right; the request is called only once.

{
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "cleuviinitgraphqlapi": {
                    "endpointType": "GraphQL",
                    "endpoint": "https://xxxxxx-api.us-east-1.amazonaws.com/graphql",
                    "region": "us-east-1",
                    "authorizationType": "API_KEY",
                    "apiKey": "da2-xxxxxxx"
                }
            }
        }
    },
    "auth": {
        "plugins": {
            "awsCognitoAuthPlugin": {
                "UserAgent": "aws-amplify/cli",
                "Version": "0.1.0",
                "IdentityManager": {
                    "Default": {}
                },
                "AppSync": {
                    "Default": {
                        "ApiUrl": "https://xxxxxxx-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "API_KEY",
                        "ApiKey": "da2-xxxxxxx",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_API_KEY"
                    },
                    "cleuviinitgraphqlapi_AMAZON_COGNITO_USER_POOLS": {
                        "ApiUrl": "https://mtxxxxxxu.appsync-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "AMAZON_COGNITO_USER_POOLS",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_AMAZON_COGNITO_USER_POOLS"
                    },
                    "cleuviinitgraphqlapi_AWS_IAM": {
                        "ApiUrl": "https://mxxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "AWS_IAM",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_AWS_IAM"
                    }
                },
                "CredentialsProvider": {
                    "CognitoIdentity": {
                        "Default": {
                            "PoolId": "us-east-1:b2bb446a-bxxxxx",
                            "Region": "us-east-1"
                        }
                    }
                },
                "CognitoUserPool": {
                    "Default": {
                        "PoolId": "us-east-xx",
                        "AppClientId": "xxxxxxx",
                        "Region": "us-east-1"
                    }
                },
                "Auth": {
                    "Default": {
                        "authenticationFlowType": "USER_SRP_AUTH",
                        "socialProviders": [],
                        "usernameAttributes": [
                            "EMAIL"
                        ],
                        "signupAttributes": [
                            "EMAIL",
                            "NAME"
                        ],
                        "passwordProtectionSettings": {
                            "passwordPolicyMinLength": 8,
                            "passwordPolicyCharacters": []
                        },
                        "mfaConfiguration": "OPTIONAL",
                        "mfaTypes": [
                            "SMS",
                            "TOTP"
                        ],
                        "verificationMechanisms": [
                            "EMAIL"
                        ]
                    }
                }
            }
        }
    },
    "geo": {
        "plugins": {
            "awsLocationGeoPlugin": {
                "region": "us-east-1",
                "searchIndices": {
                    "items": [
                        "cleuviinitplaceindex-init"
                    ],
                    "default": "cleuviinitplaceindex-init"
                }
            }
        }
    }
}
{
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api": {
        "plugins": {
            "awsAPIPlugin": {
                "cleuviinitgraphqlapi": {
                    "endpointType": "GraphQL",
                    "endpoint": "https://xxxxxxxxx,apsync-api.us-east-1.amazonaws.com/graphql",
                    "region": "us-east-1",
                    "authorizationType": "API_KEY",
                    "apiKey": "da2-xxxxxxxxxxxx"
                }
            }
        }
    },
    "auth": {
        "plugins": {
            "awsCognitoAuthPlugin": {
                "UserAgent": "aws-amplify/cli",
                "Version": "0.1.0",
                "IdentityManager": {
                    "Default": {}
                },
                "AppSync": {
                    "Default": {
                        "ApiUrl": "https://mtdxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "API_KEY",
                        "ApiKey": "da2-xxxxxxxx",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_API_KEY"
                    },
                    "cleuviinitgraphqlapi_AMAZON_COGNITO_USER_POOLS": {
                        "ApiUrl": "https://xxxxxxxxxxxhzx4u.appsync-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "AMAZON_COGNITO_USER_POOLS",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_AMAZON_COGNITO_USER_POOLS"
                    },
                    "cleuviinitgraphqlapi_AWS_IAM": {
                        "ApiUrl": "https://xxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql",
                        "Region": "us-east-1",
                        "AuthMode": "AWS_IAM",
                        "ClientDatabasePrefix": "cleuviinitgraphqlapi_AWS_IAM"
                    }
                },
                "CredentialsProvider": {
                    "CognitoIdentity": {
                        "Default": {
                            "PoolId": "us-east-1:xxxxxxxxxxxb",
                            "Region": "us-east-1"
                        }
                    }
                },
                "CognitoUserPool": {
                    "Default": {
                        "PoolId": "us-east-xxxxxxxx",
                        "AppClientId": "57daxxxxxxxxxxxxxxh7",
                        "Region": "us-east-1"
                    }
                },
                "Auth": {
                    "Default": {
                        "authenticationFlowType": "USER_SRP_AUTH",
                        "socialProviders": [],
                        "usernameAttributes": [
                            "EMAIL"
                        ],
                        "signupAttributes": [
                            "EMAIL",
                            "NAME"
                        ],
                        "passwordProtectionSettings": {
                            "passwordPolicyMinLength": 8,
                            "passwordPolicyCharacters": []
                        },
                        "mfaConfiguration": "OPTIONAL",
                        "mfaTypes": [
                            "SMS",
                            "TOTP"
                        ],
                        "verificationMechanisms": [
                            "EMAIL"
                        ]
                    }
                }
            }
        }
    },
    "geo": {
        "plugins": {
            "awsLocationGeoPlugin": {
                "region": "us-east-1",
                "searchIndices": {
                    "items": [
                        "cleuviinitplaceindex-init"
                    ],
                    "default": "cleuviinitplaceindex-init"
                }
            }
        }
    }
}
  1. No, I dont have any special custom interceptors. However, I am using imported user pool and identity pool.

  2. let request = try await Amplify.API.mutate(request: .update(user, authMode: .amazonCognitoUserPools))
    let user = try request.get()

  3. I ran po request at https://github.com/aws-amplify/amplify-swift/blob/main/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift#L41 and it returned a request without an api name but with the cognito authorization type at the end:

(lldb) po request
▿ GraphQLRequest

  • apiName : nil
  • document : "mutation UpdateUser($input: UpdateUserInput!) {\n updateUser(input: $input) {\n id\n appVersion\n bioAdress\n bioLanguage\n biography\n biographyTitle\n countryCode\n createdAt\n currentDevice\n dateOfBirth\n deviceTypes\n email\n emailConfirmed\n favouriteArtiste\n favouriteMusicGenre\n favoutieShowAsAKid\n favoutiteSong\n firstName\n funFact\n hobby\n identityVerified\n image {\n id\n index\n s3Key\n __typename\n }\n isConfirmed\n isPremium\n isSubscribed\n lastName\n middleName\n mobile\n mobileNumberConfirmed\n modelStatus\n modelString\n pets\n profilePhotoStandardUrl\n profilePhotoThumbnailUrl\n rating\n staus\n subscriptionEndDate\n subscriptionId\n subscriptionRenewedDate\n subscriptionStartDate\n subscriptionType\n timeZone\n timeZoneIdentifier\n updatedAt\n userType\n username\n verifiedDOB\n verifiedFirstName\n verifiedLastName\n version\n __typename\n }\n}"
    ▿ variables : Optional<Dictionary<String, Any>>
    ▿ some : 1 element
    ▿ 0 : 2 elements
    - key : "input"
    ▿ value : 48 elements
    ▿ 0 : 2 elements
    - key : "verifiedLastName"
    - value : nil
    ▿ 1 : 2 elements
    - key : "favoutiteSong"
    - value : nil
    ▿ 2 : 2 elements
    - key : "firstName"
    ▿ value : Optional
    - some : "Dan"
    ▿ 3 : 2 elements
    - key : "username"
    - value : nil
    ▿ 4 : 2 elements
    - key : "currentDevice"
    ▿ value : Optional
    - some : "IOS"
    ▿ 5 : 2 elements
    - key : "isPremium"
    ▿ value : Optional
    - some : false
    ▿ 6 : 2 elements
    - key : "countryCode"
    ▿ value : Optional
    ▿ some : Optional
    - some : "CA"
    ▿ 7 : 2 elements
    - key : "deviceTypes"
    ▿ value : Optional
    ▿ some : 2 elements
    - 0 : IOS
    - 1 : IOS
    ▿ 8 : 2 elements
    - key : "favoutieShowAsAKid"
    - value : nil
    ▿ 9 : 2 elements
    - key : "biographyTitle"
    - value : nil
    ▿ 10 : 2 elements
    - key : "biography"
    - value : nil
    ▿ 11 : 2 elements
    - key : "lastName"
    ▿ value : Optional
    - some : "Codes"
    ▿ 12 : 2 elements
    - key : "mobile"
    - value : nil
    ▿ 13 : 2 elements
    - key : "mobileNumberConfirmed"
    ▿ value : Optional
    - some : false
    ▿ 14 : 2 elements
    - key : "pets"
    - value : nil
    ▿ 15 : 2 elements
    - key : "timeZone"
    ▿ value : Optional
    - some : "MDT"
    ▿ 16 : 2 elements
    - key : "bioAdress"
    - value : nil
    ▿ 17 : 2 elements
    - key : "id"
    ▿ value : Optional
    - some : "xxxxx-a061-xxxxx-xxxxxx-xxxxxxxx"
    ▿ 18 : 2 elements
    - key : "subscriptionId"
    - value : nil
    ▿ 19 : 2 elements
    - key : "userType"
    ▿ value : Optional
    - some : "GUEST"
    ▿ 20 : 2 elements
    - key : "verifiedFirstName"
    - value : nil
    ▿ 21 : 2 elements
    - key : "image"
    - value : nil
    ▿ 22 : 2 elements
    - key : "appVersion"
    ▿ value : Optional
    - some : "1.0.0"
    ▿ 23 : 2 elements
    - key : "modelStatus"
    ▿ value : Optional
    - some : "ACTIVE"
    ▿ 24 : 2 elements
    - key : "isSubscribed"
    ▿ value : Optional
    - some : false
    ▿ 25 : 2 elements
    - key : "identityVerified"
    ▿ value : Optional
    - some : false
    ▿ 26 : 2 elements
    - key : "modelString"
    ▿ value : Optional
    - some : "User"
    ▿ 27 : 2 elements
    - key : "profilePhotoStandardUrl"
    - value : nil
    ▿ 28 : 2 elements
    - key : "subscriptionEndDate"
    - value : nil
    ▿ 29 : 2 elements
    - key : "subscriptionType"
    - value : nil
    ▿ 30 : 2 elements
    - key : "emailConfirmed"
    ▿ value : Optional
    - some : false
    ▿ 31 : 2 elements
    - key : "favouriteMusicGenre"
    - value : nil
    ▿ 32 : 2 elements
    - key : "dateOfBirth"
    ▿ value : Optional
    - some : "2006-05-11Z"
    ▿ 33 : 2 elements
    - key : "profilePhotoThumbnailUrl"
    - value : nil
    ▿ 34 : 2 elements
    - key : "subscriptionRenewedDate"
    - value : nil
    ▿ 35 : 2 elements
    - key : "hobby"
    - value : nil
    ▿ 36 : 2 elements
    - key : "verifiedDOB"
    - value : nil
    ▿ 37 : 2 elements
    - key : "funFact"
    - value : nil
    ▿ 38 : 2 elements
    - key : "staus"
    ▿ value : Optional
    - some : "ACTIVE"
    ▿ 39 : 2 elements
    - key : "middleName"
    - value : nil
    ▿ 40 : 2 elements
    - key : "version"
    ▿ value : Optional
    - some : 1
    ▿ 41 : 2 elements
    - key : "bioLanguage"
    - value : nil
    ▿ 42 : 2 elements
    - key : "rating"
    - value : nil
    ▿ 43 : 2 elements
    - key : "email"
    ▿ value : Optional
    - some : "xxxxx@gmail.com"
    ▿ 44 : 2 elements
    - key : "timeZoneIdentifier"
    ▿ value : Optional
    - some : "America/Edmonton"
    ▿ 45 : 2 elements
    - key : "isConfirmed"
    ▿ value : Optional
    - some : false
    ▿ 46 : 2 elements
    - key : "subscriptionStartDate"
    - value : nil
    ▿ 47 : 2 elements
    - key : "favouriteArtiste"
    - value : nil
  • responseType : Xploree.User
    ▿ authMode : Optional
    • some : AWSPluginsCore.AWSAuthorizationType.amazonCognitoUserPools
      ▿ decodePath : Optional
    • some : "updateUser"
  • options : nil

@dan-codes1
Copy link
Author

Just to add a few things:

  1. The behaviour of the api is inconsistent; e.g. it allows me to create a user but not update it, it doesnt allow me to create a listing and so on.
  2. This error now happens to happen even when I use the old mutation api, entering the api name instead of the authType.
  3. All these operations work on the app sync console when I try it but not on the mobile client so this is most likely because of some client configurations.
  4. I tried downgrading my amplify-swift version but the error persists. Everything was working before I upgraded and tried the new apis.

@lawmicha
Copy link
Member

Hi @dan-codes1, thank you for providing more details. I was having troubles getting the schema from your comment. Could you show us the full schema? If you run amplify diagnose --send-report (https://docs.amplify.aws/gen1/javascript/tools/cli/reference/diagnose/), I can grab the schema file and review some of the configuration in the Amplify CLI project.

Could you also confirm in the Amplify CLI project, in cli.json you do have generateModelsForLazyLoadAndCustomSelectionSet enabled?

I have a hunch that there may be two issues related to why there may be unauthorized responses.

  1. When we load the model (the first level query- get[Model] operation), we actually contruct the nested selection set with only the primary keys of the nested model. If the user doesn't have acces to the nested model, this may come back as unauthorized for the entire response for query.
  2. When performing lazy loading, it uses the metadata about that model; the identifiers retrieved during the first query, the optional api name (to use the right endpoint), to make another request to AppSync. I suspect that lazy loading is using the default auth type of API Key since I don't recall passing the authMode parameter down to the lazy loading metadata. This is a bug i'm looking into right now.

If you can provide the schema file that I can use directly in my sample app (the diagnose report will provide the same), and any more sequences of API calls that you can share. I do see the one you've shared already from the description of this issue:

let request = try await Amplify.API.query(request: .get(Message.self, byId: id, authMode: .amazonCognitoUserPools))
let message = try request.get() 
let sender = try await message.sender
let space = try await message.space

This is a good example that we'll try and get back to you. In my original repro steps, I only tried creating an updating the User without the full schema containing all the connected models

@lawmicha
Copy link
Member

lawmicha commented May 13, 2024

Here's a branch you can try out fix.lazy-load-auth-mode. It contains fixes that propagate the authMode down to the metadata used for loading the connected models, for example

let request = try await Amplify.API.query(request: .get(Message.self, byId: id, authMode: .amazonCognitoUserPools))
let message = try request.get() 
let sender = try await message.sender  // with fix, uses user pool auth. without fix, uses default API key auth from config
let space = try await message.space // with fix, uses user pool auth. without fix, uses default API key auth from config

#3690

@dan-codes1
Copy link
Author

dan-codes1 commented May 13, 2024

Hello,

I have sent the schema via the diagnose command. Report saved: /var/folders/bg/3xhpqh1d6s16yg6wjqqsn3m80000gn/T/cleuviinit/report-1715640147663.zip

Yes, I have generateModelsForLazyLoadAndCustomSelectionSet enabled

I will try out the bug fix and give feedback.

@lawmicha
Copy link
Member

lawmicha commented May 14, 2024

Do you have the project identifier? it should be in the log output of the diagnose command. I ran the command and noticed it is different from the value in the path, ie.

✅ Report saved: /var/folders/gr/xxx/T/zzz/report-1715691794532.zip

✔ Done

Project Identifier: yyy

xxx is different from yyy

We use the project identifier to pull in the project

@dan-codes1
Copy link
Author

Project ID = 334a9b8600e19b2e4377d45122ca8871

@lawmicha
Copy link
Member

lawmicha commented May 16, 2024

Hi @dan-codes1, I was able to try out your schema and reproduce to update User issue. Here's a simplification of the schema

type User2 @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update]}]) {
    id: ID!
    requiredName: String!
    optionalName: String
} 

Semd createUser2 request, with the user pool user signed in ("michael" user)

mutation CreateUser {
  createUser2(input: {requiredName: "1", optionalName: "2"}) {
    createdAt
    optionalName
    id
    owner
    requiredName
    updatedAt
  }
}

Response

{
  "data": {
    "createUser2": {
      "createdAt": "2024-05-16T16:01:13.753Z",
      "optionalName": "2",
      "id": "a74354c6-afa4-4f8f-8861-79b2f25cd9d7",
      "owner": "michael",
      "requiredName": "1",
      "updatedAt": "2024-05-16T16:01:13.753Z"
    }
  }
}

Let's say now I want to remove the value set for optionalName. Perform an update on the user with optionalName set to null.

mutation UpdateOptionalFieldToNull {
  updateUser2(input: {id: "a74354c6-afa4-4f8f-8861-79b2f25cd9d7", optionalName: null}) {
    createdAt
    id
    optionalName
    owner
    requiredName
    updatedAt
  }
}

Response

{
  "data": {
    "updateUser2": null
  },
  "errors": [
    {
      "path": [
        "updateUser2"
      ],
      "data": null,
      "errorType": "Unauthorized",
      "errorInfo": null,
      "locations": [
        {
          "line": 29,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Unauthorized on [optionalName]"
    }
  ]
}

The issue is that I cannot update the user to remove an optional string value, whether it was originally set to something or not.

@lawmicha
Copy link
Member

The call site on Swift fails because the optional fields are set to Null on update mutations. Because Swift only has a value or nil, the library doesn't know the intention of the caller whether a value is being removed or not. So for example, when the User has the optional field created with nil, the create mutation will omit the field from the GraphQL request input. When the User has the optional field created with some value, it will pass the value in the GraphQL request.

On Updates, if the optional field is nil, we always send Null for the field in the GraphQL request since we don't know if it was originally set to some value and is now being removed.

I opened an issue over in aws-amplify/amplify-category-api#2562 to track this specific issue.

A workaround would be to remove the nil input values from the request before sending it

let request = GraphQLRequest<User>.update(user, authMode: .amazonCognitoUserPools)
// Remove values from `request.variables` which have keys but value is `nil`
let updatedUser = try await Amplify.API.mutate(
    request: request).get()

Regarding lazy loading with the original authMode, ie. #3677 (comment) This issue is separate since this is a bug with authMode not being propagated to the lazy loading functionality

@dan-codes1
Copy link
Author

dan-codes1 commented May 16, 2024

Hello,

Just tried this out, it works!

Thanks for helping out. Just to be informed, when is there likely to be a fix for this?

@lawmicha
Copy link
Member

Hi @dan-codes1, please track this issue for library fixes such as #3690 . For updates on the backend functionality issues please check with aws-amplify/amplify-category-api#2562 They may have more details on the status and any data modeling workarounds for your use case.

@lawmicha
Copy link
Member

I tested the mutations again with an updated owner auth rule to include delete. So the auth rule for owner auth is {allow: owner, operations: [create, update, delete]}

type User2 @model @auth(rules: [{allow: public, operations: [read]}, {allow: private, operations: [read]}, {allow: owner, operations: [create, update, delete]}]) {
    id: ID!
    requiredName: String!
    optionalName: String
} 

I was able to set the optional fields to null. This seems to be the access provided by the given "delete" operation. Would this work for your use cases? Only the owner of the model object that was created is able to delete it, including setting fields to null.

@lawmicha
Copy link
Member

#3690 has been merged to main and will most likely be released next week

@dan-codes1
Copy link
Author

Thanks for the updates!

As for the delete operation, we don't delete our models on the amplify client, we just tag the models as DELETED and hanlde the deletion in backend, so I might not be able to thoroughly verify that.

@lawmicha lawmicha added pending-release Code has been merged but pending release Code has been merged but pending release and removed pending-triage Issue is pending triage labels May 21, 2024
@phantumcode
Copy link
Member

Issue fixed and released in version 2.34.0

@github-actions github-actions bot removed the pending-release Code has been merged but pending release Code has been merged but pending release label May 23, 2024
Copy link

This issue is now closed. Comments on closed issues are hard for our team to see.
If you need more assistance, please open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@lawmicha
Copy link
Member

Hi @dan-codes1, please see the latest discussion on delete access over in aws-amplify/amplify-category-api#2562 (comment) . The Amplify Swift library, Amplify.API.mutate(request: .update(model)) will set null for nil fields for this update mutation request, which requires delete access on the field.

If you do not want to add delete access, you can customize the document and variables of the GraphQL request so that null is not passed for the updates.

@dan-codes1
Copy link
Author

Okay I understand the issue. Thank you! @lawmicha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Issues related to the API category bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants