Open VicFrolov opened 4 years ago
@VicFrolov
If you use Amplify you can do something like is mentioned here
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
const client = new AWSAppSyncClient({
url: awsconfig.aws_appsync_graphqlEndpoint,
region: awsconfig.aws_appsync_region,
auth: {
type: AUTH_TYPE.AWS_IAM,
credentials: () => Auth.currentCredentials(),
},
});
@elorzafe thanks for the reply, but as mentioned I am looking on accomplishing this with ApolloClient, and am already doing this successfully with 1 auth, the issue is changing the type and credentials dynamically.
if users are not registered, they are authenticated as guests via IAM
auth: {
type: AUTH_TYPE.AWS_IAM,
credentials: () => Auth.currentCredentials(),
}
After they signup/login, they are authenticated via
auth: {
jwtToken: async () =>
Auth.currentSession()
.then(value => value.getIdToken().getJwtToken())
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
}
I can get both ways working individually, but I am not able to swap between the two on users logging in and out. Would be great to get a working example in the docs. This is why I mentioned setContext
, one way would be to use the header there, however getting the token for IAM users is not trivial with the SDK.
Hi @VicFrolov , I'm working on exactly the same thing today :)
Can you perhaps share what we have around trying to use setContext? Maybe I can also try and get it working.
I can also get both authentication types working independantly - just not sure how to dynamically switch between the two when the user eventually logs in.
Just FYI, I ended up just going with two ApolloClients, and switching between them in my store.
I did achieve it before but with v3 it's not works for appsync sockets:
import AWSAppSyncClient, {createAppSyncLink} from 'aws-appsync';
import {PureQueryOptions} from 'apollo-client';
import {ApolloQueryResult, MutationOptions, QueryOptions} from 'apollo-client';
import {map} from 'lodash';
import '../polyfills/server-fetch';
import {InMemoryCache} from 'apollo-cache-inmemory';
import {setContext} from 'apollo-link-context';
import {ApolloLink} from 'apollo-link';
import {createHttpLink} from 'apollo-link-http';
import {logError, logResponse} from './apolloLogHelpers';
import {getAccessToken, getDefaultQueryParams, getMeteorTokenString} from '../helpers/apiHelper';
import AppSyncConfig from '../../aws.config';
const credentials = {
url: AppSyncConfig.API_URL,
region: AppSyncConfig.REGION,
auth: {
type: AppSyncConfig.AUTHENTICATION_TYPE,
apiKey: AppSyncConfig.API_KEY
}
};
const httpLink = createAppSyncLink({
...credentials,
resultsFetcherLink: ApolloLink.from([
setContext((_request, previousContext) => ({
headers: {
...previousContext.headers,
Authorization: getAccessToken() || getMeteorTokenString()
}
})),
createHttpLink({
uri: AppSyncConfig.API_URL
})
]),
complexObjectsCredentials: (): null => null
});
export const apolloClient = new AWSAppSyncClient(
{
...credentials,
disableOffline: true
},
{
link: httpLink,
cache: new InMemoryCache()
}
);
export default apolloClient;
@nicokruger thanks for sharing this solution! It's a good hack, but I didn't like the idea of having two different states of cache for one session.
Here is a solution using setContext from Apollo. The extra hoop required to jump is signing IAM tokens with sigv4, and get the necessary headers:
Setting headers in Apollo dynamically, and creating the client
// TODO: cache token
const authLinkWithContext = setContext(async (operation, forward) => {
let token; // authenticated user
let unauthenticateddHeader; // unauthenticated (guest) user
try {
const session = await Auth.currentSession();
token = session?.getIdToken()?.getJwtToken();
} catch (error) {
// no-op, this catches a thrown error: no current user
}
if (!token) {
try {
const credentials = await Auth.currentCredentials();
unauthenticateddHeader = await getHeadersForIamAuth(
{ credentials, region, url },
operation
);
} catch (error) {
// tslint:disable-next-line
console.log(error);
}
}
const headers = token
? { Authorization: token || '' }
: { ...unauthenticateddHeader };
return {
...forward,
headers,
};
});
export const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([authLinkWithContext, new HttpLink({ uri: url })]),
});
When usingcreateAuthLink
, iAM signing is handled by iamBasedAuth. I copied the same logic, but removed any operation,forward and context logic, just headers are needed.
export const getHeadersForIamAuth = async (
{ credentials, region, url },
operation
) => {
const service = SERVICE;
const creds =
typeof credentials === 'function' ? credentials.call() : credentials || {};
if (creds && typeof creds.getPromise === 'function') {
await creds.getPromise();
}
const { accessKeyId, secretAccessKey, sessionToken } = await creds;
const { host, path } = Url.parse(url);
const formatted = {
...formatAsRequest(operation, {}),
service,
region,
url,
host,
path,
};
const { headers } = Signer.sign(formatted, {
access_key: accessKeyId,
secret_key: secretAccessKey,
session_token: sessionToken,
});
return {
...headers,
[USER_AGENT_HEADER]: USER_AGENT,
};
};
I imported their Signer, formatAsRequest function, and USER_AGENT
Now I can use both cognito and IAM authentication with Apollo.
It would be great if createAuthLink
was able to take multiple auth types.
@hmelenok you are using AWSAppSyncClient
, not ApolloClient
ApolloLink is flexible enough to allow you to switch between multiple AuthLink instances created by aws-appsync-auth-link
(you can cache them using link context). You can create as many as you need as switch between them based on whether the user is signed in.
Here's my (Apollo v3-beta based) prototype, wrapped up in a useApolloClient
react hook to make it easy to consume. It uses Hub events to automatically handle sign in/out events. I'm new to Apollo and haven't written tests or used this in a real app (yet), so YMMV (feedback appreciated).
amplify-auth-link.js
import { ApolloLink } from "@apollo/client"
import { setContext } from "@apollo/link-context"
import { onError } from "@apollo/link-error"
import Auth from "@aws-amplify/auth"
import { Hub } from "@aws-amplify/core"
import {
AUTH_TYPE,
createAuthLink as awsCreateAuthLink,
} from "aws-appsync-auth-link"
// To keep things simple, only support a single instance.
let amplifyAuthLink = null
let region
let url
// Create an ApolloLink that uses IAM/Cognito based on sign-in state.
// Uses a cached AuthLink created by aws-appsync-auth-link under the covers.
export const createAuthLink = (appSyncConfig) => {
region = appSyncConfig.region
url = appSyncConfig.url
return cachedAmplifyAuthLink.concat(
new ApolloLink((operation, forward) =>
operation.getContext().amplifyAuthLink.request(operation, forward)
),
resetToken
)
}
// Create an AWS AuthLink that uses Cognito, suitable for signed-in users.
const createCognitoAuthLink = (session) =>
awsCreateAuthLink({
auth: {
jwtToken: session.getIdToken().getJwtToken(),
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
},
region,
url,
})
// Create an AWS AuthLink that uses IAM, suitable for non signed-in users.
const createIamAuthLink = () =>
awsCreateAuthLink({
auth: {
credentials: () => Auth.currentCredentials(),
type: AUTH_TYPE.AWS_IAM,
},
region,
url,
})
// An ApolloLink that uses context to cache the amplifyAuthLink instance.
const cachedAmplifyAuthLink = setContext(() => {
if (amplifyAuthLink) {
return { amplifyAuthLink }
}
// Asynchronously initialise and cache amplifyAuthLink.
return Auth.currentSession()
.then((session) => {
amplifyAuthLink = createCognitoAuthLink(session)
return { amplifyAuthLink }
})
.catch((error) => {
// Amplify throws when not signed in.
amplifyAuthLink = createIamAuthLink()
return { amplifyAuthLink }
})
})
// An ApolloLink that reverrts to using IAM when 401 is encountered.
// TODO: Decide if this is desirable.
const resetToken = onError(({ networkError }) => {
if (networkError?.name == "ServerError" && networkError?.statusCode == 401) {
amplifyAuthLink = createIamAuthLink()
}
})
// Add Hub auth listeners, to detect sign-in/out.
export const addListeners = () => {
const handleAuthEvents = ({ payload }) => {
switch (payload.event) {
case "signIn":
amplifyAuthLink = createCognitoAuthLink(payload.data.signInUserSession)
break
case "signOut":
amplifyAuthLink = createIamAuthLink()
break
case "configured":
case "signIn_failure":
case "signUp":
default:
break
}
}
Hub.listen("auth", handleAuthEvents)
return handleAuthEvents
}
// Remove Hub auth listeners.
export const removeListeners = (handler) => Hub.remove("auth", handler)
use-apollo-client.js
import { ApolloClient, HttpLink, InMemoryCache, concat } from "@apollo/client"
import React from "react"
import {
addListeners,
createAuthLink,
removeListeners,
} from "./amplify-auth-link"
const createApolloClient = (appSyncConfig) =>
new ApolloClient({
cache: new InMemoryCache(),
link: concat(
createAuthLink(appSyncConfig),
new HttpLink({ uri: appSyncConfig.url })
),
})
export const useApolloClient = (appSyncConfig) => {
const [client] = React.useState(() => createApolloClient(appSyncConfig))
React.useEffect(() => {
const handler = addListeners()
return () => removeListeners(handler)
})
return client
}
Index.js
import React from "react"
import { ApolloProvider } from "@apollo/client"
import { useApolloClient } from "./use-apollo-client"
Amplify.configure({ Auth: {...} })
const appSyncConfig = { region: "us-east-1", url: "..." }
const Index = () => {
const client = useApolloClient(appSyncConfig)
return (
<ApolloProvider client={client}>
...
</ApolloProvider>
)
}
export default Index
@patspam the one thing that is unclear to me in your solution is what the input of Amplify.configure should be in the index.js file. What parameters are you using to initialize the Amplify instance?
Is it using the IAM credentials since that's the fallback, or are you somehow instantiating Amplify configure without indicating an auth type?
@spencergrimes I believe the initial call to Amplify.configure is not even technically needed and all it does is configure @amplify/auth to connect to the right place (in this example a specific cognito user pool). In our app we don't even call this until the user goes through the signin process.
In @patspam's (excellent) example the initial call to Auth.currentSession() will throw an error if Amplify.configure has not been called or if the user is not signed in. This error will be caught and then createIamAuthLink() is called to fallback to the IAM credentials.
When the user signs in, Auth.currentSession no longer throws. In order to sign in you'll have to eventually call Amplify.configure or Auth.configure plus Auth.signIn() in your app. Hope this helps.
I have found a "solution" that's simple and seems to work. It might be a good idea to cache the jwtToken
somewhere instead of using Auth.currentSession()
from aws on every request.
Ideas for improvements are much appreciated
import {
ApolloClient,
InMemoryCache,
HttpLink,
ApolloLink,
} from "@apollo/client";
import { setContext } from 'apollo-link-context';
import { Auth } from 'aws-amplify';
import { AuthOptions, AUTH_TYPE, createAuthLink } from 'aws-appsync-auth-link';
import { createSubscriptionHandshakeLink } from 'aws-appsync-subscription-link';
import appSyncConfig from "./aws-exports";
const url = appSyncConfig.aws_appsync_graphqlEndpoint;
const region = appSyncConfig.aws_appsync_region;
const auth: AuthOptions = {
type: AUTH_TYPE.API_KEY,
apiKey: appSyncConfig.aws_appsync_apiKey,
};
const httpLink = new HttpLink({ uri: url });
// Hack for removing x-api-key (api key authentication) and adding jwt token (cognito oAuth user)
// as the Authorization header won't be recognised by the aws api if the x-api-key is present
const authLink = setContext(() => new Promise((resolve) => {
Auth.currentSession()
.then(session => {
const token = session.getIdToken().getJwtToken();
// Resolve with jwt token in header
resolve({
headers: { Authorization: token, 'x-api-key': '' }
});
}).catch(() => {
// Resolve with default api key
resolve({})
})
}));
const link = ApolloLink.from([
(authLink as unknown) as ApolloLink,
createAuthLink({ url, region, auth }),
createSubscriptionHandshakeLink({ url, region, auth }, httpLink),
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
});
export default client;
It took me quite a while to get working, so I hope it can help someone else :)
Hi folks! Do you know if it's possible to set the systemClockOffset to address clock skew issues? I can't seem to find a way to pass one to the auth middleware to allow some of my clients with skewed clocks to still use our system.
@emolr thank you soooooo soooo much for your solution 🙏
@emolr thank you soooooo soooo much for your solution 🙏
You're very welcome. I'm glad it was helpful
Do you want to request a feature or report a bug? Feature question
What is the current behavior? After following the README for integration with ApolloClient, I had it working great with 1 client (https://github.com/awslabs/aws-mobile-appsync-sdk-js#using-authorization-and-subscription-links-with-apollo-client-no-offline-support)
I am trying to swap between IAM unauthenticated auth ,and cognito authenticated auth when users sign in/out. I am able to get each working individually, but cannot get both working.
I have tried using
setContext
, which would return an AuthLink, as well as ApolloLink.from, but it would always return 401. An example would be great!I think the issue I am stumbling on is with IAM, I am not sure what to put in the header, whereas for cognito auth I can simply do:
Which versions and which environment (browser, react-native, nodejs) / OS are affected by this issue? Did this work in previous versions? aws-appsync-auth-link: 2.0.1