August 11, 2020
Passwordless Logins in Python
@anthonycorletti

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.