Closed stavroslee closed 3 years ago
@stavroslee Does passwordless login work locally? What is value for COOKIE_DOMAIN
? It should be of format .yourdomain.com
and I think all subdomains will be included. I haven't tried deploying APP
and API
to different domains.
Sorry for not starting with that info.
Yes it works fine when both are on localhost. for COOKIE_DOMAIN I have tried a full url (https://xxxx.vercel.app) partial (vercel.app and .vercel.app)
So when the user clicks the email to login it is a link to the express server (heroku in my case). This sets a cookie on the browser and that cookie is not available when it calls the nextjs server (since they are on different domains).
additionally I found out that putting both on heroku and setting the cookie domain to .herokuapp.com wont share the cookie because of https://devcenter.heroku.com/articles/cookies-and-herokuapp-com
@stavroslee Thanks for more info. Still I am not sure what code throws error. Can you provide error's message on APP
and on API
. I think it should work even on different domains but haven't tried it yet.
passwordless
, when successful, will populate req.user
with proper object on API
server. When you load APP
in your browser, you will be logged-in because APP
will send request to API
and check for req.user
.
Do you have a CORS error by any chance? What are your value for PRODUCTION_URL_APP
and PRODUCTION_URL_API
? Does redirect to APP
works after you click on link that leads to API
?:
res.redirect(
`${dev ? process.env.URL_APP : process.env.PRODUCTION_URL_APP}${redirectUrlAfterLogin}`,
);
You can add console.log
statements into passwordless
setup code:
import * as passwordless from 'passwordless';
import sendEmail from './aws-ses';
import getEmailTemplate from './models/EmailTemplate';
import User from './models/User';
import PasswordlessMongoStore from './passwordless-token-mongostore';
import Invitation from './models/Invitation';
function setupPasswordless({ server }) {
const mongoStore = new PasswordlessMongoStore();
const dev = process.env.NODE_ENV !== 'production';
passwordless.addDelivery(async (tokenToSend, uidToSend, recipient, callback) => {
try {
const template = await getEmailTemplate('login', {
loginURL: `${
dev ? process.env.URL_API : process.env.PRODUCTION_URL_API
}/auth/logged_in?token=${tokenToSend}&uid=${encodeURIComponent(uidToSend)}`,
});
await sendEmail({
from: `Kelly from saas-app.builderbook.org <${process.env.EMAIL_SUPPORT_FROM_ADDRESS}>`,
to: [recipient],
subject: template.subject,
body: template.message,
});
callback();
} catch (err) {
console.error('Email sending error:', err);
callback(err);
}
});
passwordless.init(mongoStore);
server.use(passwordless.sessionSupport());
server.use((req, __, next) => {
if (req.user && typeof req.user === 'string') {
User.findById(req.user, User.publicFields(), (err, user) => {
req.user = user;
console.log('passwordless middleware');
next(err);
});
} else {
next();
}
});
server.post(
'/auth/email-login-link',
passwordless.requestToken(async (email, __, callback) => {
try {
const user = await User.findOne({ email })
.select('_id')
.setOptions({ lean: true });
if (user) {
callback(null, user._id);
} else {
const id = await mongoStore.storeOrUpdateByEmail(email);
callback(null, id);
}
} catch (error) {
callback(error, null);
}
}),
(req, res) => {
if (req.query && req.query.invitationToken) {
req.session.invitationToken = req.query.invitationToken;
} else {
req.session.invitationToken = null;
}
res.json({ done: 1 });
},
);
server.get(
'/auth/logged_in',
passwordless.acceptToken(),
(req, __, next) => {
if (req.user && typeof req.user === 'string') {
User.findById(req.user, User.publicFields(), (err, user) => {
req.user = user;
next(err);
});
} else {
next();
}
},
(req, res) => {
if (req.user && req.session.invitationToken) {
Invitation.addUserToTeam({
token: req.session.invitationToken,
user: req.user,
}).catch((err) => console.error(err));
req.session.invitationToken = null;
}
let redirectUrlAfterLogin;
if (req.user && !req.user.defaultTeamSlug) {
redirectUrlAfterLogin = '/create-team';
} else {
redirectUrlAfterLogin = `/team/${req.user.defaultTeamSlug}/discussions`;
}
res.redirect(
`${dev ? process.env.URL_APP : process.env.PRODUCTION_URL_APP}${redirectUrlAfterLogin}`,
);
},
);
server.get('/logout', passwordless.logout(), (req, res) => {
req.logout();
res.redirect(`${dev ? process.env.URL_APP : process.env.PRODUCTION_URL_APP}/login`);
});
}
export { setupPasswordless };
@tima101 thanks for the quick follow up. I was experimenting with this to confirm my issue.
When I click the link from the email, passwordless works. It finds the email token and sets the cookie for the session. This cookie however is tied to the domain of API. The client is then redirected to the proper PRODUCTION_URL_APP and thats where I am then redirected to /login (since nextjs is not authenticated).
when APP calls API (when the nextjs server makes a call to the express server) there is no user on the request (req.user) I tracked this down to the fact that the browser does not send the session cookie to APP. I confirmed the cookie headers are copied from the browser to nextjs when a request is recieved. I believe thats because the browser does not share the cookie from API with APP since they're on different domains.
I confirmed the proper login cookie exists on the browser for API (in my case with chrome seen by right clicking > Inspect > clicking the application tab > expanding cookies under the Storage header). As I read up on this browsers wont expose cookies to different domains.
To answer the rest of your questions: I am using CORS and as far as I can tell its working correctly (the socket IO connections are working)
Again thank you for being responsive.
@stavroslee I can look at your codebase if you like. Did you make any changes other than env variables?
Does Google OAuth work for you?
I can try deploying APP
to aaa.builderbook.org
and API
to bbb.async-await.com
but this is not my priority.
I have made a few other changes - like instead of having different env vars like URL_APP and PRODUCTION_URL_APP I just have URL_APP and its up to the configuration of the server.
edit to answer your question: I have not tried google oauth and had removed it for now.
I don't think you should prioritize the different domains if its not a priority for you, I was hoping others had already and I just had configuration issues. Can you drop a note when you do try it out?
@stavroslee I am curious to know if you tried removing cookie.domain
property from:
cookie: {
httpOnly: true,
maxAge: 14 * 24 * 60 * 60 * 1000, // expires in 14 days
domain: process.env.COOKIE_DOMAIN,
} as any,
Hi @tima101 . I did. To keep moving forward, I've had to use passwordless JWT so I could pass the token around more easily.
Let me know if I should close this issue.
@stavroslee It can stay open, someone may reproduce and suggest solution.
When you create your new DNS records for APP
and API
apps, do you have the same root domain for them or different? For example, app.domain1.com/api.domain1.com or app.domain1.com/api.domain2.com?
I had different root domains .herokuapp.com and .vercel.app
@stavroslee So it will never have more user friendly domain name? If it will, will APP
and API
have the same domain?
I understand you question now.
It will have a nicer domain name (for APP). I wasn't planning on a nicer domain name for API. If you tell me that would solve my problem, I would look into it.
@stavroslee Yes, Passwordless API should work for app.domain1.com
and api.domain1.com
combo.
I was attempting to deploy the servers on 2 different services (vercel for nextjs and heroku for the express server) when I ran into a problem. When clicking the login email I was correctly getting my session cookie set but when the browser is redirected to the nextjs server it does not have the cookie and therefore can never log in.
I tried many variations of setting the COOKIE_DOMAIN to be the full URL of the nextjs app but chrome kept the cookie tied to my heroku domain (the express server).
Has anyone else had this issue or had it work correctly?
At this point I'm not sure how it could work if the servers are not on the same domain (unless the cookies are not httpOnly and its explicitly passed to nextjs)
Thanks in advance.
Click to see Hill for issue #124
created by Async