aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.42k stars 2.13k forks source link

Enabling Device Tracking from Cognito User Pool sets `token_use` on SSR auth call to `access` instead of `id` #10819

Open asp3 opened 1 year ago

asp3 commented 1 year ago

Before opening, please confirm:

JavaScript Framework

React, Next.js

Amplify APIs

Authentication

Amplify Categories

auth

Environment information

    @aws-amplify/api: ^5.0.7 => 5.0.7 
    @aws-amplify/auth: ^5.1.1 => 5.1.1 
    @aws-amplify/core: ^5.0.7 => 5.0.7 
    @aws-amplify/storage: ^5.0.7 => 5.0.7 

Describe the bug

IDENTITY when called from ssr.

identity: {
    claims: {
      sub: '82328429-dbb9-46d9-b8eb-0ebd238e221c',
      device_key: 'us-east-1_e2605136-8c55-406b-914c-baaf706c935c',
      token_use: 'access',
      scope: 'aws.cognito.signin.user.admin',
      auth_time: 1672183360,
      iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_pj3FdFG6C',
      exp: 1672187757,
      iat: 1672184157,
      jti: 'd00deefd-5149-44fe-9e84-923a2526d3fe',
      client_id: 'linov2orokihpnfsq2aqqugsc',
      username: 'kstudent12@-@knowt.io'
    },
    defaultAuthStrategy: 'ALLOW',
    groups: null,
    issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_pj3FdFG6C',
    sourceIp: [ '100.37.153.113' ],
    sub: '82328429-dbb9-46d9-b8eb-0ebd238e221c',
    username: 'kstudent12@-@knowt.io'
  },

IDENTITY from client side call.

identity: {
    claims: {
      birthday: '2002-9-13',
      sub: '82328429-dbb9-46d9-b8eb-0ebd238e221c',
      referralcode: '94FE1F98',
      timezone: 'america/los_angeles',
      rating: '5',
      accounttype: 'Student',
      iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_pj3FdFG6C',
      cover: 'https://knowt-user-attachments.s3.amazonaws.com/847a780f1f494a56ab6be61628d60042.jpeg',
      auth_time: 1672183360,
      ID: '5f608122-f843-11ea-8356-4502370cf8a3',
      ...otherCustomClaimsFromPreToken
      'cognito:username': 'kstudent12@-@knowt.io',
      aud: 'linov2orokihpnfsq2aqqugsc',
      ratingcount: '1',
      token_use: 'id',
      lastnotified: '1649437274',
      grade: '2nd',
      name: 'Max',
      username: 'user12'
    },
    defaultAuthStrategy: 'ALLOW',
    groups: null,
    issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_pj3FdFG6C',
    sourceIp: [ '100.37.153.113' ],
    sub: '82328429-dbb9-46d9-b8eb-0ebd238e221c',
    username: 'kstudent12@-@knowt.io'
  },

The problem seems pretty straightforward, the token_use key is access instead of ID, which I also assume is the reason why the data from the PreTokenTrigger is not populated as well. We use custom fields (like a custom username field), which is why it is necessary to get the overridden fields from the PreToken generation.

Expected behavior

SSRContext should have the same auth information as the client side call.

Reproduction steps

set up amplify environment, and set up a pretoken trigger on the user pool that is used. Then, make client side and server side calls and see the differences in payload in appsync.

Code Snippet

The value of the identity is above, printed in lambda (called from AppSync)

Log output

``` // Put your logs below this line ```

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

nadetastic commented 1 year ago

Hi @asp3, thank you for opening this issue. I've taken a look at it and haven't yet reproduced this issue. However I have some follow up questions:

Here's is what is working for me as expected:

import { Amplify, Auth, withSSRContext } from 'aws-amplify';
import awsExports from "../src/aws-exports";
Amplify.configure({ ...awsExports, ssr: true });

...

export async function getServerSideProps(ctx){
    const SSR = withSSRContext(ctx);
    try {
        const session = await SSR.Auth.currentSession();
        return {
            props: { msg: session.idToken.payload }
        }
    } catch (e) {
        console.log("ERROR", e);
        return {
            props: { msg: e }
        }
    }
}

...

With the above, I am able to get my custom claims in my props including token_use: 'id'. Are you doing this differently from the above? Any additional context will be helpful here.

asp3 commented 1 year ago

Hi @nadetastic thank you for the response!

That code snippet works for me as well, and I am able to see the custom claims in my next app as well. However, the issue I have is with appsync graphql calls, the claims there seem to only include the access token

