Working with Callbacks

Use callbacks to implement custom functionality at key points in the auth flow.

Introduction

Many applications want to perform additional actions at specific points in the authentication flow. For example, you may want to create a user in your own database when the user is registered with Passage or include additional registration fields that Passage does not support. This guide will show you how to easily implement this type of functionality using Passage's Javascript callbacks.

Let's consider the following scenario: You have a Python Flask application that wants to know a user's email address and full name. The email address is their primary identifier for authentication and the full name will be used to customize the user's view when they log in.

Passage now supports custom user fields on registration for this specific use case! Check out our documentation for it here.

Continue following this guide for an example of how to use Passage callbacks.

At a high level you will do the following:

  • Create the registration input field for the "name" parameter.

  • Create an endpoint in your web server that creates a user in your database with a name and a Passage User ID field.

  • Create a Passage onSuccess callback that will create a user in your database by hitting the /user endpoint whenever a user has registered with Passage.

To see a full example of a Python Flask application that implements custom user data, check out the example application on Github.

Setup

This guide starts with a simple Python Flask application. Download this Github repository and start with the 01-Login example. This will give you a basic Flask app with a single page for login and registration using the <passage-auth> element and will be the starting point for this guide.

If you already have an application ready to go, skip to this section to start adding custom fields.

Setup your virtual environment and install dependencies.

python3 -m venv venv
source venv/bin/activate
pip install -r requirements

Add your Passage App ID and API Key to the .env file. If you aren't sure how to get your App ID or API Key, check out this page before continuing. Then confirm the application is running.

flask run

Create Separate Registration Page

The first thing you want to do is make a separate registration page, which will use the <passage-register> element and have other fields for data you want to collect on user registration. Copy the index.html file to a new file, called register.html. Change the Passage Elements so that the index file has <passage-login> and the register page has <passage-register>.

Add a Database to your Application

For demonstration purposes, you can use the flask-sqlalchemy package to make a simple file-based database.

pip install flask-sqlalchemy    

Create the database model for our user by creating a models.py file and adding the following code to it.

from flask_sqlalchemy import SQLAlchemy
from app import db

class User(db.Model):
   id = db.Column(db.Integer, primary_key=True, autoincrement=True)
   name = db.Column(db.String(100))
   passage_id = db.Column(db.String(24), index=True)  

Add the package to your __init__.py file and configure the app to use your database and User model.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()   
def create_app():
    app = Flask(__name__)

    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.sqlite3'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    db.init_app(app)
    with app.app_context():
        db.create_all()
    
    # blueprint for auth routes in our app
    from .main import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    # blueprint for non-auth parts of app
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    return app

At this point you should have a working application that is ready for custom data to be added.

Adding Custom Fields to the UI

Adding the form fields to your application is pretty straightforward. Add the following HTML to the register.html template.

<div class="form-container">
  <!-- fields for custom data -->
  <div id="custom-data" style="align-self: center; font-weight: bold">
    <h2>Register</h2>
    <br></br>
    <div style="font-weight: normal; font-size:14px">
      <div class="label">Name</div>
      <input
       type="text"
       class="input"
       id="name"
       placeholder="Authentigator"
      />
    </div>
  </div>
  
  <!-- passage register -->
  <passage-register app-id="{{psg_app_id}}"></passage-register>
  <script src="https://cdn.passage.id/passage-web.js"></script>
  
  <!-- custom footer with link to login page -->
  <p>
    <div id="goto-login" style="align-self: center;">
    Already have an account? <span><a href="/login">Login</a></span>
   </div>
  </p>
</div>

Great! Now you have a form that looks like this.

Responding to the Passage Element UI

This is where it gets a little interesting. The screenshot above looks great - but once a user submits their information, the Passage Element will change its view to no longer show the email input and begin to guide users through the registration process. Passage Elements have multiple views for users depending on whether they are using biometrics or magic links to register for an account on your application. Because of this, you need to add some custom JavaScript to ensure that the name field and other message don't appear on subsequent steps of the login process.

Mutation observers can be used to respond to the updating state of the Passage Register UI. A mutation observer is a built-in feature of JavaScript that observes a DOM element and fires a callback when it detects a change. It can be used to enable changes in the browser UI based on the current state of the Passage element. In this case, you don't want to show the name field or the "Already have an account?" footer once a user has moved past the email input view.

Add the following code in a script tag just after the Passage Register element in register.html. This code will check if the email input view is active on each mutation, and show or remove certain HTML elements depending on the answer.

