I think the success rate of this system is also highly affected by the kind of public you have. Here is my experience:
I built a bar management application, people purchase products themselves on their phone with their own account. It’s a socially controlled system.
People (re)authenticate just once a year
I estimated 80% forget their password, having to use the password reset link, the magic link flow is much nicer
A password is optional, a password manager/instant login can be used
Simple registration, only a name/email required, no password
You want things to be as simple as possible at a bar
I did ask for some feedback on this, and many liked this system better for this use case. Others opted for using a password, and were fine with that as well. It doesn’t work everywhere, but I think developers should definitely consider a magic link implementation for some applications, to use the best of both worlds.
I agree it heavily depends on the type of userbase you have. For example, I prefer the traditional email address/password setup since I always use my password manager, which auto-fills the fields for me. Magic link-based authentication feels a bit more tedious to me as a user since it would mean I would always have to do the one extra step of going into my email for the link. Also, the (re)authentication once a year is an excellent idea. If it’s a phone app, one way to help prevent people from forgetting their password would be similar to what Authy does, where it gives you the option to enter your password if you want to be sure that you remember it or you can just hit ignore.
Your experience differs from mine. We implemented email based login as primary authentication as a large proportion of our helpdesk tickets were from people having trouble logging in due to their forgetting their password and even with a password reset function available to them, they wouldn’t use it unless prompted (I guess because they couldn’t possibly have forgotten their password and the system must therefore be broken.)
I digress, we use mailgun for ensuring email deliverability and have so far not had issues in that regard, on a good day the 95th percentile of emails will be delivered within 30 seconds of the request being made.
Our product is provided to clients as a B2B tool, so user on-boarding is different than usual in that the client administrator will add users and the system then email those users an on-boarding email.
Magic link authentication wont be for everyone and may not work for certain user groups, but I like it.
Portier tries to improve on some of these points. It’s an OpenID Connect-based protocol and small server that takes an email address as login, then gives you a JWT when it has been verified. (Disclaimer: I’m the current maintainer.)
We want to beat password managers by being a bit smarter about email authentication and detecting the provider. Currently, this means if you enter a GMail address you get the Google OAuth flow, and domains using G Suite can also opt-in using Webfinger. There’s also an experimental Webfinger flow that anyone can implement to customize auth for a domain or even individual users, but of course, this sort of thing requires traction to succeed eventually.
If you don’t want to deal with email deliverability, Portier runs a public instance. This also works well for development environments. Of course, in production, that also means you’re subject to our reputation, and I don’t think we currently have to experience to really make any reliability claims for our public instance. (It currently relies on Postmark.)
Point 4 sounds odd. Sounds like you allow users to continue with registration before the email is verified? Is it possible your form can be used to check if an account exists? I’ve always thought of this as bad security practice, but I also don’t claim to be an expert here. Portier doesn’t fully protect against this, but maybe makes it a bit less natural, considering you’d usually wait for the JWT to arrive before checking the account.
On prefetching: we’ve seen a virus scanner prefetch email links once. I can’t remember which brand. This is one of the reasons we have to use some JavaScript. (I know this turns away some folks, unfortunately.)
This wouldn’t really be an option for GoatCounter, as it tries to be as easy to self-host as possible; having to run an additional service would greatly increase the complexity of that, even though Portier doesn’t look especially hard to run at a glance.
I guess I could use the public instance, but it’s one more point of failure dependent on “someone from the internet” running a comparatively small project. It’s not that I don’t trust you or think you’re incapable; it’s just that there’s all sorts of things that can go wrong with that (not necessarily in your control). Even if I would run a public broker myself I wouldn’t consider it a good option for the self-host use case.
Other than that, I haven’t really looked at it enough to have a truly informed opinion, but I think it looks interesting and might be a good option for some other things.
Point 4 sounds odd. Sounds like you allow users to continue with registration before the email is verified? Is it possible your form can be used to check if an account exists? I’ve always thought of this as bad security practice, but I also don’t claim to be an expert here. Portier doesn’t fully protect against this, but maybe makes it a bit less natural, considering you’d usually wait for the JWT to arrive
before checking the account.
The original version didn’t mention it (updated now), but one of the complexities here is that you choose a subdomain to access your site at (e.g. mycode.goatcounter.com); we want to “reserve” that while waiting for the confirmation so it can’t be taken by someone else in the time it takes you to verify, which would still leave us in the same position as before.
This is probably fixable, but it goes to show that things are not really simpler to implement than standard password-auth, at least in my use case.
CC: @adsouza since the last part also replies to your comment.
Thanks for taking the time to write this reply, and I agree with your points here and also in the original post. It’s a difficult problem in general, and trust/reputation is a big part of it.
The case of ‘self-hosted app using Portier’ is something we may not have paid much attention to, besides running a public Portier instance. I wonder what we can do to improve that.
One issue I had is people misspelling their email during signup, so they were immediately locked out of their account. This is an issue especially on GoatCouner since ..
If the misspelling of ‘GoatCouner’ here wasn’t intentional, the timing was great.
found myself changing and re-using passwords just to log in on mobile
Oh yeah, I simply stopped logging in on websites. For example lobsters. I’m using it 99% on my mobile device, and if I want to comment or vote I’ll simply have to start my laptop. On the plus site this decreases my site interaction drastically. (And I don’t like switching away from keepassXC, it’s just working too damn good on desktop. Which is nearly everything relevant. Sure I can’t login on lobsters, but that’s like 1% of the times I’ll have to login where I’m not on my phone.) Oh and I don’t have any email on my smartphone, so no, the email login wouldn’t work here.
I really like iOS’s integration with OSX via iCloud to generate complex passwords and fill it in for me. I wish Firefox could access the key store, but I’m content to dive into it manually and extract it for insertion into Firefox’s keystore. Integrated password managers with strongly generated passwords is really nice.
I also find logging in with a strong password on mobile annoying
Maybe it is on Android, but I don’t find it so on iOS. My Keychain is synced (E2E) across all my Apple devices, and Safari auto fills passwords. It also prefills a strong password when registering and saves it to the Keychain.
I’m still waiting for when we can just use client-side certs for auth, but I also understand all the roadblocks. At least with FIDO2 it’s getting closer.
I found it even worse on iOS since it asks my friggin’ iCloud password every damn time.
An iPhone was also the only Apple device I ever had, so it didn’t really have an option to sync with my Linux machines. There are probably solutions for this, but I don’t use my phone much and never bothered to look at it.
I no longer have my iPhone so I can’t check, but it asked my iCloud password a lot of times. The iPhone SE doesn’t support Face ID and I didn’t use touch ID as that’s not very reliable for me. I live in Indonesia where it’s very hot and humid and I’m very European (I once got a sunburn in Ireland; IRELAND!) and sweat a lot, and the touch ID doesn’t seem to work very well with sweaty fingers; just using a PIN worked much better for me.
I guess it’s one of those edge-cases where Apple’s “just use [..]” no-configure approach is rather sub-optimal.
Most password-based systems still offer email-based login, if in a somewhat convoluted way. You just declare that you forgot your password upon each login, and then you can log in by email without need to remember any password. I do that every time I have to buy anything off amazon, and it works great.
Email is not the right transport for this. A long time ago (2005ish?), I put together a demo doing this over XMPP. When you logged in, the site would store your XMPP ID in a cookie. It would then send a message to your XMPP client (which had guaranteed delivery, unlike email). You’d then get a link to click on to log in and doing so would then set a cookie with your session ID and you were logged in. It was generally a nice workflow for people using XMPP because they’d have their chat client running anyway and expect it to pop up into the foreground (unlike email, which tends to be a pull UI model even where the underlying protocol supports push). I also experimented with setting some metadata in the message so your chat client could present a ‘log in to this site, yes / no’ message, rather than showing the text with the link (XMPP has non-forgeable senders, so you could check the domain in the sender and the domain in the link and define policies based on that).
Unfortunately, XMPP never really took off. I also never really pushed this because I didn’t want every web site to know my XMPP ID, because that’s a good cross-site tracking identifier. I did experiment a bit with having some automatic forwarding from a throw-away address, but that either has too small an anonymity set or becomes a centralised tracking point.
I’ve been quite impressed with the auth stuff at work. When I log into any Microsoft service on the web, it uses WebAuthn to authenticate. This stores a secret key in the device’s secure key store (most phones have one, on Windows it stores it in the TPM) and uses it to sign a nonce from the server. WebAuthn is supported by most web browsers now (Mobile Safari, annoyingly, doesn’t use the Secure Element and only supports it via an external U2F device). That’s great for the second time I log in from a particular device. The first time, I use the authenticator app on my mobile phone.
The system I’d love to see would be to use WebAuthn for the second log-in from any authorised device, but provide a QR code for authenticating new devices. If I log in from a browser on another machine, it should present me with a QR code that I scan with my phone, then give me the option to set up a new WebAuthn key there or allow a one-time login (Amazon has a nice UI for this: they can email you a one-time password if you forget your password but your login manager on another computer remembers it and you don’t want to change it).
Thanks, this is interesting to read! I’ve only used 1 site that used email-based login, and mostly found it mildly annoying, as somebody who mostly uses a password manager and Google OAuth for logins.
Regarding prefetches breaking your workflow, I think there are some hosted anti-spam systems that try to access URLs in emails to see if the page looks spammy before the email even gets delivered to the user. I wonder if any of the big services have some special sauce to handle this sort of thing, since single-click email verification emails seem to be pretty common. I suppose this is also why they say that you shouldn’t change anything based on GET requests, but I’m not sure how to get around that while providing a single-click email experience.
I just finished a guide about passwordless authentication for my employer, so this post and discussion are timely for me. We recently ran into an issue with outlook prefetching the links (more here if you’re interested: https://github.com/FusionAuth/fusionauth-issues/issues/629 ).
I think that the key takeaway is to know your audience and offer them options. As someone else implied, people who manage bars have different authentication expectations than people who value privacy aware web analytics software.
About the “misspelling their email during signup” point. Would it improve things if registration would be a two step process?
First page asks for the email only. Email contains the URL to registration with the choice to pick the domain and autologins the user once not used domain is picked.
Yeah, that could work. But I think this is a good example of how email auth is not really simpler for either users or the code, at least in this particular use case.
What did the UI look like for sign up? A lot of places ask you to type your email address twice, which normally catches this (either the browser auto-fills, so you don’t get errors, or you need to make the same typo twice).
Thank you for writing about this, I’ve been wondering about its practicality.
If you know that your users are using gmail on desktop, you could send your authentication email with a random nonce and forward your users to https://mail.google.com/mail/u/0/#search/in%3Aanywhere+from%3Amy_website+random_nonce . So long as Google doesn’t straight up bitbucket your email, ending up in spam would be no problem. But this is still fairly fragile and hard-coded; some new mailfrom: URI sounds ideal.
I think the magic email link has a lot of misconceptions built in, that at least for me make them often horribly unusable:
people only have one email account
people have access to their email account when they try to login (e.g. missing 2FA on phone)
clicking the link will open the login in the correct browser (also Firefox tabs are a thing now)
And that is in addition to having to wait a few minutes and the other things you mentioned.
You can make it worse though, by deciding that every user is living in a country where dial-up IPs are semi-static. For a certain game I played I had to get a verification code via email every single day I logged in because “YOUR IP HAS CHANGED OMG THE WORLD WILL END”. ;)
Are you sure you want to be handling passwords yourself? Shouldn’t you be using a third-party authentication provider? That way, you run no risk of getting compromised and leaking (reused) passwords.
Handling passwords is really not that complicated. There are libraries around to do it, and quite frankly, it’s not magic. Just use bcrypt or something similar.
I would note that it’s not so much just the handling of passwords, but getting all of the workflows for authentication and session management right too. That’s why I like libraries like Devise for Rails that add the full set of workflows and DB columns already using all best-practices to your application, with appropriate hooks for customization as needed.
If anything that’s an argument against “just let someone else do it”.
You can review your own systems, you can organise an audit for them.
How do you plan to review Twitter’s processes to ensure they do it securely, given that they already have precedence for screwing the pooch in this domain?
Well, there’s a risk with anything you do when dealing with secrets; you can leak tokens or whatnot when using external services too.
As I mentioned in another comment, the self-host use case makes “just use an external service” a lot harder. It’s not impossible, but I went out of my way to make self-hosting as easy as possible; this is why it can use both SQLite and PostgreSQL for example, so you don’t need to set up a PostgreSQL server.
you run no risk of getting compromised and leaking (reused) passwords
You still have to handle authentication correctly, and sometimes having an external system to reason about can expose other bugs in your system.
I recall wiring up Google SSO on an app a few years ago and thinking configuring google to only allow people through who were on our domain was sufficient to stop anyone being able to sign in with a google account. Turns out in certain situations you could authenticate to that part of the app using a google account that wasn’t in our domain (we also had Google SSO for anyone in the same application, albeit at a different path.) Ended up having to check the domain of the user before we accepted their authentication from google, even though google was telling us they’d authenticated successfully as part of our domain.
If password hashing is a hard task for your project, I’d argue that’s because your language of choice is severely lacking. In most languages or libraries (where it isn’t part of the stdlib) it should be one function call to hash a new password, or a different single function call to compare an existing hash to a provided password.
This idea that password hashing is hard and thus “we should use X service for auth” has never made any sense to me, and I don’t quite understand why it persists.
I have never written a line of Go in my life, but it took me longer to find out that the author’s project is written in Go, than it did for me to find a standard Go module providing bcrypt password hashing and comparison.
I would consider it a bug for a library/function to (a) require the developer to provide the salt, or (b) not include the salt in the resulting string.
Problem is what provider do you choose to use? Do you just go and “support everyone”, or do you choose one that you hope all your users use, and that you are in support of (I don’t support nor have accounts at Facebook, Twitter, and Google), which narrows it down quite a bit. And what about those potential users that aren’t using your chosen platform(s)? Are you gonna provide password-based login as an alternative?
I’ve experienced similar issues with an email-only login we implented in a small B2B application. First of all, it was not easier that just implement Email/Password with Django (there was no need to add a recovery password option, since the product is very small, people will just send us an email). Second, most emails arrived fine or in spam folder, which is ok, but some companies (10%) never got an email from us. We used SendInBlue because it was easy to implement (just use SMTP). However. even if it worked, it’s somehow strange yet, and not an option for all users.
there was no need to add a recovery password option, since the product is very small, people will just send us an email
I’m pretty sure the entire premise behind claiming that email-login is easier is that you’re already implementing the password reset option. If you aren’t implementing password reset by email, then the premise is no longer true.
I think the success rate of this system is also highly affected by the kind of public you have. Here is my experience:
I built a bar management application, people purchase products themselves on their phone with their own account. It’s a socially controlled system.
I did ask for some feedback on this, and many liked this system better for this use case. Others opted for using a password, and were fine with that as well. It doesn’t work everywhere, but I think developers should definitely consider a magic link implementation for some applications, to use the best of both worlds.
I agree it heavily depends on the type of userbase you have. For example, I prefer the traditional email address/password setup since I always use my password manager, which auto-fills the fields for me. Magic link-based authentication feels a bit more tedious to me as a user since it would mean I would always have to do the one extra step of going into my email for the link. Also, the (re)authentication once a year is an excellent idea. If it’s a phone app, one way to help prevent people from forgetting their password would be similar to what Authy does, where it gives you the option to enter your password if you want to be sure that you remember it or you can just hit ignore.
Your experience differs from mine. We implemented email based login as primary authentication as a large proportion of our helpdesk tickets were from people having trouble logging in due to their forgetting their password and even with a password reset function available to them, they wouldn’t use it unless prompted (I guess because they couldn’t possibly have forgotten their password and the system must therefore be broken.)
I digress, we use mailgun for ensuring email deliverability and have so far not had issues in that regard, on a good day the 95th percentile of emails will be delivered within 30 seconds of the request being made.
Our product is provided to clients as a B2B tool, so user on-boarding is different than usual in that the client administrator will add users and the system then email those users an on-boarding email.
Magic link authentication wont be for everyone and may not work for certain user groups, but I like it.
Portier tries to improve on some of these points. It’s an OpenID Connect-based protocol and small server that takes an email address as login, then gives you a JWT when it has been verified. (Disclaimer: I’m the current maintainer.)
We want to beat password managers by being a bit smarter about email authentication and detecting the provider. Currently, this means if you enter a GMail address you get the Google OAuth flow, and domains using G Suite can also opt-in using Webfinger. There’s also an experimental Webfinger flow that anyone can implement to customize auth for a domain or even individual users, but of course, this sort of thing requires traction to succeed eventually.
If you don’t want to deal with email deliverability, Portier runs a public instance. This also works well for development environments. Of course, in production, that also means you’re subject to our reputation, and I don’t think we currently have to experience to really make any reliability claims for our public instance. (It currently relies on Postmark.)
Point 4 sounds odd. Sounds like you allow users to continue with registration before the email is verified? Is it possible your form can be used to check if an account exists? I’ve always thought of this as bad security practice, but I also don’t claim to be an expert here. Portier doesn’t fully protect against this, but maybe makes it a bit less natural, considering you’d usually wait for the JWT to arrive before checking the account.
On prefetching: we’ve seen a virus scanner prefetch email links once. I can’t remember which brand. This is one of the reasons we have to use some JavaScript. (I know this turns away some folks, unfortunately.)
This wouldn’t really be an option for GoatCounter, as it tries to be as easy to self-host as possible; having to run an additional service would greatly increase the complexity of that, even though Portier doesn’t look especially hard to run at a glance.
I guess I could use the public instance, but it’s one more point of failure dependent on “someone from the internet” running a comparatively small project. It’s not that I don’t trust you or think you’re incapable; it’s just that there’s all sorts of things that can go wrong with that (not necessarily in your control). Even if I would run a public broker myself I wouldn’t consider it a good option for the self-host use case.
Other than that, I haven’t really looked at it enough to have a truly informed opinion, but I think it looks interesting and might be a good option for some other things.
The original version didn’t mention it (updated now), but one of the complexities here is that you choose a subdomain to access your site at (e.g.
mycode.goatcounter.com
); we want to “reserve” that while waiting for the confirmation so it can’t be taken by someone else in the time it takes you to verify, which would still leave us in the same position as before.This is probably fixable, but it goes to show that things are not really simpler to implement than standard password-auth, at least in my use case.
CC: @adsouza since the last part also replies to your comment.
Thanks for taking the time to write this reply, and I agree with your points here and also in the original post. It’s a difficult problem in general, and trust/reputation is a big part of it.
The case of ‘self-hosted app using Portier’ is something we may not have paid much attention to, besides running a public Portier instance. I wonder what we can do to improve that.
If the misspelling of ‘GoatCouner’ here wasn’t intentional, the timing was great.
There are no mistakes, just happy little accidents.
At least I didn’t misspell it as GoatCunter; that’s what some of my friends have been calling it 😅
Oh yeah, I simply stopped logging in on websites. For example lobsters. I’m using it 99% on my mobile device, and if I want to comment or vote I’ll simply have to start my laptop. On the plus site this decreases my site interaction drastically. (And I don’t like switching away from keepassXC, it’s just working too damn good on desktop. Which is nearly everything relevant. Sure I can’t login on lobsters, but that’s like 1% of the times I’ll have to login where I’m not on my phone.) Oh and I don’t have any email on my smartphone, so no, the email login wouldn’t work here.
If you have an android, KeePassDX works well. (Available from playstore or F-droid)
let me suggest bitwarden, with bitwarden_rs if you like to self-host
I really like iOS’s integration with OSX via iCloud to generate complex passwords and fill it in for me. I wish Firefox could access the key store, but I’m content to dive into it manually and extract it for insertion into Firefox’s keystore. Integrated password managers with strongly generated passwords is really nice.
Maybe it is on Android, but I don’t find it so on iOS. My Keychain is synced (E2E) across all my Apple devices, and Safari auto fills passwords. It also prefills a strong password when registering and saves it to the Keychain.
I’m still waiting for when we can just use client-side certs for auth, but I also understand all the roadblocks. At least with FIDO2 it’s getting closer.
I found it even worse on iOS since it asks my friggin’ iCloud password every damn time.
An iPhone was also the only Apple device I ever had, so it didn’t really have an option to sync with my Linux machines. There are probably solutions for this, but I don’t use my phone much and never bothered to look at it.
Huh? It should not ask for your iCloud password to fill a password.
It should ask you to authenticate to the device - for most people this will mean TouchID or FaceID, with a fallback to the device pin.
I no longer have my iPhone so I can’t check, but it asked my iCloud password a lot of times. The iPhone SE doesn’t support Face ID and I didn’t use touch ID as that’s not very reliable for me. I live in Indonesia where it’s very hot and humid and I’m very European (I once got a sunburn in Ireland; IRELAND!) and sweat a lot, and the touch ID doesn’t seem to work very well with sweaty fingers; just using a PIN worked much better for me.
I guess it’s one of those edge-cases where Apple’s “just use [..]” no-configure approach is rather sub-optimal.
Most password-based systems still offer email-based login, if in a somewhat convoluted way. You just declare that you forgot your password upon each login, and then you can log in by email without need to remember any password. I do that every time I have to buy anything off amazon, and it works great.
Thanks for this. I’ve been planning to use this on all future side-projects, and your notes will make me pause and think more carefully about it.
Email is not the right transport for this. A long time ago (2005ish?), I put together a demo doing this over XMPP. When you logged in, the site would store your XMPP ID in a cookie. It would then send a message to your XMPP client (which had guaranteed delivery, unlike email). You’d then get a link to click on to log in and doing so would then set a cookie with your session ID and you were logged in. It was generally a nice workflow for people using XMPP because they’d have their chat client running anyway and expect it to pop up into the foreground (unlike email, which tends to be a pull UI model even where the underlying protocol supports push). I also experimented with setting some metadata in the message so your chat client could present a ‘log in to this site, yes / no’ message, rather than showing the text with the link (XMPP has non-forgeable senders, so you could check the domain in the sender and the domain in the link and define policies based on that).
Unfortunately, XMPP never really took off. I also never really pushed this because I didn’t want every web site to know my XMPP ID, because that’s a good cross-site tracking identifier. I did experiment a bit with having some automatic forwarding from a throw-away address, but that either has too small an anonymity set or becomes a centralised tracking point.
I’ve been quite impressed with the auth stuff at work. When I log into any Microsoft service on the web, it uses WebAuthn to authenticate. This stores a secret key in the device’s secure key store (most phones have one, on Windows it stores it in the TPM) and uses it to sign a nonce from the server. WebAuthn is supported by most web browsers now (Mobile Safari, annoyingly, doesn’t use the Secure Element and only supports it via an external U2F device). That’s great for the second time I log in from a particular device. The first time, I use the authenticator app on my mobile phone.
The system I’d love to see would be to use WebAuthn for the second log-in from any authorised device, but provide a QR code for authenticating new devices. If I log in from a browser on another machine, it should present me with a QR code that I scan with my phone, then give me the option to set up a new WebAuthn key there or allow a one-time login (Amazon has a nice UI for this: they can email you a one-time password if you forget your password but your login manager on another computer remembers it and you don’t want to change it).
Thanks, this is interesting to read! I’ve only used 1 site that used email-based login, and mostly found it mildly annoying, as somebody who mostly uses a password manager and Google OAuth for logins.
Regarding prefetches breaking your workflow, I think there are some hosted anti-spam systems that try to access URLs in emails to see if the page looks spammy before the email even gets delivered to the user. I wonder if any of the big services have some special sauce to handle this sort of thing, since single-click email verification emails seem to be pretty common. I suppose this is also why they say that you shouldn’t change anything based on GET requests, but I’m not sure how to get around that while providing a single-click email experience.
I just finished a guide about passwordless authentication for my employer, so this post and discussion are timely for me. We recently ran into an issue with outlook prefetching the links (more here if you’re interested: https://github.com/FusionAuth/fusionauth-issues/issues/629 ).
I think that the key takeaway is to know your audience and offer them options. As someone else implied, people who manage bars have different authentication expectations than people who value privacy aware web analytics software.
About the “misspelling their email during signup” point. Would it improve things if registration would be a two step process? First page asks for the email only. Email contains the URL to registration with the choice to pick the domain and autologins the user once not used domain is picked.
Yeah, that could work. But I think this is a good example of how email auth is not really simpler for either users or the code, at least in this particular use case.
What did the UI look like for sign up? A lot of places ask you to type your email address twice, which normally catches this (either the browser auto-fills, so you don’t get errors, or you need to make the same typo twice).
Thank you for writing about this, I’ve been wondering about its practicality.
If you know that your users are using gmail on desktop, you could send your authentication email with a random nonce and forward your users to
https://mail.google.com/mail/u/0/#search/in%3Aanywhere+from%3Amy_website+random_nonce
. So long as Google doesn’t straight up bitbucket your email, ending up in spam would be no problem. But this is still fairly fragile and hard-coded; some newmailfrom:
URI sounds ideal.I think the magic email link has a lot of misconceptions built in, that at least for me make them often horribly unusable:
And that is in addition to having to wait a few minutes and the other things you mentioned.
You can make it worse though, by deciding that every user is living in a country where dial-up IPs are semi-static. For a certain game I played I had to get a verification code via email every single day I logged in because “YOUR IP HAS CHANGED OMG THE WORLD WILL END”. ;)
Are you sure you want to be handling passwords yourself? Shouldn’t you be using a third-party authentication provider? That way, you run no risk of getting compromised and leaking (reused) passwords.
Handling passwords is really not that complicated. There are libraries around to do it, and quite frankly, it’s not magic. Just use bcrypt or something similar.
I would note that it’s not so much just the handling of passwords, but getting all of the workflows for authentication and session management right too. That’s why I like libraries like Devise for Rails that add the full set of workflows and DB columns already using all best-practices to your application, with appropriate hooks for customization as needed.
It’s not only the password in the database, but also the password in transit. For example, Twitter managed to log passwords:
The risk remains, it’s just more subtle and in places you might not immediately think of instead.
If anything that’s an argument against “just let someone else do it”.
You can review your own systems, you can organise an audit for them.
How do you plan to review Twitter’s processes to ensure they do it securely, given that they already have precedence for screwing the pooch in this domain?
It’s easier in smaller systems.
Well, there’s a risk with anything you do when dealing with secrets; you can leak tokens or whatnot when using external services too.
As I mentioned in another comment, the self-host use case makes “just use an external service” a lot harder. It’s not impossible, but I went out of my way to make self-hosting as easy as possible; this is why it can use both SQLite and PostgreSQL for example, so you don’t need to set up a PostgreSQL server.
You still have to handle authentication correctly, and sometimes having an external system to reason about can expose other bugs in your system.
I recall wiring up Google SSO on an app a few years ago and thinking configuring google to only allow people through who were on our domain was sufficient to stop anyone being able to sign in with a google account. Turns out in certain situations you could authenticate to that part of the app using a google account that wasn’t in our domain (we also had Google SSO for anyone in the same application, albeit at a different path.) Ended up having to check the domain of the user before we accepted their authentication from google, even though google was telling us they’d authenticated successfully as part of our domain.
If password hashing is a hard task for your project, I’d argue that’s because your language of choice is severely lacking. In most languages or libraries (where it isn’t part of the stdlib) it should be one function call to hash a new password, or a different single function call to compare an existing hash to a provided password.
This idea that password hashing is hard and thus “we should use X service for auth” has never made any sense to me, and I don’t quite understand why it persists.
I have never written a line of Go in my life, but it took me longer to find out that the author’s project is written in Go, than it did for me to find a standard Go module providing bcrypt password hashing and comparison.
And salting! So many of these libraries store the salt as part of the hash, making comparison easy but breaking hard.
I would consider it a bug for a library/function to (a) require the developer to provide the salt, or (b) not include the salt in the resulting string.
Problem is what provider do you choose to use? Do you just go and “support everyone”, or do you choose one that you hope all your users use, and that you are in support of (I don’t support nor have accounts at Facebook, Twitter, and Google), which narrows it down quite a bit. And what about those potential users that aren’t using your chosen platform(s)? Are you gonna provide password-based login as an alternative?
I’ve experienced similar issues with an email-only login we implented in a small B2B application. First of all, it was not easier that just implement Email/Password with Django (there was no need to add a recovery password option, since the product is very small, people will just send us an email). Second, most emails arrived fine or in spam folder, which is ok, but some companies (10%) never got an email from us. We used SendInBlue because it was easy to implement (just use SMTP). However. even if it worked, it’s somehow strange yet, and not an option for all users.
I’m pretty sure the entire premise behind claiming that email-login is easier is that you’re already implementing the password reset option. If you aren’t implementing password reset by email, then the premise is no longer true.
Thanks for posting. I appreciate this kind of succinct, practical article on a focused subject that can save me a bunch of future time.