How to add Passkey Login to your Python Flask App

How to add Passkey Login to your Python Flask App

If you're looking to add Passkey Login to your existing Python Flask app, you're in the right place! In this tutorial, we'll walk you through the step-by-step process, showing you how to use the Hanko Passkey API to make it happen. So, let's dive in and get started on upgrading your app's login experience.

If you already have authentication set up in your app, you're off to a great start! We won't focus on that here. Instead, we'll jump right into adding Passkeys to your existing auth system.

To bring Passkey functionality to your app, you'll typically need two things:

  1. A backend to handle the authentication flow and store your users' passkey.

  2. A couple of functions on your frontend to display and handle the "Sign in with passkey" and "Register passkey" dialogs. In this tutorial, we'll be using React.js for our frontend, but the process will be pretty similar for any other frontend framework you might be using.

Let's get started and give your users a seamless and secure login experience. To make things even simpler, we have created a sample app that you can use as a reference throughout the tutorial.

Get the API key and Tenant ID from Hanko

Before we dive into the code, there's one important thing we need to take care of – getting your credentials from Hanko Cloud.

Head over to Hanko and create your account. Once you're in, set up your organization and then click on "Create new project." Then, choose "Passkey infrastructure" and hit "Create project."

When you're creating your project, you'll need to enter a project name and URL. Please note that the project URL should be your frontend localhost URL when you're in dev mode. And once you're ready to go live, you'll want to switch that over to your production URL.

Now, copy the 'Tenant ID', then create an API key, copy the API key secret, and add it to your .env file.

Once you have your Python app set up with a Flask backend and React frontend, the next step is to add the environment variables you copied earlier to your backend. These variables will be essential for the APIs you'll create to handle passkey registration and login.

PASSKEY_API_KEY="your-passkey-api-secret"
PASSKEY_TENANT_ID="your-tenant-id"

Passkey Registration

Flask Backend:

First, you need to retrieve the PASSKEY_TENANT_ID and PASSKEY_API_KEY. These variables are used to authenticate and communicate with the Hanko Cloud API.

tenant_id = os.getenv("PASSKEY_TENANT_ID")
api_key = os.getenv("PASSKEY_API_KEY")

baseUrl = f"https://passkeys.hanko.io/{tenant_id}"
headers = {
    "apikey": api_key,
    "Content-Type": "application/json", 
}

Next, we define two routes in our Flask app to handle passkey registration

  1. /passkey/start-registration (POST):

    • This route is responsible for initiating the passkey registration process.

    • It first checks if the user is logged in by verifying the presence of the user_id in the session.

    • If the user is logged in, it retrieves the user_id and user_email from the session.

    • It then sends a POST request to the Hanko Cloud API's /registration/initialize endpoint with the user_id and username in the payload.

    • The response from the API contains the necessary creation options, which are returned as JSON to the frontend.

@app.route('/passkey/start-registration', methods=["POST"])
def start_registration():
    print("registering passkey")
    if 'user_id' not in session:
        return jsonify({"error": "User must be logged in to register a passkey"}), 401

    user_id = session['user_id']
    user_email = session['email']

    payload = {
        "user_id": user_id,
        "username": user_email,
    }

    response = requests.post(f"{baseUrl}/registration/initialize", headers=headers, json=payload)
    creationOptions = response.json()
    return jsonify(creationOptions)
  1. /passkey/finalize-registration (POST):

    • This route is responsible for finalizing the passkey registration process.

    • It expects the registration data to be sent in the request body as JSON.

    • It sends a POST request to the Hanko Cloud API's /registration/finalize endpoint with the received data.

    • If the registration is successful, it returns a success message as JSON along with a 200 status code.

@app.route("/passkey/finalize-registration", methods=["POST"])
def finalize_registration():
    data = request.json

    response = requests.post(f"{baseUrl}/registration/finalize", headers=headers, json=data)
    data = response.json()

    return jsonify({"message": "Passkey registered successfully"}), 200

React Frontend:

Make sure to install the @github/webauthn-json library in your frontend.

npm install @github/webauthn-json

Next, create a function called registerPasskey that will be called when the user clicks a button to register a new passkey.

import { create, type CredentialCreationOptionsJSON } from "@github/webauthn-json";

async function registerPasskey() {
  // Step 1: Start the passkey registration process
  const createOptionsResponse = await fetch("http://localhost:8000/passkey/start-registration", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    credentials: 'include',
  });
  const createOptions = await createOptionsResponse.json();
  console.log("createOptions", createOptions);

  // Step 2: Create the passkey credential using the WebAuthn API
  const credential = await create(
    createOptions as CredentialCreationOptionsJSON,
  );
  console.log(credential);

  // Step 3: Finalize the passkey registration
  const response = await fetch("http://localhost:8000/passkey/finalize-registration", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    credentials: "include",
    body: JSON.stringify(credential),
  });
  console.log(response);

  if (response.ok) {
    toast.success("Registered passkey successfully!");
    return;
  }
}

