Anthony Corletti
Published on

Passwordless Logins in Python

Authors

Many internet services use OAuth and email based protocols for sending login links and tokens.

I think it's a pretty slick way to log users in and reduces overhead for them by adding yet another credential to their password managers.

I hadn't found a python implementation I liked so I decided to implement a login credential issuer in pure python that I could use to send login links to users, similar to a slack-like login flow.

It didn't take much code to implement! I'll walk you through it.

I started by defining a basic class that could generate and validate a token.

Pypale stands for (Py)thon (Pa)ssword(le)ss.

import base64
import logging
import random
import string
import time
import jwt
class Pypale:
JWT_ALGORITHM = "HS256"
ENCODING = "utf8"
def __init__(self, token_ttl_minutes: int, base_url: str, secret_key: str,
token_issue_ttl_seconds: int):
self.token_ttl_minutes = token_ttl_minutes
self.base_url = base_url
self.secret_key = secret_key
self.token_issue_ttl_seconds = token_issue_ttl_seconds
def generate_token(self):
pass
def validate_token(self):
pass

Next I just filled in jwt building and field setting for building the token.

def generate_token(self, email: str) -> str:
return base64.b64encode(
jwt.encode(self.generate_token_metadata(email),
self.secret_key,
algorithm=self.JWT_ALGORITHM)).decode(self.ENCODING)
def generate_token_metadata(self, email: str) -> dict:
return {
"sub": email,
"jti": self.one_time_nonce(),
"iat": int(time.time()),
"exp": int(time.time()) + (self.token_ttl_minutes * 60),
"iss": self.base_url
}
def one_time_nonce(
self,
size=16,
chars=string.ascii_letters + string.digits + "-") -> str:
return "".join(random.choice(chars) for _ in range(size))

What's a nonce? Read this.

Then I wrote a validator. I'm basically checking that the link click event occurs before the link expiry event, set by the iat + token_issue_ttl_seconds and that the user we're checking for is the user that's mentioned in the token.

def valid_token(self, return_token: str, return_email: str = "") -> bool:
try:
decoded_return_token = base64.b64decode(return_token).decode(
self.ENCODING)
token_metadata = jwt.decode(decoded_return_token,
self.secret_key,
algorithms=[self.JWT_ALGORITHM])
if (token_metadata["iat"] + self.token_issue_ttl_seconds) < int(
time.time()):
logging.warning("Token was issued too long ago.")
return False
elif return_email != "":
if token_metadata["sub"] != return_email:
logging.warning("Token is not issued to the right user.")
return False
return True
else:
return True
except Exception as e:
logging.exception(
f"Raised exception while validating login link: {e}")
return False

And that's pretty much it! I recently published this code for you to use in whatever project you want to integrate passwordless logins into.

You can checkout the package on github at github.com/anthonycorletti/pypale and you can install it by running pip install pypale.

If you were following along and something didn't look quite right, let me know; a twitter dm works just fine – and feel free to open a pull request or issue if you'd like to contribute to pypale.