Mastering Passwordless Auth: How I Overcame Security Challenges

May 18, 20244 min read
SecurityAuthTutorialGolang
Mastering Passwordless Auth: How I Overcame Security Challenges

What is Passwordless Auth?

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:

  • No need to remember a password.
  • There's no single master key (password) that can be stolen to access your account.
  • Security responsibility shifts to your email provider.
  • Easier and simpler to implement (no need for "forgot password" or "change password" endpoints, etc.)

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.

Passwordless Auth Diagram

Problem with one-time URLs

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.

OTP Approach

We need to provide a method for users to manually activate token verification, this can be achieved by inputting a one-time token.

OTP Login

Improving Security

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.

Token format and length

If we use a base 10 [0-9] format with a length of 4, like an ATM pin. This means we have a total of 104 = 10,000 possible combinations, which isn't alot. If each request takes 20ms, with a brute force approach it'll take the hacker a total of ~3 mins to guess the token.


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 base 62 [a-z,A-Z,0-9], this gives a total combination of 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 ~70,000 years to crack the password.


We can do better!

Increasing response time

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!

Token Expiration

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

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 100req/min this eliminates DDOS attacks and any form of brute-force attacks. Implementing it in Golang with go-chi is very simple.

// Rate limiting per IP 100reqs/minute
s.Router.Use(httprate.LimitByIP(100, 1*time.Minute))

Conclusion

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!