<passage-register app-id="{{psg_app_id}}"></passage-register>
<script src="https://cdn.passage.id/passage-web.js"></script>
<script>
 // Mutation observer code to show fields
  function checkShowMessage() {
    const shadowRoot = (document.querySelector("passage-register")).shadowRoot
    if (shadowRoot === null) {
      return;
    }
    const emailInput = shadowRoot.querySelector('.view-email-input')
    // if the email input view is shown, show the custom data fields
    if  (emailInput === null) {
      document.getElementById("custom-data").style.display="none";
      document.getElementById("goto-login").style.display="inline";
    }
    // otherwise, hide them
    else {
      document.getElementById("custom-data").style.display="inline";
      document.getElementById("goto-login").style.display="inline";
    }
  }
  const observer = new MutationObserver(function (mutations) {
    mutations.forEach(() => {
      checkShowMessage()
    })
  })
  
  // attach mutation observer to the passage register element
  const shadowRoot = (document.querySelector("passage-register")).shadowRoot
  if (shadowRoot !== null) {
    observer.observe(shadowRoot, { childList: true, subtree: true })
  }
</script>

The Passage Elements are web components which means the HTML that is rendered by the elements is contained in the shadowDOM of the <passage-auth>, <passage-login>, and <passage-register> tags. To observe mutations in the shadowDOM of the Passage Elements you simply direct the mutation observer to observe the element's shadowRoot property which returns the root element of the shadowDOM. Now the mutation observer can respond to any changes in the rendered UI of the Passage Element just like it would with any other HTML element.

Submitting and Storing User Data

The final step is to submit the user's name to an endpoint on your web server so that you can store it along side the Passage User ID in your database. If you haven't already, ensure that your database model includes a field for the Passage User ID. It will look like this:

class User(db.Model):
   id = db.Column(db.Integer, primary_key = True)
   name = db.Column(db.String(100))
   passage_id = db.Column(db.String(32), primary_key = True)  

In main.py update the register function and add the createUser function.

# set in .flaskenv to http://localhost:5000
API_URL = os.environ.get("API_URL")

@main.route('/register')
def register():
    return render_template('register.html', psg_app_id=PASSAGE_APP_ID, api_url=API_URL)

@auth.route('/user', methods=['POST'])
def createUser():
    # g.user will be set to the Passage user id
    u = User()
    u.passage_id = g.user
    u.name = request.get_json()["name"]

    # commit to database
    db.session.add(u)
    db.session.commit()

    return jsonify({"result": 200})

Then create an onSuccess callback that will send user data to the above endpoint once a user has been created successfully. Add the following code to a script tag just after the Passage Register element.

<passage-register app-id="{{psg_app_id}}"></passage-register>
<script src="https://cdn.passage.id/passage-web.js"></script>
<script>
    const onSuccess = (authResult) =>{
      document.cookie = "psg_auth_token=" + authResult.authToken + ";path=/";
      const urlParams = new URLSearchParams(window.location.search)
      const magicLink = urlParams.has('psg_magic_link') ? urlParams.get('psg_magic_link') : null
      if (magicLink !== null) {
        new Promise((resolve) => {
          setTimeout(resolve, 3000)
        }).then( ()=>{ window.location.href = authResult.redirectURL;})
        return;
      }
      $.ajax({
        type: "POST",
        async: false,
        url: "{{api_url}}/user",
        data: JSON.stringify({"name":document.getElementById('name').value}), // get this data from form fields
        contentType: "application/json",
        dataType: 'json' 
      });    
      window.location.href = authResult.redirectURL;
    }
</script>

Conclusion

Finally, you can use this name field as intended. The goal was to use the name field to customize the dashboard, so change the template in dashboard.html to include the following.

<div class="title">Welcome {{name}}!</div>
<div class="message">
    You successfully signed in with Passage.
    <br/><br/>
    Your email is: <b>{{email}}</b>
</div>

Then update the dashboard function in main.py. In this function, you have the user's Passage ID because it is authenticated through the middleware. Use Passage ID to look up the user in the database and use the Passage SDK to look up the user's information stored in Passage. You get the user's name from your own database and the user's email from Passage.

@auth.route('/dashboard', methods=['GET'])
def dashboard():
    # g.user should be set here to the Passage user ID

    # use Passage user ID to get user record in DB
    user = User.query.filter_by(passage_id=g.user).first()

    # use Passage SDK to get info stored in Passage (email)
    psg_user = psg.getUser(g.user)

    return render_template('dashboard.html', name=user.name, email=psg_user.email)

Last updated