Passwordless authentication means setting up a way for users to log in without needing a traditional password. Instead of a password, users enter their email and receive a one-time password (OTP) or a login link sent to their email every time they want to log in. This approach is considered better for several reasons:
This article doesn't cover the technical implementation details, but it provides insights into considerations when implementing passwordless authentication. Below is a simple diagram that illustrates how it works.
Instead of sending a one-time password (OTP) to the user's email, we can send a URL that, when opened, will authenticate the user. However, we need to ensure that the link can only be used once for security reasons.
The problem arises when the link is opened on a mobile device like an iPhone, as it may not redirect the user to a browser but instead open a preview of the link within the email app's in-app browser. Even if the user tries to open the link in their preferred browser, it may show an "Invalid Token" message because the browser preview already activated the verification process.
We need to provide a method for users to manually activate token verification, this can be achieved by inputting a one-time token.
We need a way to improve security from our end, the main thing we want to prevent is an attacker trying to guess the generated token or brute-force it. Let's try to make the token hard to guess.
If we use a
format with a length of 4, like an ATM pin. This means we have a total of base 10 [0-9]
104 = 10,000
possible combinations, which isn't alot.
If each request takes
, with a brute force approach it'll take the hacker a total of 20ms
to guess the token.~3 mins
Let's increase the length to 6, we do not want our length to be too long to provide a good user experience for our users.
Now we can 106 = 1,000,000
possible combinations which still isn't enough.
Let's go for
, this gives a total combination of base 62 [a-z,A-Z,0-9]
626 = 56,800,235,584
possible combinations, it will take the hacker a total of
.~20 years
Let's increase the length to 8 with a base62 format, this gives a wapping total combination of 628 = 218,340,105,584,896
. Brute forcing at 20ms per response will give an average time of
to crack the password.~70,000 years
We can do better!
We want to slow down any brute-force attacks, so let's add a bit of delay to the login endpoint, we'll add a 2-second delay. This means each request is going to take at least 2 seconds to complete.
func (r *mutationResolver) Login(ctx context.Context, token string) (bool, error) {
// add delay to avoid brute force attacks
time.Sleep(time.Second * 2)
return r.UserService.Login(ctx, token)
}
The estimated average time required to crack the token is 6 million years 👴🏿.
We can do better!
We can reduce the window for potential attacks by setting an expiration time for the token. Once the specified time has passed, the token will be considered invalid. The duration can vary, but the shorter it is, the more secure it will be. However, it should not be too short, as we need to account for the time it takes for the user to receive the email and input the token.
We can go one step further.
Rate limiting works by setting limits on the number of requests that can be made to a specific server/client/IP within a specific time frame.
We can limit the number of requests a user can send to our server to something like
this eliminates DDOS attacks and any form of brute-force attacks.
Implementing it in Golang with go-chi is very simple.100req/min
// Rate limiting per IP 100reqs/minute
s.Router.Use(httprate.LimitByIP(100, 1*time.Minute))
I've learned that nothing is unhackable; the main goal is to make it extremely difficult to hack so that it's impractical to even try. Instead of using the user's email, you can use an authentication app. I haven't implemented that method for my startup yet because I don't want my users to go through additional steps.
Stay safe, happy hacking!