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.
Related
I am following Ben Awads Reddit Clone Tutorial and have a problem with setting a cookie.
I have an express app and I am trying to set a cookie if the user provides a correct username and password to my GraphQL mutation. When I use Apollo GraphQLs Sandbox to send a login request. I can see in the Network Tab, that there is a Set-Cookie Header with the correct data. But when I send a request to see if there is another query and check for req.session.userId it is undefined.
Here is my index.ts file:
import 'reflect-metadata';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { PrismaClient } from '#prisma/client';
import { buildSchema } from 'type-graphql';
import { PostResolver } from './resolvers/post';
import { UserResolver } from './resolvers/user';
import { __prod__ } from './constants';
import { MyContext } from './types';
import session from 'express-session';
import connectRedis from 'connect-redis';
import { createClient } from 'redis';
const prisma = new PrismaClient();
const main = async () => {
const app = express();
const RedisStore = connectRedis(session);
const redisClient = createClient({ legacyMode: true });
redisClient.connect().catch(console.error);
app.use(
session({
name: 'qid',
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
httpOnly: true,
sameSite: 'lax', // lax für csrf
secure: __prod__, // cookie only works in https
},
secret: 'keyboard cat',
store: new RedisStore({ client: redisClient }),
saveUninitialized: false,
resave: false,
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }): MyContext => ({ prisma, req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({ app });
app.listen(4000, () => {
console.log('Server is running on http://localhost:4000');
});
};
And here is my users resolver:
#Resolver()
export class UserResolver {
// create a me query
#Query(() => UserType, { nullable: true })
async me(#Ctx() { prisma, req }: MyContext) {
// you are not logged in
if (!req.session.userId) {
return null;
}
const user = await prisma.user.findUnique({
where: {
id: req.session.userId,
},
});
return user;
}
#Mutation(() => UserResponse)
async login(
#Arg('options') options: UsernamePasswordInput,
#Ctx() { prisma, req }: MyContext
): Promise<UserResponse> {
const user = await prisma.user.findUnique({
where: {
username: options.username,
},
});
if (!user) {
return {
errors: [
{
field: 'username',
message: 'That username does not exist',
},
],
};
}
if (!(await argon2.verify(user.password, options.password))) {
return {
errors: [
{
field: 'password',
message: 'Incorrect password',
},
],
};
}
req.session.userId = user.id;
return {
user,
};
}
}
I tried using different versions of the dependencies "connect-redis", "redis" and "express-session"
I have a module whith the connection:
import { createContext } from 'react';
import SInfo from 'react-native-sensitive-info';
import io from 'socket.io-client';
export const socket = io('http://192...', {
forceNew: true,
auth: {
token: 'eyJhbGciO...'
},
});
export const SocketContext = createContext(undefined);
But my token is saved by lib react-native-sensitive-info and i need to get it by
await SInfo.getItem('token', {});
My question is: How can I use await to get the token I need to export the socket?
Something like this:
export const socket = io('http://192...', {
forceNew: true,
auth: {
token: await SInfo.getItem('token', {});
},
});
Thanks in advance :D
Try this:
export const socket = async () => {
const token = await SInfo.getItem('token', {});
return io('http://192...', {
forceNew: true,
auth: {
token,
},
});
}
or
export const createSocket = (token) => {
return io('http://192...', {
forceNew: true,
auth: {
token,
},
});
}
and then use it in this way:
const token = await SInfo.getItem('token', {});
const socket = createSocket(token)
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);
I'm trying to make a smart request in nuxt with nuxt-apollo-module in order to grab my routes for the nuxt-sitemaps-module (so I can create my sitemap with them).
I need to make this request from within nuxt.config.js file. I have tried this way with no luck (as app doesn't exist in this context). What would be the right way to do this?
Thanks in advance!
The relevant part of my nuxt.config.js
import gql from 'graphql-tag'
module.exports = {
modules: [
'#nuxtjs/apollo',
'#nuxtjs/sitemap'
],
apollo: {
clientConfigs: {
default: {
httpEndpoint: 'https://example.com/graphql'
}
}
},
sitemap: {
path: '/sitemap.xml',
hostname: 'https://example.com/',
generate: true,
cacheTime: 86400,
trailingSlash: true,
routes: async ({ app }) => {
const myRoutes = ['/one-random-path/']
let client = app.apolloProvider.defaultClient
let myProductsQuery = gql`query {
products {
slug
}
}`
let myBrandsQuery = gql`query {
brands {
slug
}
}`
const myProducts = await client.query({ query: myProductsQuery })
const myBrands = await client.query({ query: myBrandsQuery })
return [myRoutes, ...myProducts, ...myBrands]
}
}
}
I was able to generate it this way, but I'm sure there is a better way.
yarn add node-fetch apollo-boost
sitemap: {
routes: async () => {
const routes = []
const fetch = require("node-fetch")
const { gql } = require("apollo-boost")
const ApolloBoost = require("apollo-boost")
const ApolloClient = ApolloBoost.default
const client = new ApolloClient({
fetch: fetch,
uri: YOUR_API_ENDPOINT,
})
const fetchUsers = gql`
query {
users {
id
}
}
`
const users = await client
.query({
query: fetchUsers,
})
.then((res) => res.data.users)
users.forEach((user) => {
routes.push({
route: `/${user.id}`,
})
})
return routes
},
},
I gave up using Apollo. It was easier to use Axios. Moreover, no nneds to configure #nuxtjs/sitemap :
import axios from 'axios'
sitemap: {
hostname: URL_SITE,
gzip: true,
},
routes() {
return axios({
url: ENDPOINT,
method: 'post',
data: {
query: `
query GET_POSTS {
posts {
nodes {
slug
}
}
}
`,
},
}).then((result) => {
return result.data.data.posts.nodes.map((post) => {
return '/blog/' + post.slug
})
})
}
This was how I was able to do it with authentication. Got round to it by following the documentation here which had a Vue example: https://www.apollographql.com/docs/react/networking/authentication/
Might be a cleaner way to do it with apollo-boost if you can also use auth?
import fetch from 'node-fetch'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, concat } from 'apollo-link'
import { InMemoryCache } from 'apollo-cache-inmemory'
import globalQuery from './apollo/queries/global.js'
const httpLink = new HttpLink({ uri: process.env.SCHEMA_URL, fetch })
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = process.env.STRAPI_API_TOKEN
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : 'Bearer',
},
})
return forward(operation)
})
export const apolloClient = new ApolloClient({
link: concat(authMiddleware, httpLink),
cache: new InMemoryCache(),
})
export default async () => {
let global = null
const globalResponse = await apolloClient.query({ query: globalQuery })
if (globalResponse?.data?.global?.data) {
global = globalResponse.data.global.data.attributes
}
console.log('globals:::', global)
}
I used passportjs in the past with expressjs and currently I'm trying to incorporate it with Sapper app but I'm unable to figure out how to inlcude the passport.authenticate() in my route because it's a sapper route not an express route. Also if I try to run everything in my server.js file I run into the issue of how to integrate it with the sapper middleware.
How do you use passport.authenticate() in/with Sapper middleware or sapper routes js files (which is the front not server routes)?
My server.js is typical:
const sirv = require('sirv');
import express from 'express';
var cookieParser = require('cookie-parser');
import * as sapper from '#sapper/server';
const session = require('express-session');
var passport = require('passport');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/passport', {
useNewUrlParser: true });
const MongoStore = require('connect-mongo')(session);
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
store: new MongoStore({ url: 'mongodb://localhost/passport' }),
cookie: { secure: false, maxAge: 1000 * 60 * 60 * 24 * 7 }
}));
app.use(passport.initialize());
app.use(passport.session());
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
const assets = sirv('static', {
maxAge: 31536000, // 1Y
immutable: true
});
app.use(assets, sapper.middleware({
session: req => ({
user: req.session && req.session.user
})})).listen(process.env.PORT, err => { if (err) console.log('error', err); });
As you can see, Sapper is just a middleware so if I want to authenticate a user and send it to the front/sapper, I need to figure out how to run passport.authenticate() inside the middleware function, right?
If I want to use passport in the route JS file which is sapper front route:
//How to import passport.js here to make passport.authenticate() middleware available?
import passport from './passport';
import User from './mongoso';
export async function post(req, res, next) {
res.setHeader('Content-Type', 'application/json');
/* Retrieve the data */
var data = req.body;
req.session.user = data.email;
console.log("Here's the posted data:", data);
console.log("information in the session is:", req.session);
/* Returns the result */
return res.end(JSON.stringify({ Email: req.session.user }));
//return res.json({ data: data });
}
Any ideas? Greatly appreciated if someone out there could help.
You don't need to run passport.authenticate() inside the sapper.middleware. You need to add passport-local strategy firstly, then do serializeUser and deserializeUser, then create routes to do passport.authenticate and
after that catch req.session.passport object in sapper.middleware. I don't use passport-local strategy, but here is my working server.js with passport-github strategy.
//server.js
import sirv from 'sirv';
import express from 'express';
import passport from 'passport';
import { Strategy } from 'passport-github';
import bodyParser from 'body-parser';
import session from 'express-session';
import sessionFileStore from 'session-file-store';
import compression from 'compression';
import * as sapper from '#sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
const FileStore = sessionFileStore(session);
passport.use(new Strategy({
clientID: 'someClientID',
clientSecret: 'someClientSecret',
callbackURL: 'http://localhost:3000/auth/callback',
}, (accessToken, refreshToken, profile, cb) => {
// console.log('success');
return cb(null, profile);
}));
passport.serializeUser(function (user, cb) {
cb(null, user);
});
passport.deserializeUser(function (obj, cb) {
cb(null, obj);
});
const expressServer = express()
.use(passport.initialize())
.use(bodyParser.json())
.use(session({
secret: 'conduit',
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 31536000
},
store: new FileStore({
path: `.sessions`
})
}))
.get('/auth/login',
passport.authenticate('github'))
.get('/auth/callback',
passport.authenticate('github', { failureRedirect: '/auth/login' }),
(req, res) => {
res.redirect('/');
//console.log(req.user.username);
})
.get('/auth/logout', (req, res) => {
req.logout();
req.session.destroy( function (err) {
res.redirect('/');
});
})
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware({
session: req => {
const user = req.session.passport ? req.session.passport.user.username : null;
// console.log(req.session.passport.user.username);
return { user };
}
})
)
if (dev) {
expressServer.listen(PORT, err => {
if (err) console.log('error', err);
});
}
export { expressServer }
Аfter this, you can catch that this { user } object in your client
sapper route component through Stores using const { session } = stores(); console.log($session) or you can get it via special preload function to apply before page is rendered, like this for example in index.svelte
<script context="module">
export function preload(page, { user }) {
return { user };
}
</script>
<script>
import { stores } from "#sapper/app";
import { onMount } from "svelte";
const { session } = stores();
export let user;
onMount(() => {
console.log($session);
});
</script>
<div>
{#if !user}
<p>Not logged in</p>
{:else}
<p>Logged in!</p>
{/if}
</div>
Here i use two approaches same time, but most of time it will be enough to
use preload, no need to direct access to session in stores.
Hope this will help you. Good luck!
I used the answer from DioXine to implement Google Auth.
The cookie is now also http only.
import sirv from "sirv";
import express from "express";
import bodyParser from "body-parser";
import session from "express-session";
import sessionFileStore from "session-file-store";
import compression from "compression";
import * as sapper from "#sapper/server";
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === "development";
passport.use(
new GoogleStrategy(
{
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/google/callback",
},
function (accessToken, refreshToken, profile, cb) {
// User.findOrCreate({ googleId: profile.id }, function (err, user) {
// return cb(err, user);
// });
return cb(null, profile);
}
)
);
passport.serializeUser(function (user, cb) {
cb(null, user);
});
passport.deserializeUser(function (obj, cb) {
cb(null, obj);
});
const FileStore = sessionFileStore(session);
const sessionConfig = {
secret: "sefmvks4Fgblolf4sdJHBd",
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
maxAge: 31536000,
},
//TODO: redis
store: new FileStore({
path: `.sessions`,
}),
};
express()
.use(passport.initialize())
.use(bodyParser.json())
.use(session(sessionConfig))
.get("/auth/google", passport.authenticate("google", { scope: ["profile"] }))
.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/auth/login" }),
(req, res) => {
res.redirect("/");
}
)
.get("/auth/logout", (req, res) => {
req.logout();
req.session.destroy(function (err) {
res.redirect("/");
});
})
.use(
compression({ threshold: 0 }),
sirv("static", { dev }),
sapper.middleware({
session: (req) => {
const user = req.session.passport ? req.session.passport.user.id : null;
return { user };
},
})
)
.listen(PORT, (err) => {
if (err) console.log("error", err);
});
This is not changed:
<script context="module">
export function preload(page, { user }) {
return { user };
}
</script>
<script>
import { stores } from "#sapper/app";
import { onMount } from "svelte";
const { session } = stores();
export let user;
onMount(() => {
console.log($session);
});
</script>
<div>
{#if !user}
<p>Not logged in</p>
{:else}
<p>Logged in!</p>
{/if}
</div>
If it only works after refresh check this:
https://github.com/sveltejs/sapper/issues/567#issuecomment-542788270