Let's break down the registerPasskey function step by step:

  1. Start the passkey registration process:

    • We make a POST request to the /passkey/start-registration endpoint of our Flask backend.

    • The request includes the necessary headers and credentials to maintain the user's session.

    • The response from the backend contains the creation options required for passkey registration.

  2. Create the passkey credential using the WebAuthn API:

    • We use the create function from the @github/webauthn-json library to create the passkey credential.

    • The create function takes the creation options received from the backend and returns a promise that resolves to the created credential.

  3. Finalize the passkey registration:

    • We make a POST request to the /passkey/finalize-registration endpoint of our Flask backend.

    • The request includes the created credential as the request body, along with the necessary headers and credentials.

    • If the response from the backend is successful (indicated by response.ok), we display a success message to the user using a toast notification.

With this code in place, when the user clicks the button to register a new passkey, the registerPasskey function will be called. It will start the registration process, create the passkey credential using the WebAuthn API, and finalize the registration with the backend.

Passkey Login

Backend:

To enable passkey login in your Flask backend, you'll need to add two more routes:

  1. /passkey/start-login (POST):

    • This route initiates the passkey login process.

    • It sends a POST request to the Hanko Cloud API's /login/initialize endpoint.

    • The response from the API contains the necessary login options, which are returned as JSON to the frontend.

  2. /passkey/finalize-login (POST):

    • This route finalizes the passkey login process.

    • It expects the client data to be sent in the request body as JSON.

    • It sends a POST request to the Hanko Cloud API's /login/finalize endpoint with the received client data.

    • The response from the API contains a token, which is decoded to extract the user ID.

    • If a user with the extracted user ID exists in your user database, the user is logged in.

    • If the login is successful, it returns a success message.

@app.route("/passkey/start-login", methods=["POST"])
def start_login():
    response = requests.post(f"{baseUrl}/login/initialize", headers=headers)
    login_options = response.json()

    return jsonify(login_options)

@app.route("/passkey/finalize-login", methods=["POST"])
def finalize_login():
    client_data = request.json

    response = requests.post(f"{baseUrl}/login/finalize", headers=headers, json=client_data)
    data = response.json()

    token = data.get('token')
    decoded_payload = jwt.decode(token, options={"verify_signature": False})

    user_id = decoded_payload.get('sub') 
    user = next((user for user in users if user['id'] == user_id), None)
    if user:
        session["user_id"] = user["id"]
        session["email"] = user["email"]
        user_info = {"id": user["id"], "email": user["email"]}
        return jsonify({"message": "Login successful", "user": user_info}), 200
    else:
        return jsonify({"message": "Invalid credentials"}), 401

Frontend:

Now we create a signInWithPasskey function. This function will interact with the Flask backend APIs to initiate and finalize the passkey login process.

Here's the code for the signInWithPasskey function:

async function signInWithPasskey() {
  // Step 1: Start the passkey login process
  const createOptionsResponse = await fetch("http://localhost:8000/passkey/start-login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    credentials: 'include',
    body: JSON.stringify({ start: true, finish: false, credential: null }),
  });
  const loginOptions = await createOptionsResponse.json();

  // Step 2: Open the "sign in with passkey" dialog and get the credential
  const options = await get(
    loginOptions as any,
  );

  // Step 3: Finalize the passkey login
  const response = await fetch("http://localhost:8000/passkey/finalize-login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    credentials: 'include',
    body: JSON.stringify(options),
  });

  if (response.ok) {
    console.log("User logged in with passkey");
    navigate("/dashboard");
    return;
  }
}

Let's break down the signInWithPasskey function:

  1. Start the passkey login process:

    • We make a POST request to the /passkey/start-login endpoint.

    • The request includes the necessary headers, credentials, and a JSON payload indicating the start of the login process.

    • The response from the backend contains the login options required for passkey authentication.

  2. Open the "sign in with passkey" dialog and get the credential:

    • We use the get function from the @github/webauthn-json library to open the "sign in with passkey" dialog.

    • The get function takes the login options received from the backend and returns a promise that resolves to the selected credential.

  3. Finalize the passkey login:

    • We make a POST request to the /passkey/finalize-login endpoint of our Flask backend.

    • The request includes the selected credential as the request body, along with the necessary headers and credentials.

    • If the response from the backend is successful (indicated by response.ok), we log a message indicating a successful passkey login.

    • After a successful login, we navigate the user to the "/dashboard" route using the navigate function (assuming you have a routing system in place).

Congrats! You've successfully integrated Passkey Login into your Python Flask app. Feel free to reach out to us on Discord, if you face any issues.

GitHub Repo:

Did you find this article valuable?

Support Ashutosh Bhadauriya by becoming a sponsor. Any amount is appreciated!