I'm new to apollo/graphql and I'm trying to get my authentication done properly in a greenfield project. My authentication provider is AWS cognito. I wrote a cognito helper module to interact with it.
Though I'm not quite sure how to sync my apollo client with my auth state.
export const authenticate = (username: string, password: string) => {
const authDetails = new AuthenticationDetails({
Username: username,
Password: password,
})
const cognitoUser = getCognitoUser(username)
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authDetails, {
onSuccess: result => {
resolve(result)
},
onFailure: err => {
reject(err)
},
})
})
}
export const getCurrentUserToken = () => {
return new Promise((resolve, reject) => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.getSession((error, session) => {
if (error) {
reject(error)
}
resolve(session.getIdToken().getJwtToken())
})
} else {
resolve(null)
}
})
}
export const logout = () => {
const currentUser = userPool.getCurrentUser()
if (currentUser) {
currentUser.signOut()
}
}
Right now I'm just using these function to handle my login by calling them in my react component handlers. I configured an apollo-link for adding the auth header. Inject my JWT token data into context at the backend and implemented a currentUser query resolver in the backend.
const resolvers = {
RootQuery: {
currentUser: (obj, args, context) =>
context.tokenData
? {
id: context.tokenData.sub,
name: context.tokenData.name,
email: context.tokenData.email,
username: context.tokenData['cognito:username'],
}
: null,
},
}
In my react App layout i got a component UserPanel which queries that currentUser query.
const CURRENT_USER_QUERY = gql`
query {
currentUser {
name
}
}
`
export default graphql(CURRENT_USER_QUERY)(UserPanel)
When i am logging in now obviously the UserPanel does not update its currentUser query except I'm reloading the page ofc. Though im also having troubles finding a good solution to sync them.
I was thinking about implementing my login via graphql mutation using apollo-link-state to do it locally and watch these to refetch if someone logged in/out. I'm not sure if this is fine since it seems to me that this link cannot resolve async stuff (e.g. promises) in its mutation resolvers.
Another option I was thinking about was to decouple the auth process from the apollo client completely and implement some auth pubsub system maybe with Observables and let the react components refetch the queries if the authentication state changes.
I'm very uncertain how to continue and every solution I'm thinking about doesn't feel like the recommended way to go.
I don't have the full picture with regards to your React setup but here I go. It might be that Apollo-client is caching CURRENT_USER_QUERY locally and is showing you the results of a previous query. You could try the network-only option on the query:
export default graphql(CURRENT_USER_QUERY, { options: {fetchPolicy: 'network-only' }})(UserPanel)
What I have in React is an AppContainer which is my parent component. It checks if the user is logged in:
const loggedInUser = gql`
query loggedInUser{
user {
id
role
}
}`
export default graphql(loggedInUser, { options: {fetchPolicy: 'network-only' }})(AppContainer)
Then on my UserProfile page, I use a data container to fetch the data before passing it down to the UserProfile child component. I think the loggedInUser query automatically updates the user in the apollo store. With it apollo-client realizes that it needs to refetch userQuery. Does that help?
const userQuery = gql`
query userQuery {
user {
id
name
email
role
company
}
}
`
export default graphql(userQuery, {name: 'userQuery'})(UserDataContainer);
Related
This example demonstrates how to use Auth0 with react-admin. It is working as expected.
We are trying to adjust it so it will use the hasura data provider. We've created a new file dataProvider.js that will construct the data-provider:
import buildHasuraProvider from "ra-data-hasura";
import { ApolloClient, InMemoryCache } from "#apollo/client";
export const initDataProvider = async (token) => {
const client = new ApolloClient({
uri: process.env.REACT_APP_GRAPHQL_URI,
headers: {
Authorization: `Bearer ${token}`,
},
cache: new InMemoryCache(),
});
const dataProvider = await buildHasuraProvider({ client });
return dataProvider;
};
However, we are missing the JWT token which is created as part of the Auth0 authentication process. We do not know how to get the token in order to initialize the data provider with it. Does react-admin know how to do it on its own? if not, how do we access the JWT token to do it ourselves manually?
This is the authProvider source-code:
import authConfig from "./authConfig";
import {Auth0Client} from '#auth0/auth0-spa-js';
const auth0 = new Auth0Client({
domain: authConfig.domain,
client_id: authConfig.clientID,
redirect_uri: authConfig.redirectURI,
cacheLocation: 'localstorage',
useRefreshTokens: true
});
export default {
// called when the user attempts to log in
login: (url) => {
if (typeof url === 'undefined') {
return auth0.loginWithRedirect()
}
return auth0.handleRedirectCallback(url.location);
},
// called when the user clicks on the logout button
logout: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) { // need to check for this as react-admin calls logout in case checkAuth failed
return auth0.logout({
redirect_uri: window.location.origin,
federated: true // have to be enabled to invalidate refresh token
});
}
return Promise.resolve()
})
},
// called when the API returns an error
checkError: ({status}) => {
if (status === 401 || status === 403) {
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
return auth0.isAuthenticated().then(function (isAuthenticated) {
if (isAuthenticated) {
return Promise.resolve();
}
return auth0.getTokenSilently()
})
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => {
return Promise.resolve()
},
};
It is unclear to us if there is a point where we can extract the token from.
getTokenSilently should give you back the token.
You'll have to structure your React app such that you have access to the result of this method before you construct your data provider.
Okey, this is the repo
What I want to do: Test my protected routes.
Currently, the security of the app is handle by passport, with this strategy: graphql-passport.
I am running my rests with supertest (for the request) and jest
When I build the Apollo Server, i use it to create the context:
import { buildContext } from 'graphql-passport';
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => {
return buildContext({ req, res, User })
},
playground: {
settings: {
'request.credentials': 'same-origin',
},
},
});
This allows me to get the user from the request. Like any authentication with passport works.
passport.use(
new GraphQLLocalStrategy((email, password, next) => {
console.log(`🎫 GraphQLLocalStrategy ${email} 🚔 👮♂`)
User.findOne({ email })
.then(user => !user
? next(null, false, 'Invalid email or password')
: user.checkPassword(password) //bcrypt
.then(match => !match
? next(null, false, 'Invalid email or password')
: next(null, user)
)
)
.catch(error => next(error))
}),
);
So far, it works good enough. For every test that i run, I can see my 🎫 GraphQLLocalStrategy ${email} 🚔 👮♂ being called. Good!.
For some mutations, like login and update user profile, i am able to do this:
user.mutations.test.js
// * Login for add the user in the context
agent
.post("/graphql")
.send({ query: ` mutation {
${loginQuery(email)}
${updateFirstName}
}`})
.set("Accept", "application/json")
.end((err, {body:{data, errors}}) => {
if (err) return done(err);
const {updateUser} = data;
expect(updateUser).toBeInstanceOf(Object);
expect(updateUser.email).toBe(email);
expect(updateUser.firstName).toBe(newName);
expect(updateUser.rol).toBe("patron");
UserFields.map(checkFields(updateUser));
done();
})
So, in one query, I can send the login mutation and then run the update the first name mutation. Both, works good enough, and according to passport I am logged and I can update the user profile.
What is the issue?? I want to run a loging mutation and after that run a query to get all users.
But, ofcourse, I can not run both at the same time in the request(app).post("/graphql").send() It has to be a one or multiple mutations or a queries... but not both.
The other idea, who doesnt work, is run one, and in the response, run the second one, like this:
const agent = request(app);
agent
.post("/graphql")
.send({ query: `mutation { ${loginQuery(email)} }`})
.end((err, {body:{data}}) => {
if (err) return done(err);
agent
.post("/graphql")
.send({ query: `query { getGuestsQuery() }`})
...
If I try to ask in a second request for a protected route, there is not a way to know that i was authenticated, at least not automatically... Can I make an authenticated request here with supertest
**How can I tell to my tested application that I am authenticated???? **
test("fetch all Guests", async (done) => {
const userAdmin = await User.findOne({rol:"admin"}).exec();
if(!userAdmin) return done('no admin users for testing');
const agent = request.agent(app);
agent
.post('/graphql')
.send({ query: ` mutation { ${loginQuery(userAdmin.email)} }`})
.expect(200)
.end((err, res) => {
if (err) return done(err);
agent
.post("/graphql")
.send({query: `{ getGuests { ${GuestInput.join(' ')} } }`})
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200)
.end((err, {body:{data}}) => {
if (err) return done(err);
expect(data).toBeInstanceOf(Object);
const {getGuests} = data;
expect(getGuests).toBeInstanceOf(Array);
getGuests.map(user => GuestInput.map(checkFields(user)))
done();
});
});
});
I've got a query that looks like this:
export const GET_PROJECT = gql`
query GetProject($id: String!) {
homework {
getProject(id: $id) {
...ProjectFields
}
}
}
${ProjectFieldsFragment}
`;
My InMemoryCache looks like this:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
getProject: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
The above cache redirect is never hit. However, if I modify it to look like:
const cache = new InMemoryCache({
dataIdFromObject: ({ id }) => id,
cacheRedirects: {
Query: {
homework: (_, args, obj) => {
console.log('Hello world');
},
},
}
});
It does get hit, however I don't have any of the arguments that are passed in the nested getProject query. What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for, like:
export const SESSION = gql`
query Session {
session {
user {
id
fullName
email
}
organizations {
name
id
}
}
}
`;
So what is going on? I've resorted to just using readFragment in the places where I want the cache to redirect, but I'd like for that logic to become centralized.
It's hard to say for sure with these kinds of issues, but I'm betting that, since you say
What's also confusing is that this cache redirect function is hit for queries that it seemingly shouldn't get hit for
the issue might be with your dataIdFromObject function.
This function is ultimately what decides if data is read from the cache or not. You should only override this if you have a very specific reason to. For example:
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
// ...
export default new ApolloClient({
link,
cache: new InMemoryCache({
dataIdFromObject(object) {
switch (object.__typename) {
case 'ModifierScale':
case 'ModifierGroup':
return [
object.__typename,
object.id,
...object.defaults
.map((defaultModifier) => defaultModifier.id)
.join(''),
].join('');
default:
return defaultDataIdFromObject(object); // fall back to default handling
}
},
}),
});
The point of this setting is to allow you to customize the key that gets put into the cache when you are loading the data.
If this doesn't solve your issue, I would definitely go into the Apollo tab in the chrome dev tools (you need the Apollo dev tools chrome extension to do this) and look at the cache section. It should show you the data in the cache and the key that the data is stored in.
I am trying to use Apollo-client to pull my users info and stuck with this problem:
I have this Container component responsible for pulling the user's data (not authentication) once it is rendered. User may be logged in or not, the query returns either viewer = null or viewer = {...usersProps}.
Container makes the request const { data, refetch } = useQuery<Viewer>(VIEWER);, successfully receives the response and saves it in the data property that I use to read .viewer from and set it as my current user.
Then the user can log-out, once they do that I clear the Container's user property setUser(undefined) (not showed in the code below, not important).
The problem occurred when I try to re-login: Call of refetch triggers the graphql http request but since it returns the same data that was returned during the previous initial login - useQuery() ignores it and does not update data. Well, technically there could not be an update, the data is the same. So my code setUser(viewer); does not getting executed for second time and user stucks on the login page.
const { data, refetch } = useQuery<Viewer>(VIEWER);
const viewer = data && data.viewer;
useEffect(() => {
if (viewer) {
setUser(viewer);
}
}, [ viewer ]);
That query with the same response ignore almost makes sense, so I tried different approach, with callbacks:
const { refetch } = useQuery<Viewer>(VIEWER, {
onCompleted: data => {
if (data.viewer) {
setUser(data.viewer);
}
}
});
Here I would totally expect Apollo to call the onCompleted callback, with the same data or not... but it does not do that. So I am kinda stuck with this - how do I make Apollo to react on my query's refetch so I could re-populate user in my Container's state?
This is a scenario where apollo's caches come handy.
Client
import { resolvers, typeDefs } from './resolvers';
let cache = new InMemoryCache()
const client = new ApolloClient({
cache,
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: localStorage.getItem('token'),
},
}),
typeDefs,
resolvers,
});
cache.writeData({
data: {
isLoggedIn: !!localStorage.getItem('token'),
cartItems: [],
},
})
LoginPage
const IS_LOGGED_IN = gql`
query IsUserLoggedIn {
isLoggedIn #client
}
`;
function IsLoggedIn() {
const { data } = useQuery(IS_LOGGED_IN);
return data.isLoggedIn ? <Pages /> : <Login />;
}
onLogin
function Login() {
const { data, refetch } = useQuery(LOGIN_QUERY);
let viewer = data && data.viewer
if (viewer){
localStorage.setItem('token',viewer.token)
}
// rest of the stuff
}
onLogout
onLogout={() => {
client.writeData({ data: { isLoggedIn: false } });
localStorage.clear();
}}
For more information regarding management of local state. Check this out.
Hope this helps!
I think I have the backend subscription setup correctly. I am using angular on the client side, when I try to call subscribe I got an error
passwordUpdatedSubscription = gql`
subscription passwordUpdated{passwordUpdated{name password}}
`;
// Apollo Subscription
var subscription = this.apollo.subscribe({
query: this.passwordUpdatedSubscription
});
subscription.subscribe(
{
next(data) {
console.log(data);
},
error(err) { console.error('err', err); },
}
);
And then this is the error appears in the console
{"type":"subscription_fail","id":0,"payload":{"errors":[{"message":"Cannot read property 'subscribe' of undefined"}]}}
Maybe I am missing something on the backend? Do I need to define the setupFunctions in the SubscriptionManager?
This is my SubscriptionManager
const sub = require('graphql-subscriptions');
const pubSub = new sub.PubSub();
const manager = new sub.SubscriptionManager({
schema,
pubSub
});
This is my schema in graphQL
const graphql = require('graphql');
var schema = graphql.buildSchema(`
type Subscription {
passwordUpdated: User
}
type Mutation {
setMessage(message: String): String,
updateUserPassword(userName: String, password: String): User!
}
type Query {
getMessage: String,
getUsers: [User],
findUsers(userName: String): [User]
}
type User {
name: String,
password: String
}
`);
Yes you are missing the setup function. You could take a look at this links graphql subscription docu or example.
Your subscription manager could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
passwordUpdated: (options, args) => ({ // name of your graphQL subscription
passwordUpdatedChannel: { // name of your pubsub publish-tag
filter: () => {
return true
},
},
}),
},
});
When you call the pubsub publish function you have to write it like this pubsub.publish("passwordUpdatedChannel").
Sidenode: You might want to add the id of the user that has the password changed to the subscription. If you do that you can add it to the filter option, could look like this filter: (user) => {return user.id === args.userId}