NextJs/ Apollo Client/ NextAuth issue setting authorization Bearer Token to headers correctly - authentication

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);

Related

vue axios.get to fetch images from imgur

While trying to fetch all images from the Galleries template I am receiving the following error/warn notification: "Property "token" was accessed during render but is not defined on instance". instead of displaying the images I receive the following: <img src="function link() { [native code] }"> What am I missing?
Galleries.vue
<template>
<div>
<img v-for="image in allImages" :src="image.link" :key="image.id" />
</div>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
export default {
name: "Galleries",
computed: mapGetters(["allImages"]),
methods: mapActions(["fetchImages"]),
created() {
this.fetchImages();
}
};
</script>
imgur.js
import qs from 'qs';
import axios from 'axios';
const CLIENT_ID = 'e28971925a8d43c';
const ROOT_URL = 'https://api.imgur.com';
export default {
login() {
const querystring = {
client_id: CLIENT_ID,
response_type: 'token',
};
window.location = `${ROOT_URL}/oauth2/authorize?${qs.stringify(querystring)}`
},
fetchImages(token) {
return axios.get(`${ROOT_URL}/3/account/me/image/`, {
headers: {
Authorization: `Bearer ${token}`
}
});
},
uploadImages(images, token) {
const promises = Array.from(images).map(image => {
const formData = new FormData();
formData.append('image', image);
return axios.post(`${ROOT_URL}/3/image`, formData, {
headers: {
Authorization: `Bearer ${token}`
}
});
});
return Promise.all(promises);
}
};
images.js
import api from '../../api/imgur';
import { router } from '../../main';
const state = {
images: []
};
const getters = {
allImages: state => state.images
};
const mutations = {
setImages: (state, images) => {
state.images = images;
}
};
const actions = {
async fetchImages({ rootState, commit }) {
const { token } = rootState.auth;
const response = await api.fetchImages(token);
commit('setImages', response.data.data);
},
async uploadImages({ rootState }, images) {
// Get the access token
const { token } = rootState.auth;
// Call our API module to do the upload
await api.uploadImages(images, token);
// Redirect use to the gallery page
router.push('/galleries');
}
};
export default {
state,
getters,
mutations,
actions
}
auth.js
import api from '../../api/imgur';
import qs from 'qs';
import { router } from '../../main';
const state = {
token: window.localStorage.getItem('imgur_token')
};
const getters = {
isLoggedIn: state => !!state.token // turn a value into boolean
};
const actions = {
login: () => {
api.login();
},
finalizeLogin({ commit }, hash) {
const query = qs.parse(hash.replace('#', ''));
commit('setToken', query.access_token);
window.localStorage.setItem('imgur_token', query.access_token);
router.push('/');
},
logout: ({ commit }) => {
commit('setToken', null);
window.localStorage.removeItem('imgur_token');
router.push('/');
}
};
const mutations = {
setToken: (state, token) => {
state.token = token;
}
};
export default {
state,
getters,
actions,
mutations
};
// Galaries.vue
// You didn't pass token when calling function `fetchImages`.
created() {
this.fetchImages(); // missing token here
}
I recommend use token as environment variable for security reason. Never public your token. Should NOT pass it as an argument of a function. Store your token in a .env file. You can rewrite your fetchImages as below.
fetchImages(token) {
return axios.get(`${ROOT_URL}/3/account/me/image/`, {
headers: {
Authorization: `Bearer ${process.env.TOKEN}`
}
});
},

Express [Nest.js] - res.cookie() not set in Heroku production

