To handle the case when someone becomes a user of a paid web application, "we", as developers, need to take several steps:
- add them to the database
- verify their email
- send them a welcome message <- server
- charge them
- redirect them
- is there something else?
Most dev guides cover "add them to the database" and "redirect them". I have created all of the steps in my application. Naively, I took all of my steps and plopped into the route handler for a new user.
As I stand on the precipice of really-this-matters development, I am wondering:
- Does the order of operations matter? (obviously, we don't want to redirect to an authenticated area before we actually make an account and charge them, but should we wait to create the account until they click on the "confirm" email?)
- Do you have a story from your experience?
- If you verify email through verification service, do you still want to send a verify/confirm email email?
- Am I missing something huge?
Top comments (47)
It's a good idea to have an
is_active
flag on users. This way you can create them but not allow access until they've confirmed/paid up, and should it become necessary to disable their access later you can do it without nuking their entire account and dealing with whatever foreign key mayhem ensues.Thanks for your comment, Dian!
I agree to having an
is_active
flag. Do you create users before confirmation in your apps? Do you have a flow that is like, "Hey, wanna join, enter your email"form collecting email
-> create temp user with email, isactive=false, and token -> send email with button to confirm -> when user clicks on button -> go to website with token and get redirected toform collecting rest of stuff
?My situation's a bit different since the stuff I work on is all enterprise software. We recently offloaded all our user management onto a single sign-on provider, but before that we did collect their information on signup and send them an activation/password reset email with a token (token hash and expiry stored in the database for verification). We didn't have a second stage, but then we weren't charging individual users.
Thanks for sharing your experience!
I would like to suggest that in stead of is_active, you call this property of user just active. (Boolean) this is more correct for many reasons.
What are those reasons? I find
is_*
clear for boolean fields since it makes it easy to identify them as answering a yes-or-no question, and where naming things isn't about clarity it's really down to taste.The word is is redundant.
User.active = true
Why would you add it? You can tell that it is yes/no by the field/property type being Boolean. It's like putting 'equals' in your property name.
It's not always obvious that something is a boolean unless you're looking at a table or class definition. Redundancy isn't universally evil: here it means you don't have to look up the type if you're just looking at usage.
thanks for your thoughts. I'll have to look at the naming conventions.
This also covers the case when the money are refunded (for any reason), or they brake the user policy of somekind (revoke access).
Yes, generally, you still want to use a confirmation email even if you are using a verification service (we use BrightVerify at my company) because you want to make sure the person who signed up is actually the person who has access to the email and isn't someone just entering their email in to get them spammed by you or just generally pretending to be be someone else.
Thanks for your thoughts here, Richard.
Follow up question on your order of operations: Do you send the confirmation email immediately and then finish your user onboarding AFTER they respond, or do you create your new user (getting all their info) and then send the confirmation email? and then handle (somehow) deleting the user if they don't confirm?
For the process I'm thinking of, I think I'd have to wait to finish onboarding to avoid collecting money from people who don't have access to the email that they gave us. However, I am super curious how other people handle it.
Jess. Create the user the moment they enter your form. In the form, only ask for minimal user info needed for a valid account. Name and email for instance. Let them add all user metadata / fields such as address and phone after they fully registered/confirmed. When users decide on their own to provide their info in stead of it being mandatory during registration they have a more positive experience, communicate more open, are more likely to trust you. Youre right. Never accept payments before email is confirmed and do not even ask for payment details in the sign up form. It should not be possible to pay without verification.Let the user add this information manually after their email is verified and they are logged in. Not only will nobody be able to say they never gave you that info (after all they consciously added it manually) and it also increases the likelihood of a purchase/lead whatever you're doing. This has a number of reasons well known to marketing and lead generation minded people.
This may sound obvious but do not send the confirmation email from the sign up page script. So don't call your mail() function from the page that handles the POST data of the form. Separate the two always. Create a task of some sorts, even if it gets executed almost immediately don't let the same process be responsible.
Thanks for your thoughts. I was thinking that the "send confirmation email" is it's own function but is called kinda like so in the route handler -> action is createUser -> go to /api/user/new -> call createUser which ... checks to see if user already exists.then(create user).then(send confirmation email).then(go to login page or something else) - However, I'm not sure if you are saying that I should exit the createUser function and say now, go to send confirmation email.
I think what he means is that sending the e-mail should be done asynchronously in a separate thread/process/worker, thus not blocking your main thread. You can return a response to the user while the e-mail is still being sent in the background.
Thanks for your thoughts Christian!
Just to throw a word of caution here: There are situations where redirects can be intercepted and exploited as a vulnerability. I'm not in a good position to go into length on this at the moment but if someone wants to add some insight here, please do!
P.S. I'm definitely overly cautious in this area. If anyone wants to tell me I'm wrong to think this could be a point of vulnerability, let me know.
This is a part of your process where security should be front and center. It always should be in a sense, but especially here. Of course, HTTPS and ensuring the verification process verifies what it's supposed to verify are other things to take a lot of care over.
I'd love to read the rest of what folks have to say. This was just a quick reaction.
Yes, I think caution is reasonable here. I hope to learn always from other developer approaches and patterns. Mostly, I care about why. :)
Regarding the dangers of redirects, I think Ben is talking about open redirects, where your taking the URL or part of it from user input. If you don't validate this, an attacker can create a link that looks like a link to your site, but that actually takes the user to another site.
For example, it's very common to redirect users back to the page they were viewing when they logged in.
Let's say a user visits
which requires login, so you redirect them to
After logging in, you take that
next
parameter from the URL and redirect them there.If you're not careful to validate that that parameter starts with a single slash (which indicates that it is a path within your site), an attacker could give someone a link to
which causes your server to redirect the user to
http://evil.com
Thanks for the description of this, Frederik! That is good to protect against. I think I've covered this risk, but I should verify.
How I handle that
next
url: in the auth middleware, if the check fails, I store the path of the request in the session before redirecting tologin
. Then, in the login handler, retrieve the path from the session and redirect. That way, we're guaranteed that the path is on our site AND it can't be modified.Nice. Thanks for sharing your approach. I have seen devs (in videos) have a catchall route for any route that doesn't match something on the server or client -> if any route is passed to
domain.com/someroute
that doesn't match an explicit route just goes todomain.com/
- but catching it in the next would ensure that they go to the right place and not just back to the homepage.One thing you see more and more in cases like this is a publish/subscribe system of events, so different parts of your site can react to things that happen. For example, your main route handler might take care of storing the user's data and then emit a
signup
event that other parts of the system can then take action on.You might have a
billing
subsystem that creates the user's subscription and charges them for the first time, and anemails
subsystem that takes care of sending the welcome email.This might be implemented within your codebase using a
signals
module built into the web framework you use, or you can use an external application like Redis pubsub or Apache KafkaWe wrote remit as a nice wrapper around RabbitMQ for exactly this purpose.
I'll check it out. Thanks!
Interesting. Thanks for sharing your thoughts!
I’ve been experimenting with multiple ways for users to sign up and I found out one, which actually increases the conversion rate. Which takes the following steps:
However this method is not suitable for all situations. I strongly recommend it when you want the email of your users to be the username and you don’t need any additional indispensable info about your users.
Thanks for this detailed response from your experience! It is very helpful.
I am reconsidering my naive flow after all of these great responses, and I really am leaning towards a structure like this. Now, I just have to figure out how to provide something meaningful to put on the page for people who are signed up but not paid members :)
Yep, you nailed it! This method uses the password as the verification process itself, which makes user feel like the process is just a step away, the email and nothing more.
So the most essential step here is to be creative when asking them for more info. once they’re logged in.
In case someone else hasn't brought this up: I think it's not unusual to allow people to log in provisionally into applications even before they've verified their e-mail. Their access is usually restricted in some way and will stop entirely if they haven't verified by a certain date... Depending on the nature of the app you're creating, maybe this would be worthwhile (usually it's a good idea if you want to lower the barrier to entry as much as possible and get people using your app right away).
That's what I meant by "it might be up to business rules". However, it opens up a door to all sorts of liabilities.
Thanks - I think that's what P Martinez was saying (and I just read that comment). I agree that seems like a good idea that I had not previously considered. I appreciate your feedback.
Depending on the legal requirements, you might want to collect and store"evidence" during the signups process, i.e. to prove when and by which ip address the sign up took place and when the subscription to a newsletter or they payments became effective. Especially here in Germany it's good to have because there are people trying to sue others for privacy claims.
In that case, it might become very valuable if you can prove that a certain ip signed up or that a validation was performed from a certain email address.
The same goes for B2B VAT handling in Europe, the seller is liable, but the buyer has to declare it (even ftom the US), so you better validate the VAT and store the response.
VAT problems are usually solved with the billing and/or shipping address.
Thanks. I'm going to use an address checking API because trying to roll your own gets so crazy.
Ooh, good call. Thanks for the insight, Philip.
What if you have a service that allows to be used without user creation and only if you want to store the results, you can then create the user? How can that be handled? Keep the info generated in memory? Use Redis?
If you want to store results and the user is not logged in or there is no user yet I would recommend you to store everything in a local cache, i.e the client. For example, on a website I would use localstorage. Then all you need is to sync it with the server as soon as he logs in.
Probably I would never use server side to store anything related to non-logged users since there are no guarantees on the user uniqueness, but it depends on the situation. That said, you could store non-logged users data on server side once, but trying to retrieve this data from/for the same non-logged user is probably not a good idea since we can’t ensure he is the same person :)
Nice approach 😎
Good questions, Pablo. That seems like a whole other can of worms. Maybe you create a temporary user in a separate database to not muckup your user database that is based only on their IP address and timestamp or something...or issue them a token that when they first start interacting with the site? I am just guessing.
Yes, the order of operations does matter. It might be up to the business model, though.
If you require an email, then you should require the email to be:
Additionally, if you want to restrict 1-to-1 relationship between user and email address, then you should also use a RegExp to filter out infinite gmail address combinations — otherwise any user will be able to register an infinite number of accounts.
If you require #4, then you must send a verification email. In that case, the order of operations is as follows:
Assuming you require an email, you shouldn't allow the user to do anything a logged-in user can do until the user clicks the confirmation link. URLs for restricted areas should be different.
Thanks for your comments, Carlos. I think I have less risk of people creating infinite accounts with my current project because it is a paid service. However, I like the way this is laid out. What do you mean URLs for restricted areas should be different? I have basically a concept of domain/actuallstuff/:userid and the route to actuallstuff checks that user is user with userid - If you meant something else, I really want to know!
Sorry for the confusing remark. What I mean is that it is cleaner and safer if domain/actualstuff is only accessed by logged-in users. If you want the world to see similar information, make it domain/almostactualstuff
Thanks for clarification!
If someone's becoming a paid member of something, you might (depending on what they're paying for) need to know who they are; that's determined by the parameters of your project.
But maybe I could sign up for a site using OAuth or some other third-party service where my identity is established without having to directly submit personal information.
I pay for a VPN which (if I so desired) I could pay for by using gift certificates purchased in cash. I can make regular micropayments to charities and organisations I support without wanting to give out my email address. I sign up for services which cost money using throw-away email accounts meaning that the email is no longer connected to me in any way after 30 minutes.
I'm not saying you're wrong, I'm just saying you're generalising requiring emails to cover all "new user" scenarios - the thing I think you're missing is that your email solution should be built in such a way that it's not a hard dependency for the rest of the application, and other modules shouldn't expect to have access to an email address or to expect it to be valid.
Thanks, Ben. Stuff like this is a real concern of mine for all projects.
For my current project, the concept was that getting email from our service is something the user should want - it's how they know what happened. However, I could rethink it such that OAuth via google or facebook lets folks in and ensure that anything we would have sent via email is presented in a notification on the service. Reasonable expectation from the perspective of a user.