3

I am trying to populate my jwtToken with the newer @aws-amplify packages and it is proving somewhat difficult.

When trying to run a Query I will get the following error: Uncaught (in promise) No current user

I can see from the source that if I have auth type set to AMAZON_COGNITO_USER_POOLS then I have to use a jwt token

        case AUTH_TYPE.AMAZON_COGNITO_USER_POOLS:
        case AUTH_TYPE.OPENID_CONNECT:
            const { jwtToken = '' } = auth;
            promise = headerBasedAuth({ header: 'Authorization', value: jwtToken }, operation, forward);

So this leads me to trying to generate my JWT token and this is where my knowledge fails me. I know that jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(), returns a promise as is required as seen in the above code... So I cannot see why this would fail?

_app.js (next.js)

import Amplify from '@aws-amplify/core'
import { Auth } from '@aws-amplify/auth'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloLink } from 'apollo-link'
import { createAuthLink } from 'aws-appsync-auth-link'
import { InMemoryCache, ApolloClient } from '@apollo/client'
import { createHttpLink } from 'apollo-link-http'

import awsExports from '../aws-exports'

Amplify.configure(awsExports)
Auth.configure(awsExports)

const url = awsExports.aws_appsync_graphqlEndpoint
const region = awsExports.aws_appsync_region

const auth = {
  type: awsExports.aws_appsync_authenticationType,
  jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken(),
}
const link = ApolloLink.from([createAuthLink({ url, region, auth }), createHttpLink({ uri: url })])
const client = new ApolloClient({
  link,
  cache: new InMemoryCache(),
})

const MyApp = function ({ Component, pageProps, router }) {
  return (
        .....
          <ApolloProvider client={client}>
        .....
  )
}

export default MyApp
Jamie Hutber
  • 26,790
  • 46
  • 179
  • 291

1 Answers1

4
jwtToken: async () => (await Auth.currentSession()).getIdToken().getJwtToken()

This is a partial correct implementation and would work just fine when the user is already logged in.

Auth.currentSession() fails with Uncaught (in promise) No current user when the user has not logged in.

The following would show the error in action

 Amplify.configure(awsExports);

 Auth.signOut()
    .then(_ => auth.jwtToken())
    .then(console.log)
    .catch(console.error);

The following is an example where you would get the token (Replace username and password in the code)

Amplify.configure(awsExports);

Auth.signIn(<username>, <password>)
    .then(_ => auth.jwtToken())
    .then(console.log)
    .catch(console.error);

The solution for this problem would be to ensure you ask for the token when the user is logged in or ensure handling the error gracefully.

Update:

If there are any public queries, I would say add the api-key based auth in addition to the Cognito auth for the AppSync GraphQL endpoint (Additional authorization providers in AppSync settings). In the example below, id and publicProperty can be accessed via somePublicQuery with the configured API Key

type Query {
  somePublicQuery:[SomeModel!]!
  @aws_api_key
}

type SomeModel {
  id: ID! @aws_api_key
  privateProperty: String!
  publicProperty: String! @aws_api_key
}

If I refer to the example pointed in the question, then this is what would change on the client side.

headerBasedAuth can take an Array of headers, one for the api-key, one for the Cognito token.

const headerBasedAuth = async ( authHeaders: Array<Headers> = [], operation, forward) => {
  const origContext = operation.getContext();
  let headers = {
    ...origContext.headers,
    [USER_AGENT_HEADER]: USER_AGENT,
  };

  for ( let authHeader of authHeaders) { // Handle the array of auth headers
    let { header, value } = authHeader;
    if (header && value) {
      const headerValue = typeof value === 'function' ? await value.call(undefined) : await value;

      headers = {
          ...{ [header]: headerValue },
          ...headers
      };
    }
  }

  operation.setContext({
      ...origContext,
      headers,
  });

  return forward(operation);

};

In the authLink function, instead of the switch statement, you would have some thing like this

const { apiKey = '', jwtToken = '' } = auth;
promise = headerBasedAuth([{ header: 'X-Api-Key', value: apiKey }, { header: 'Authorization', value: jwtToken }], operation, forward);

and finally the auth object would look something like this

const auth = {
  apiKey: awsExports.aws_appsync_apiKey, //Add apiKey to your aws-exports
  jwtToken: async () => {
      try {
        return (await Auth.currentSession()).getIdToken().getJwtToken()
      } catch (e) {
        console.error(e);
        return ""; // In case you don't get the token, hopefully that is a public api and that should work with the API Key alone.
      }
    }
}
GSSwain
  • 5,787
  • 2
  • 19
  • 24
  • Thanks GSSWain, I assume then, for queries that are public I make sure my roles are updated in my schema to be `role: public`? – Jamie Hutber Oct 02 '20 at 16:00
  • Thank you very much again @GSSWain! I appreciate the in depth and look forward to check this tomorrow :) I do have another generalised question about RDS MySQL db and app sync too if you're intetrested :D https://stackoverflow.com/questions/64176243/failed-to-start-api-mock-endpoint-error-cloudformation-stack-parameter-rdsregio – Jamie Hutber Oct 03 '20 at 21:30
  • This was very helpful, much appreciated! – mufasa Dec 24 '20 at 03:14