const { API, Auth } = withSSRContext(context);
    const session = await Auth.currentSession();
    console.log("session", session.idToken.payload); // Works as expected
    const userId = await fetchUserId(Auth);
    const { textbookId, code } = context.query;

    let textbook = null;
    let serverUser = null;

    if (!userId && !code) {
        return {
            props: {
                textbook: null,
                serverUser: null,
            },
        };
    } else {
        // This call only has the access token payload
        serverUser = API.graphql({
                query: gql(getCurrentUser),
                variables: {
                    input: {
                        userId,
                    },
                },
            })
            .then(res => res?.data?.getCurrentUser)
            .catch(console.error);
    }

Thanks for the help!

nadetastic commented 1 year ago

Ok thanks you @asp3 for the additional context - to confirm, are you passing code and textbook in the query parameters of the URL?

asp3 commented 1 year ago

@nadetastic Yeah, the call hits our lambda code, which gets called from Appsync.

The appsync resolver,

getCurrentUser(input: GetCurrentUserInput): UserDetails!

which is connected to the lambda code that just prints the event passed in to the lambda handler, which is pasted above as event.identity.

So I assume that the withSSRContext API library works differently than the other one, somehow?

nadetastic commented 1 year ago

@asp3 I'd like to clarify my understanding of the flow, which I am assuming is the following:

  1. Client (Visit Next APP with Params in URL)
  2. getSSProps (Invoked and calls WithSSRContext().API.graphql)
  3. AppSync (Backend with Lambda resolver)

If this is correct, what part seems to have the issue?

I am also curious what const userId = await fetchUserId(Auth) does under the hood and if it successfully returns userId

asp3 commented 1 year ago

So fetchUserId is just a simple wrapper around the Auth library

export const fetchUserId = async (Auth): Promise<string> => {
    try {
        const userId = (await Auth.currentAuthenticatedUser()).signInUserSession.idToken.payload.ID;
        refreshToken(Auth);
        return userId;
    } catch {
        return null;
    }
};

It does successfully return, which means from SSRContext, we are able to get the idToken payload. However, the issue is that when we make the graphql call and the appsync is invoked, the payload is that of the accessToken, not the idToken. (printing the event from the lambda handler of the appsync query/mutation)

@nadetastic

asp3 commented 1 year ago

After doing some digging, I can see that the error happens here:

https://github.com/aws-amplify/amplify-js/blob/736918491e74d9aefb543fa4e531efe867195521/packages/api-graphql/src/GraphQLAPI.ts#L285

https://github.com/aws-amplify/amplify-js/blob/736918491e74d9aefb543fa4e531efe867195521/packages/api-graphql/src/GraphQLAPI.ts#L170-L177

However, when running the same function in my getServerSideProps call, Auth.currentSession().getAccessToken().getJwtToken(), it return the token successfully.

@nadetastic

asp3 commented 1 year ago

Also some other info that we found out:

The error that we were getting, No Current User, has been fixed! We figured out the issue happened since we had been destructuring the return from withSSRContext.

const {Auth, API} = withSSRContext(context)

...
API.graphql(....)
// FAILS - No Current User

Changed to

const SSR = withSSRContext(context)

SSR.API.graphql(...)
// NO error thrown

However, the issue stated at the beginning of the issue still stands, where the event in the parameters that is received in our lambda (called through appsync) does not contain the info of the authenticated user, since it only has the accessToken info, instead of the idToken info.

The only way I have found that fixes this issue so far is by changing https://github.com/aws-amplify/amplify-js/blob/main/packages/api-graphql/src/GraphQLAPI.ts#L174 to

- Authorization: session.getAccessToken().getJwtToken(),
+ Authorization: session.getIdToken().getJwtToken(),

After this patch, I can see the proper information in my lambda! I wonder if this is configurable through some parameter, and if this is a safe change to make. @nadetastic

asp3 commented 1 year ago

A bit more into this,

looks like turning off Device Tracking from Cognito User Pool Settings fixes this!

https://stackoverflow.com/questions/46879876/aws-cognito-invalid-refresh-token

Not sure why, but I turned on debug logging, and it seems that the Refresh Session did not work because of this, and works as expected after turning this off. Very weird! @nadetastic

nadetastic commented 1 year ago

Hi @asp3 a fix for SSR with API as discussed in https://github.com/aws-amplify/amplify-js/issues/10786 was released with https://github.com/aws-amplify/amplify-js/pull/10831

Could you test with 5.0.10 ?

nadetastic commented 1 year ago

Following up on this issue, I was able to reproduce with aws-amplify@5.0.10. Updating the title to better reflect the problem