I am using Next.js (Frontend) and Nest.js (Backend) Frameworks and I am having issues with a HTTP Cookies. The following code works in development trough localhost, but not when publishing the code to vercel (Next.js) and Heroku (Nest.js)
Here is a snippet of my Nest.js Setup
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new LoggerService(),
cors: true,
});
app.use(cookieParser());
app.enableCors({
origin: ["<my-site>"],
credentials: true,
});
...
await app.listen(process.env.PORT || 5000);
}
bootstrap();
Here is my Route:
#Post("login/verify")
#HttpCode(201)
#ApiResponse({ description: "returns JWT for verified login attempt" })
async verify(
#Body() method: VerificationDto,
#Req() req: Request,
#Res({ passthrough: true }) res: Response
) {
const { accessToken } = await this.userService.verifyLogin(method);
const origin = req.get("origin");
this.loggerService.log(`origin: ${origin}`);
res
.set("Access-Control-Allow-Credentials", "true")
.set("Access-Control-Allow-Origin", origin)
.cookie("accessToken", accessToken, {
expires: new Date(new Date().getTime() + 60 * 1000 * 60 * 2),
sameSite: "none",
secure: true,
httpOnly: false,
});
return {
accessToken,
refreshToken,
};
}
Here is my useAuth component in Next.js
import React from "react";
import jwtDecode from "jwt-decode";
import nookies from "nookies";
import { redirect } from "../../services/redirect";
import { GetServerSideProps, NextPage } from "next";
export interface User {
accessToken: string;
email: string;
exp: number;
iat: number;
id: string;
name: string;
roles: Array<string>;
}
const AuthContext = React.createContext<User>(null as unknown as User);
const loginRoute = `/login`;
export const authenticate = (
getServerSidePropsInner: GetServerSideProps = async () => ({ props: {} })
) => {
const getServerSideProps: GetServerSideProps = async ctx => {
const { req, res } = ctx;
if (!req.headers.cookie) {
console.log("no cookie found");
redirect(ctx, loginRoute);
return { props: {} };
}
const { accessToken } = nookies.get(ctx);
console.log(accessToken);
let user = null;
try {
user = {
accessToken,
...(jwtDecode(accessToken as string) as object),
};
} catch (e) {
console.log(e);
redirect(ctx, loginRoute);
}
const result = await getServerSidePropsInner(ctx);
return {
...result,
props: {
user,
//#ts-ignore
...result.props,
},
};
};
return getServerSideProps;
};
export const withAuth = (C: NextPage) => {
const WithAuth = (props: any) => {
const { user, ...appProps } = props;
return (
<AuthContext.Provider value={user}>
<C {...appProps} />
</AuthContext.Provider>
);
};
WithAuth.displayName = `WithAuth(${C.displayName})`;
return WithAuth;
};
export default withAuth;
export const useAuth = (): User => React.useContext(AuthContext);
I am using axios with the following login request
verifyLogin: ({ code, email }: { code: string; email: string }) => {
return axios(`${apiURI}/user/login/verify`, {
method: `POST`,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
data: {
code: code,
email: email,
},
});
},
Setup works with localhost. It adds a http cookie which is read by the next.js application.
However, when deploying my Nest.js app to Heroku, the cookie does not seem to send to the next application. I have tried adding different values for the CookieOptions
sameSite: "none",
secure: true,
httpOnly: false,
However, this did not help either.
Any help is much appreciated as I am fighting with this for days.

Apollo client onError not retrying request

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);
}
})();
});
}
});

apiplatform - react admin: token in localstorage null on redirect, have to refresh page

I'm using the stack apiplatform and react admin.
My JWT authentification works fine on apiplatform.
I try to use it on my react admin backoffice.
I followed those documentations:
https://api-platform.com/docs/admin/authentication-support/
https://marmelab.com/react-admin/doc/2.9/Authentication.html
The authentification/authorization works but on login success, I am redirected to the backoffice and I have a server communication error because the token send to the api is null.
I see it in the localstorage and if I refresh the page, everything works fine.
It's like the redirection on success happen before the token was store.
Here is my code:
App.js
import React from "react";
import { HydraAdmin, ResourceGuesser } from "#api-platform/admin";
import authProvider from "./components/authProvider";
import parseHydraDocumentation from "#api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation";
import {
dataProvider as baseDataProvider,
fetchHydra as baseFetchHydra
} from "#api-platform/admin";
import { Redirect } from "react-router-dom";
const entrypoint = "http://localhost:8089/api";
const fetchHeaders = {
Authorization: `Bearer ${window.localStorage.getItem("token")}`
};
const fetchHydra = (url, options = {}) =>
baseFetchHydra(url, {
...options,
headers: new Headers(fetchHeaders)
});
const apiDocumentationParser = entrypoint =>
parseHydraDocumentation(entrypoint, {
headers: new Headers(fetchHeaders)
}).then(
({ api }) => ({ api }),
result => {
switch (result.status) {
case 401:
return Promise.resolve({
api: result.api,
customRoutes: [
{
props: {
path: "/",
render: () => <Redirect to={`/login`} />
}
}
]
});
default:
return Promise.reject(result);
}
}
);
const dataProvider = baseDataProvider(
entrypoint,
fetchHydra,
apiDocumentationParser
);
export default () => (
<HydraAdmin
apiDocumentationParser={apiDocumentationParser}
dataProvider={dataProvider}
authProvider={authProvider}
entrypoint={entrypoint}
>
<ResourceGuesser name="resource" />
</HydraAdmin>
);
authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK, AUTH_ERROR } from "react-admin";
export default (type, params) => {
if (type === AUTH_LOGIN) {
const { email, password } = params;
const request = new Request("http://localhost:8089/api/login_check", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: new Headers({ "Content-Type": "application/json" })
});
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response.json();
})
.then(({ token }) => {
localStorage.setItem("token", token);
});
}
if (type === AUTH_LOGOUT) {
localStorage.removeItem("token");
return Promise.resolve();
}
if (type === AUTH_ERROR) {
console.log("AUTH_ERROR");
//localStorage.removeItem("token");
return Promise.resolve();
}
if (type === AUTH_CHECK) {
return localStorage.getItem("token")
? Promise.resolve()
: Promise.reject({ redirectTo: "/login" });
}
return Promise.resolve();
};
I don't know if it's the right solution because like you, I didn't find anything about this problem.
But for me, if I just call window.location.reload(); after localStorage.setItem('token', token); it solves the problem for me, because after login, it reloads admin and at that moment, it can recognize token. Maybe it's not the cleanest solution ever but it works well.
By the way, I think, this is not a problem related with the HydraAdmin component, I tried the classic React Admin component and the problem is still there, so it's related to React Admin.

