Amplify-js: Using Auth.currentCredentials() server side

Created on 1 Mar 2019  路  11Comments  路  Source: aws-amplify/amplify-js

Which Category is your question related to?

Using Auth.currentCredentials() server side.

What AWS Services are you utilising?

aws-amplify, aws-appsync

Provide additional details e.g. code snippets

Hi!

I'm using AWSAppSyncClient with my nuxt.js project and I stumbled into an issue. I'm running nuxt app in the AWS Lambda behind API Gateway.

When I'm using the AWSAppSyncClient on the client side (in the browser) everything works as expected. However using it on the server side, the credentials returned by the Auth.currentCredentials() are totally different than on the client side. This results in a 401 http error when querying the AppSync.

I'm storing user credentials in the cookies and I'm passing them via request headers to the server side. I've also written the custom CredentialsStorage class, so it can be used server and client side.

Code samples

CredentialsStorage

Used for storing credentials in the cookies. I've verified the getItem returns the same result server and client side.

import * as Cookies from 'js-cookie'

// This implementation is based on the amazon-cognito-auth-js/es/CookieStorage.js
class CredentialsStorage {
  constructor(req, params) {
    this.logger = new Logger('CredentialsStorageLogger', 'DEBUG')

    this.logger.debug('params: %j', params)
    this.req = req
    this.path = params.path
    this.expires = params.expires
    this.domain = params.domain
    this.secure = params.secure
    this.isClient = params.isClient
  }

  setItem(key, value) {
    Cookies.set(key, value, {
      path: this.path,
      expires: this.expires,
      domain: this.domain,
      secure: this.secure,
    })

    return Cookies.get(key)
  }

  getItemClient(key) {
    return Cookies.get(key)
  }

  getItemServer(passedKey) {
    const req = this.req
    const key = encodeURIComponent(passedKey) // there are @ symbols in the passedKey sometimes
    const cookieItem = req.cookies && req.cookies[key]

    this.logger.debug('key', key)
    this.logger.debug('cookieItem', cookieItem)
    return cookieItem
  }

  getItem(key) {
    return this.isClient ? this.getItemClient(key) : this.getItemServer(key)
  }

  removeItem(key) {
    return Cookies.remove(key, {
      path: this.path,
      domain: this.domain,
      secure: this.secure,
    })
  }

  clear() {
    const cookies = Cookies.get()
    for (let index = 0; index < cookies.length; ++index) {
      Cookies.remove(cookies[index])
    }
    return {}
  }
}

Auth configuration

This is how I configured the Auth:

req - is coming from nuxtjs and is a usual nodejs request.

const credentialsStorage = new CredentialsStorage(req, {
  domain: process.env.hostname,     // example.com
  path: '/',
  expires: 365,
  secure: process.env.isProduction, // true or false
  isClient: process.client,         // true or false
})

Auth.configure({
  identityPoolId: process.env.awsCognitoIdentityPoolId, // eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  region: process.env.awsCognitoRegion,                 // eu-central-1
  userPoolId: process.env.awsCognitoUserPoolId,         // eu-central-1_XXXXXXXX
  userPoolWebClientId: process.env.awsCognitoClientId,  // cognito client id
  authenticationFlowType: 'USER_PASSWORD_AUTH',
  storage: credentialsStorage,
})

import { Auth } from 'aws-amplify'

const apolloDefaultClient = new AWSAppSyncClient(
  {
    url: process.env.awsAppsyncGraphqlEndpoint,       // App sync url
    region: process.env.awsAppsyncRegion,             // eu-central-1
    auth: {
      type: 'AWS_IAM',
      credentials: () => Auth.currentCredentials(),
    },
    disableOffline: true,
  },
  {
    ssrMode: true,
  },
)

Credentials received from Auth.currentCredentials() the client side

