Using Auth.currentCredentials() server side.
aws-amplify, aws-appsync
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.
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 {}
}
}
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,
},
)
{
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 }
{
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:
I am however unable to reproduce this issue locally. It only happens in the lambda. Thank you for any idea / hint you might have.
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.
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.
Most helpful comment
actually calling
Auth.currentUserCredentials()instead ofAuth.currentCredentials()did the trick for me.