I'm doing a little api with register and auth using jwt, apollo-vue and graphql
I can`t get data through queries (or set it through mutations) from/to my backend.
But i can do it from Postman, cause i know how to send a token in the headers.
I'm too try to call onLogin(apolloClient, token) bellow the action login from vuex. Nothings work
I'm very newby with backend, i will appreciate any advice
Another problem? : If in the function below...
const authLink = setContext(async (_, { headers }) => {
// add here console.log(localStorage.getItem('apollo-token'))
const token = await localStorage.getItem('apollo-token')
// and then console.log(token)
return {...}
})
The first console print a token, but the second console print null. This is weird for me.
This is my vue-apollo.js
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
import { setContext } from 'apollo-link-context'
Vue.use(VueApollo)
const AUTH_TOKEN = 'apollo-token'
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:3000/graphql'
const authLink = setContext(async (_, { headers }) => {
const token = await localStorage.getItem(AUTH_TOKEN)
return {
...headers,
Authorization: token || ''
}
})
// Files URL root
export const filesRoot = process.env.VUE_APP_FILES_ROOT || httpEndpoint.substr(0, httpEndpoint.indexOf('/graphql'))
Vue.prototype.$filesRoot = filesRoot
// Config
const defaultOptions = {
httpEndpoint,
wsEndpoint: null,
tokenName: AUTH_TOKEN,
websocketsOnly: false,
ssr: false,
link: authLink
}
export const { apolloClient } = createApolloClient({
...defaultOptions,
})
export function createProvider(options = {}) {
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
})
apolloClient.wsClient = wsClient
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler(error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
},
})
return { apolloProvider, apolloClient }
}
// Manually call this when user log in
export async function onLogin(apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message)
}
}
// Manually call this when user log out
export async function onLogout(apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
}
}
main.js from vue
// HTTP connection to the API
const httpLink = createHttpLink({
// You should use an absolute URL here
uri: 'http://localhost:3000/graphql',
})
// Cache implementation
const cache = new InMemoryCache()
// Create the apollo client
const apolloClient = new ApolloClient({
link: httpLink,
cache,
})
Vue.config.productionTip = false
Vue.use(VueScreen)
.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
new Vue({
router,
store,
vuetify,
apolloProvider,
render: h => h(App)
}).$mount('#app')
EDIT: more code
This is the query, in a view on vue
import gql from "graphql-tag";
export default {
name: "Home",
apollo: {
Users: gql`
{
Users {
_id
username
email
password
token
createdAt
}
},
`,
},
};
The error that i receive is:
bundle.esm.js:75 POST http://localhost:3000/graphql 500 (Internal Server Error)
Error sending the query 'Users' ServerError: Response not successful: Received status code 500
at throwServerError
In the backend, this is my query
Query: {
async Users(_, req, context) {
const auth = checkAuth(context)
if (auth.id) {
const users = await User.find()
users.forEach(e => {
e.password = null
})
return users
} else {
return new Error("must be logged.")
}
},
and this is my checkAuth.js
import jwt from 'jsonwebtoken'
import { AuthenticationError } from 'apollo-server'
import 'dotenv/config'
module.exports = (context) => {
const authHeader = context.headers.authorization;
console.log("headers: ",context.headers)
if (authHeader) {
const token = authHeader.split('Bearer ')[1];
if (token) {
try {
const user = jwt.verify(token, process.env.SECRET_KEY);
return user
} catch (err) {
return new AuthenticationError("Invalid token.")
}
}
return new Error("Token must be 'Bearer [token]'")
}
return new Error("I need a token bro!")
}
EDIT 2
the context.header received on the backend
headers: {
host: 'localhost:3000',
connection: 'keep-alive',
'content-length': '160',
'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"',
accept: '*/*',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36',
'content-type': 'application/json',
origin: 'http://localhost:8081',
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:8081/',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'es-419,es;q=0.9,en;q=0.8'
},
The vue-apollo.js file is not used.
In your main.js the apolloClient you inject in Vue is declared in the main.js and doesn't contain the authLink. All your code in vue-apollo.js isn't called.
So instead of this:
// HTTP connection to the API
const httpLink = createHttpLink({
// You should use an absolute URL here
uri: 'http://localhost:3000/graphql',
})
// Cache implementation
const cache = new InMemoryCache()
// Create the apollo client
const apolloClient = new ApolloClient({
link: httpLink,
cache,
})
Vue.config.productionTip = false
Vue.use(VueScreen)
.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
new Vue({
router,
store,
vuetify,
apolloProvider,
render: h => h(App)
}).$mount('#app')
Try this:
import { createProvider } from 'vue-apollo.js';
Vue.config.productionTip = false
Vue.use(VueScreen)
.use(VueApollo)
const { apolloProvider, apolloClient } = createProvider();
new Vue({
router,
store,
vuetify,
apolloProvider,
render: h => h(App)
}).$mount('#app')
From what i see, you only send the token in the authorization header.
const authLink = setContext(async (_, { headers }) => {
const token = await localStorage.getItem(AUTH_TOKEN)
return {
...headers,
Authorization: token || ''
}
})
but in the backend you expect to find a bearer token:
module.exports = (context) => {
const authHeader = context.headers.authorization;
console.log("headers: ",context.headers)
if (authHeader) {
const token = authHeader.split('Bearer ')[1]; << Your code is breaking here
if (token) {
try {
const user = jwt.verify(token, process.env.SECRET_KEY);
return user
} catch (err) {
return new AuthenticationError("Invalid token.")
}
}
return new Error("Token must be 'Bearer [token]'")
}
return new Error("I need a token bro!")
}
You must send 'Bearer [token]' instead of just the token. Like so:
const authLink = setContext(async (_, { headers }) => {
const token = await localStorage.getItem(AUTH_TOKEN)
return {
...headers,
Authorization: `Bearer ${token}`
}
})
In the documentation this is how setContext is used:
const setAuthorizationLink = setContext((request, previousContext) => ({
headers: {authorization: "1234"}
}));
The setContext function takes a function that returns either an object or a promise that returns an object to set the new context of a request.
In the code below you only return the headers. When you're supposed to return the context.
const authLink = setContext(async (_, { headers }) => {
const token = await localStorage.getItem(AUTH_TOKEN)
return {
...headers,
Authorization: token || ''
}
})
Try this instead
const authLink = setContext(async (_, { headers }) => {
const token = await localStorage.getItem(AUTH_TOKEN)
return {
headers: {
...headers,
Authorization: token || ''
}
}
})
Related
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
So according to the apollo docs for apollo-link-error, onError can be used to handle re-authentication if used with forward(operation).
So I wrote the following code
import { ApolloClient } from 'apollo-client'
import { createHttpLink, HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { InMemoryCache } from 'apollo-cache-inmemory'
import AsyncStorage from '#react-native-community/async-storage'
import { refresh } from 'react-native-app-auth'
import { onError } from 'apollo-link-error'
import { ApolloLink, from } from 'apollo-link'
import { RetryLink } from "apollo-link-retry"
import { KC_CONFIG } from '../../config/env'
const httpLink = new HttpLink({
uri: 'graphqlEndpointOfYourchoice'
})
const authLink = setContext(async (_, { headers }) => {
const accessToken = await AsyncStorage.getItem('accessToken')
const unwrappedAccessToken = JSON.parse(accessToken)
return {
headers: {
...headers,
authorization: unwrappedAccessToken ? `Bearer ${unwrappedAccessToken}` : "",
}
}
})
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
AsyncStorage.getItem('refreshToken')
.then(data => {
const refreshToken = JSON.parse(data)
// console.log(data)
refresh(KC_CONFIG, {
refreshToken,
})
.then(({ accessToken, refreshToken }) => {
const oldHeaders = operation.getContext().headers
operation.setContext({
...oldHeaders,
authorization: accessToken
})
console.log(oldHeaders.authorization)
console.log(accessToken)
// console.log(refreshToken)
AsyncStorage
.multiSet([
['accessToken', JSON.stringify(accessToken)],
['refreshToken', JSON.stringify(refreshToken)]
])
// tried putting forward() here <--------------
})
.catch(e => {
if (e.message === 'Token is not active') console.log('logging out')
else console.log('Refresh error: ' + e)
})
})
.then(() => {
console.log('Refreshed the accesstoken')
return forward(operation)
})
.catch(e => {
console.log('Storage error: ' + e)
})
}
if (networkError) {
console.log('network error: ' + networkError)
}
// tried putting forward() here <--------------
})
const retryLink = new RetryLink()
export const client = new ApolloClient({
link: from([
retryLink,
errorLink,
authLink,
httpLink
]),
cache: new InMemoryCache()
})
This does not achieve the desired result.
The error gets caught and runs it's course, refreshing the token as it should, but it never does a second request.
try this:
export const logoutLink = onError(({ networkError, operation, forward }) => {
if (networkError?.statusCode === 401) {
return new Observable(observer => {
(async () => {
try {
const newToken = await getToken();
// Modify the operation context with a new token
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${newToken}`,
},
});
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
// Retry last failed request
forward(operation).subscribe(subscriber);
} catch (error) {
observer.error(error);
}
})();
});
}
});
I am creating an app in react native and using laravel as backend. To manage error globally i am trying to create a axios request interceptor (never create before) which can handle errors but, i am getting error _api.default.get is not a function.
// My Axios Interceptor File
import axios from 'axios';
import {Config} from './common';
import {AsyncStorage} from '#react-native-community/async-storage';
const TIMEOUT = 1 * 60 * 1000;
axios.defaults.timeout = TIMEOUT;
axios.defaults.baseURL = Config.apiUrl;
const axiosInterceptors = async () => {
const token = await AsyncStorage.getItem('token');
const onRequest = config => {
if (token) {
config.headers.common.Authorization = `Bearer ${token}`;
}
return config;
};
const onSuccess = response => {
return response.data;
};
const onError = error => {
return Promise.reject(error);
};
axios.interceptors.request.use(onRequest);
axios.interceptors.response.use(onSuccess, onError);
};
export default axiosInterceptors;
and in my reducer
import axiosInterceptors from "../api"
export const fetchData = () => {
return dispatch => {
axiosInterceptors.get(apiUrl).then(something).catch(something)
}
}
It's work for me
const axiosInterceptors = axios.create({
baseURL: "your base url",
timeout: 500,
headers: {
Accept: "application/json",
Authorization: "Bearer "
}
});
API.interceptors.request.use(
async function(config) {
axios.defaults.timeout = 500;
const token = await AsyncStorage.getItem('token');
config.headers.Authorization = "Bearer ".concat(token);
return config;
},
function(error) {
return Promise.reject(error);
}
);
export default axiosInterceptors;
I am building a Vue frontend with Rails backend.
On frontend I am using Axios and I have set these interceptors for authentication:
import axios from 'axios'
const API_URL = 'http://localhost:3000'
const securedAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
const plainAxiosInstance = axios.create({
baseURL: API_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
})
securedAxiosInstance.interceptors.request.use(config => {
const method = config.method.toUpperCase()
if (method !== 'OPTIONS' && method !== 'GET') {
config.headers = {
...config.headers,
'X-CSRF-TOKEN': localStorage.csrf
}
}
return config
})
securedAxiosInstance.interceptors.response.use(null, error => {
if (error.response && error.response.config && error.response.status === 401) {
// If 401 by expired access cookie, we do a refresh request
return plainAxiosInstance.post('/refresh', {}, { headers: { 'X-CSRF-TOKEN': localStorage.csrf } })
.then(response => {
localStorage.csrf = response.data.csrf
localStorage.signedIn = true
// After another successfull refresh - repeat original request
let retryConfig = error.response.config
retryConfig.headers['X-CSRF-TOKEN'] = localStorage.csrf
return plainAxiosInstance.request(retryConfig)
}).catch(error => {
delete localStorage.csrf
delete localStorage.signedIn
// redirect to signin if refresh fails
location.replace('/')
return Promise.reject(error)
})
} else {
return Promise.reject(error)
}
})
export { securedAxiosInstance, plainAxiosInstance }
On main.js I am making them available this way:
import VueAxios from 'vue-axios'
import { securedAxiosInstance, plainAxiosInstance } from './axios'
Vue.use(VueAxios, {
secured: securedAxiosInstance,
plain: plainAxiosInstance
})
new Vue({
el: '#app',
router,
store,
securedAxiosInstance,
plainAxiosInstance,
render: h => h(App)
})
And in components I can successfully use them like:
this.$http.secured.get('/items')
The problem is that I am unable to use them in store where I get:
Cannot read property 'secured' of undefined"
I tried in store among others:
import { securedAxiosInstance, plainAxiosInstance } from '../axios'
const store = new Vuex.Store({
secured: securedAxiosInstance,
plain: plainAxiosInstance,
.....
What is the correct way to do it?
You can use this._vm inside the store which refers to the Vue instance of the current application.
So in your case:
this._vm.$http.secured.get('/items')
As alternative you can pass the Vue instance as payload to your mutation/action, like:
this.$store.commit('myMutation',this)
I want to add an Authorization header to every request I make to the GraphQL backend.
I am using a Remotbackend.
The Apollo documentation has an example how to add a header:
https://www.apollographql.com/docs/react/recipes/authentication.html#Header
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null,
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
But how would I do this with ReactQL?
In the Example Repo of ReactQL
https://github.com/reactql/example-auth
a method is mentioned:
config.setApolloNetworkOptions({
credentials: 'include',
});
config.addApolloMiddleware((req, next) => {
const token = 'the_token'
req.options.headers = {
...req.options.headers,
authorization: token
};
next();
});
This adds the header to every request!