{                                                                                                                    
  expired: false,
  expireTime: 2019-03-01T13:21:51.000Z,
  accessKeyId: 'XXXXXXXXXXXXXXXXXXXX',
  sessionToken: 'some-session-token',
  params: 
   { IdentityPoolId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
     Logins: 
      { 'cognito-idp.eu-central-1.amazonaws.com/eu-central-1_XXXXXXXX': 'some-id-token' },
     RoleSessionName: 'web-identity',
     IdentityId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
  data: 
   { IdentityId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
     Credentials: 
      { AccessKeyId: 'XXXXXXXXXXXXXXXXXXXX',
        SecretKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        SessionToken: 'some-session-token',
        Expiration: 2019-03-01T13:21:51.000Z } },
  _identityId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
  _clientConfig: { region: 'eu-central-1' },
  webIdentityCredentials: 
   WebIdentityCredentials {
     expired: true,
     expireTime: null,
     accessKeyId: undefined,
     sessionToken: undefined,
     params: 
      { IdentityPoolId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
        Logins: [Object],
        RoleSessionName: 'web-identity',
        IdentityId: 'eu-central-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' },
     data: null,
     _clientConfig: { region: 'eu-central-1' } },
  cognito: 
   Service {
     config: 
      Config {
        credentials: null,
        credentialProvider: [Object],
        region: 'eu-central-1',
        logger: null,
        apiVersions: {},
        apiVersion: null,
        endpoint: 'cognito-identity.eu-central-1.amazonaws.com',
        httpOptions: [Object],
        maxRetries: undefined,
        maxRedirects: 10,
        paramValidation: true,
        sslEnabled: true,
        s3ForcePathStyle: false,
        s3BucketEndpoint: false,
        s3DisableBodySigning: true,
        computeChecksums: true,
        convertResponseTypes: true,
        correctClockSkew: false,
        customUserAgent: 'aws-amplify/1.0.22 js',
        dynamoDbCrc32: true,
        systemClockOffset: 0,
        signatureVersion: 'v4',
        signatureCache: true,
        retryDelayOptions: {},
        useAccelerateEndpoint: false,
        clientSideMonitoring: false,
        params: [Object] },
     isGlobalEndpoint: false,
     endpoint: 
      Endpoint {
        protocol: 'https:',
        host: 'cognito-identity.eu-central-1.amazonaws.com',
        port: 443,
        hostname: 'cognito-identity.eu-central-1.amazonaws.com',
        pathname: '/',
        path: '/',
        href: 'https://cognito-identity.eu-central-1.amazonaws.com/' },
     _events: { apiCallAttempt: [Array], apiCall: [Array] },
     MONITOR_EVENTS_BUBBLE: [Function: EVENTS_BUBBLE],
     CALL_EVENTS_BUBBLE: [Function: CALL_EVENTS_BUBBLE],
     _clientId: 1 },
  sts: 
   Service {
     config: 
      Config {
        credentials: null,
        credentialProvider: [Object],
        region: 'eu-central-1',
        logger: null,
        apiVersions: {},
        apiVersion: null,
        endpoint: 'https://sts.amazonaws.com',
        httpOptions: [Object],
        maxRetries: undefined,
        maxRedirects: 10,
        paramValidation: true,
        sslEnabled: true,
        s3ForcePathStyle: false,
        s3BucketEndpoint: false,
        s3DisableBodySigning: true,
        computeChecksums: true,
        convertResponseTypes: true,
        correctClockSkew: false,
        customUserAgent: 'aws-amplify/1.0.22 js',
        dynamoDbCrc32: true,
        systemClockOffset: 0,
        signatureVersion: 'v4',
        signatureCache: true,
        retryDelayOptions: {},
        useAccelerateEndpoint: false,
        clientSideMonitoring: false },
     isGlobalEndpoint: true,
     endpoint: 
      Endpoint {
        protocol: 'https:',
        host: 'sts.amazonaws.com',
        port: 443,
        hostname: 'sts.amazonaws.com',
        pathname: '/',
        path: '/',
        href: 'https://sts.amazonaws.com/' },
     _events: { apiCallAttempt: [Array], apiCall: [Array] },
     MONITOR_EVENTS_BUBBLE: [Function: EVENTS_BUBBLE],
     CALL_EVENTS_BUBBLE: [Function: CALL_EVENTS_BUBBLE],
     _clientId: 2 },
  authenticated: true }

Credentials received from Auth.currentCredentials() the server side

{
expired: false,
expireTime: null,
accessKeyId: 'XXXXXXXXXXXXXXXX',
sessionToken: 'some-session-token',
envPrefix: 'AWS' }

As you can see credentials on the server side are missing these params:

  • data
  • webIdentityCredentials
  • cognito

I am however unable to reproduce this issue locally. It only happens in the lambda. Thank you for any idea / hint you might have.

Auth investigating node.js pending-close-response-required question

Most helpful comment

actually calling Auth.currentUserCredentials() instead of Auth.currentCredentials() did the trick for me.

All 11 comments

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This issue has been automatically closed because of inactivity. Please open a new issue if are still encountering problems.

Hey @mihaerzen - thanks so much for the detailed issue, this looks precisely what I'm trying to achieve with Next.js. Did you manage to find a solution in the end?

@josephluck No, unfortunately not. I had to work around it which was really painful. Basically, do all the user authenticated queries on the browser side...

@josephluck which solution came you up with? i am in the same spot right now with next.js.
Auth.currentCredentials always returning creds from environment or ~/.aws config.

actually calling Auth.currentUserCredentials() instead of Auth.currentCredentials() did the trick for me.

@JuHwon Are you also running this on AWS Lambda with server-side rendering? How did you implement the credentials storage class if it is not a secret? Thanks for the response.

@josephluck which solution came you up with? i am in the same spot right now with next.js.
Auth.currentCredentials always returning creds from environment or ~/.aws config.

Hi @JuHwon - I ended up ditching Amplify altogether as I only wanted it to use Cognito authentication. I tried for a while to get it to work, but it was _wayyyy_ more trouble than it was worth.

I ended up writing my own wrapper around cognito using Axios. My implementation can be found here. I'm planning on open-sourcing it at some point, when I have the time.

Basically I figured out what the request objects and responses looked like for each request and built up a custom SDK around them. Works both client-side and server-side.

Note that you'll have to build a storage mechanism so that credentials can be shared on both SSR and client-side. I used cookies for that (which can be found in the same repo).

@mihaerzen i am using aws fargate to host my nextjs app. i am not sure what you mean with crednetails storage class, though i got an express app, and using cookies for auth. so i set up the cookieParser middleware and just wrote a simple auth config for the express app.

// TODO: refactor authMiddleware
// the current implementation can lead to security issues 
const authMiddleware: Handler = (req, _res, next) => {
  const { cookieStorage: _cookieStorage, ...config } = defaultAuthConfig
  Auth.configure({
    ...config,
    storage: {
      store: {},
      getItem(key: string) {
        return req.cookies[key]
      },
      setItem(_key: string, _value: string) {
        throw new Error('auth storage `setItem` not implemented')
      },
      removeItem(_key: string) {
        throw new Error('auth storage `removeItem` not implemented')
      },
      clear() {
        throw new Error('auth storage `clear` not implemented')
      },
    },
  })

  next()
}

for the apollo ssr implementation i used a similar approach as in the nextjs example https://github.com/zeit/next.js/tree/canary/examples/with-apollo while my client creation looks like this:

import Auth from '@aws-amplify/auth'
import { AWSAppSyncClient, AUTH_TYPE } from 'aws-appsync'
import exports from '~/aws-exports'
import { NormalizedCacheObject } from 'apollo-cache-inmemory'

// isomorphic-fetch is required for ssr
import 'isomorphic-fetch'

let appSyncClient: AWSAppSyncClient<NormalizedCacheObject> = null
const createClient = (initialState?: any) => {
  const newClient = new AWSAppSyncClient({
    url: exports.aws_appsync_graphqlEndpoint,
    region: exports.aws_appsync_region,
    disableOffline: true,
    auth: {
      type: AUTH_TYPE.AWS_IAM,
      credentials: () => Auth.currentUserCredentials(),
    },
  })
  newClient.cache.restore(initialState)
  return newClient
}

export const initApollo = (initialState?: any) => {
  if (!appSyncClient) {
    appSyncClient = createClient(initialState)
  }
  return appSyncClient
}

export default client

its in WIP atm, i know there is a ssr flag missing from the example. though it does the job for now and is working fine.

Attention

I need an other way to configure the AuthStorage on the server though. Since i think this could lead to security issues when facing multiple requests from different users!

@JuHwon Yes, the storage class is the storage: part in your Auth.configure more or less.

I'm not sure if this issue exists in AWS Fargate, cause I've only experienced it while running it on AWS Lambda. I can see these environments being different.

I'll try to re-create a sample Next.js app with your solution and see if I can reproduce it on AWS Lambda.

Thank you for your comments!

@mihaerzen imo you do get different credentials on the server, because your lambda environment has credentials from the current lambda role. and the same should be the case for a fargate container.

Also if you look into the sourcecode of the amplify Auth.ts (thats how i found the solution) you can see that just using currentCredentials() does not set the credentials according to the amplify auth configuration, while the method currentUserCredentials() does.

So if you configure the auth store properly on the server, so the server side can read it, there should be no issues with using the Auth.currentUserCredentials() method i guess.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cosmosof picture cosmosof  路  3Comments

karlmosenbacher picture karlmosenbacher  路  3Comments

leantide picture leantide  路  3Comments

DougWoodCDS picture DougWoodCDS  路  3Comments

oste picture oste  路  3Comments