All apollo query errors throw a network 400 error

How do I globally handle my query and mutation errors? When I use a malformed graphql query, in a mutation or query, my react-native app always throws the following error.
Unhandled (in react-apollo:Apollo(MyScreen)) Error: Network error:
Response not successful: Received status code 400
Running the same queries on the graphiql endpoint, provide relevant errors however. While the goal is globally handled errors, local query errors also do not work for me. Printing them shows nothing.
//MyComponent.js
class MyComponent extends React.Component {
render () {
/*
outputs
{
error: undefined
loading: true
networkStatus: 1
}
*/
console.log(this.props.data);
//outputs undefined
console.log('error', this.props.error);
//outputs undefined
console.log('errors', this.props.errors);
return (
<Container>
{this.props.myData}
<Button block onPress={this.props.onPress}>
<Text>Button</Text>
</Button>
</Container>
)
}
}
const mapDispatchToProps = (dispatch, {navigation: {navigate}}) => ({
onPress: async rest => {
//do stuff
}
});
export default connect(null, mapDispatchToProps)(MyDataService.getMyDataInjector(MyComponent));
//MyDataService.js
import { graphql } from 'react-apollo';
import getApolloClient from '../../../apolloClient';
import { dataFragment } from './dataFragments';
import gql from 'graphql-tag';
export default MyDataService = {
getMyDataInjector: graphql(gql`
query {
myData {
...dataFragment
}
}
${dataFragment}
`,
{
props: ({ data, errors, error }) => ({
data,
loading: data.loading,
myData: data.myData,
errors,
error,
})
}),
addData: async (data) => {
const res = await apolloClient.mutate({
mutation: gql`
mutation addData($data: String!) {
addData(data: $data) {
...dataFragment
}
}
${dataFragment}
`,
variables: {
data,
}
});
return res.data.addData;
},
};
//apolloClient.js
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
let apolloClient;
const initApolloClient = store => {
const httpLink = new HttpLink({uri: 'http://192.168.0.11:3000/graphql'})
// const errorLink = onError(({ response, graphQLErrors, networkError }) => {
// console.log('graphql error in link', graphQLErrors);
// console.log('networkError error in link', networkError);
// console.log('response error in link', response);
// if (graphQLErrors)
// graphQLErrors.map(({ message, locations, path }) =>
// console.log(
// `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
// ),
// );
// if (networkError) console.log(`[Network error]: ${networkError}`);
// });
const authLink = setContext((_, { headers }) => {
const {accessToken: {type, token}} = store.getState().signedInUser;
return {
headers: {
...headers,
authorization: type && token ? type + ' ' + token : null
}
}
});
apolloClient = new ApolloClient({
// using error link throws - Error: Network error: forward is not a function
// link: authLink.concat(errorLink, httpLink),
link: authLink.concat(httpLink),
// doesn't console log
// onError: (e) => { console.log('IN ON ERROR', e.graphQLErrors) },
cache: new InMemoryCache({
dataIdFromObject: o => (o._id ? `${o.__typename}:${o._id}`: null),
}),
// turning these on do nothing
// defaultOptions: {
// query: {
// // fetchPolicy: 'network-only',
// errorPolicy: 'all',
// },
// mutate: {
// errorPolicy: 'all'
// }
// }
});
return apolloClient;
}
export default getApolloClient = () => apolloClient;
export { initApolloClient };
was able to get it working by using apollo-link with onError.
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { onError } from 'apollo-link-error';
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory';
let apolloClient;
const initApolloClient = store => {
const httpLink = new HttpLink({uri: 'http://192.168.0.11:3000/graphql'})
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.error(
`[GraphQL error]: Message: ${message}, Location: ${JSON.stringify(locations)}, Path: ${path}`,
),
);
}
if (networkError) console.error(`[Network error]: ${networkError}`);
});
const authLink = setContext((_, { headers }) => {
const {accessToken: {type, token}} = store.getState().signedInUser;
return {
headers: {
...headers,
authorization: type && token ? type + ' ' + token : null
}
}
});
const link = ApolloLink.from([
authLink,
errorLink,
httpLink,
]);
apolloClient = new ApolloClient({
link,
cache: new InMemoryCache({
//why? see https://stackoverflow.com/questions/48840223/apollo-duplicates-first-result-to-every-node-in-array-of-edges/49249163#49249163
dataIdFromObject: o => (o._id ? `${o.__typename}:${o._id}`: null),
}),
});
return apolloClient;
}