Guide
8 Min.
Read

Integrate passkeys into your Python Flask app

If you're looking to add a passkey login to your Python Flask app, you're in the right place! In this tutorial, we'll walk you through the step-by-step process and showing you how to use the Hanko Passkey API to make it happen. So, let's dive in and start 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' passkeys.
  2. A couple of functions on your frontend to display and handle the "Sign in with a passkey" and "Register a 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.

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)

2. /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.

Get credentials from Hanko Cloud

Now, to make it work we're missing one crucial step: getting the Tenant ID and API key secret from Hanko Cloud. For that, navigate over to Hanko, create an account, and set up your organization. Navigate to 'Create new project', select 'Passkey Infrastructure', and provide your project name and URL.

Note: It's recommended to create separate projects for your development and production environments. This way, you can use your frontend localhost URL for the development project and your production URL for the live project, ensuring a smooth transition between environments without the need to modify the 'App URL' later.

Obtain your Tenant ID and create an API key, then add the Tenant ID and API key secret to your backend's '.env' file.

Now that you have your secrets added to your backend's .env file, you should be all set to register a passkey. Go ahead and give it a try!

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 a 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) {
    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 a passkey" dialog and get the credential:
    • We use the get function from the @github/webauthn-json library to open the "sign in with a 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 passkeys into your Python Flask app. Feel free to reach out to us on Discord if you face any issues.

GitHub Repo: https://github.com/teamhanko/passkeys-python

arrow
Back to overview

More blog posts

Don't miss out on latest blog posts, new releases and features of Hanko's products, and more.

Your submission has been received!
Something went wrong.