Integrating Trusted Registration for PushAuth™

Welcome to the final post of the Power of PushAuth™ blog series. In this post, we will enhance the basic website we created in the Building a Web Application with PushAuth™ post by integrating trusted registration to provide a way for the mobile SDK to only register authorized users.

For a detailed explanation on trusted registration, please refer to the following:

In this tutorial, we will integrate trusted registration into a web application so that a user, upon signup, is given a 4 digit “pairing code” they can enter in the mobile app. After this, the mobile client added will be the only one who is able to receive login request push notifications.

The end result of this blog post will be almost identical to the publicly available sample web application used in the previous blog post that introduced trusted registration.

Setup

To follow this tutorial, you will need:

This tutorial assumes a basic familiarity with the Rails framework. Also, the starting point of this guide is the Rails app we built in the Building a Web Application with PushAuth™ post. If you have not followed this previous post to build the web application from scratch, you can clone the pushauth-sample-server in our GitHub repository as the starting point. If you choose to use the GitHub project as a starting point, make sure to initialize the project by following the steps in README of the project.

Step 1 – Provide a pairing code for users

Database Setup

First, we need to add new data to the User table in the database to keep track of an integer pairing code and a boolean which tracks whether or not the code has been used.

Let’s generate a migration to add these columns:

$ rails generate migration add_verification_code_to_users verification_code:integer

The newly generated migration should look like this:

# db/migrate/{some_date}_add_verification_code_to_users.rb

class AddVerificationCodeToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :verification_code, :integer
  end
end

We also want to add a column to keep track of whether each pairing code has been used. This column should default to false so that we don’t have to set it when we create a user. Add this line under the other add_column line from the migration file:

add_column :users, :verification_used, :boolean, default: false

Run bundle exec rails db:migrate, and we should have the appropriate database setup.

User Model

Let’s add some business logic to our User model so that we can ensure the consistency of our table.

We want to add validation to the username such that it must be unique, and we want to add a before_create hook that generates a random verification code for a new User. While we’re at it, we may as well make a function that tries to use up a validation code, and returns whether or not it was a success.

# app/models/user.rb

class User < ApplicationRecord
  has_secure_password

 validates :username, presence: true, uniqueness: { case_sensitive: false }

 before_create :generate_verification_code

 def consume_verification_code(provided_code)
   if provided_code == self.verification_code && !self.verification_used
     self.update_attributes(verification_used: true)
   end
 end

 private

 def generate_verification_code
   self.verification_code = SecureRandom.rand(1000...9999)
 end
end

A quick note about consume_verification_code: in absence of any statements following the if, Ruby will return a boolean corresponding to the evaluated condition, so this will return true if and only if the code was correct and had not been used before.

Users Controller and View

Now, let’s make a UsersController so that we can implement user registration:

$ rails generate controller Users

In UsersController, we will add three actions

  • new : a signup form
  • create : the endpoint to which the signup data is sent
  • post_signup : the page displaying the verification code to the user
# app/controllers/users_controller.rb

class UsersController < ApplicationController
  skip_before_action :authorized

  def new
    @user = User.new
  end

  def create
    @user = User.new(params.require(:user).permit(:username, :password))

    if @user.save
      session[:signup_username] = @user.username
      session[:signup_verification_code] = @user.verification_code
      redirect_to post_signup_users_path
    else
      render :new
    end
  end

  def post_signup
    @username = session[:signup_username]
    @pairing_code = session[:signup_verification_code]
  end
end

Now, let’s add some new views.

app/views/users/new.html.erb for the signup page:

# app/views/users/new.html.erb

<%= form_for @user, class: "form-signin" do |f| %>
<% if @user.errors.any? %>
    <div class="error_messages">
      <h2>Form is invalid</h2>
      <ul>
        <% @user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <%= f.label :username, class: "sr-only" %>
  <%= f.text_field :username, class: "form-control", placeholder: "Username", autofocus: true %>

  <%= f.label :password, class: "sr-only" %>
  <%= f.password_field :password, class: "form-control", placeholder: "Password" %>

  <%= f.submit "Sign up!", {class: ["btn", "btn-lg", "btn-primary", "btn-block"]} %>
<% end %>

app/views/users/post_signup.html.erb displays the pairing code.

# app/views/users/post_signup.html.erb

<p>
Thanks for registering! Please enter the following in our app:
</p>

<pre>
  Username: <%= @username %>
  Pairing code: <%= @pairing_code %>
</pre>

And finally, we add our routes:

# config/routes.rb
# somewhere within the main do...end block

  resource :users, only: [:new, :create] do
    get "post_signup"
  end

We now want to add signup links from a couple different pages:

Replace the Welcome! Please <%= link_to "log in", "login" %>. line in app/views/application/home.html.erb with the following:

Welcome! Please <%= link_to "log in", "login" %> or <%= link_to "sign up", new_users_path %>.

And add this to the bottom of app/views/sessions/new.html.erb:

Don't have an account yet? Sign up <%= link_to "here", new_users_path %>!

At this point, you should be able to test the new signup flow by running bundle exec rails server.

Step 2 – Implement Trusted Registration Webhook Endpoint

When the user enters their name and pairing code in the app, the UnifyID PushAuth™ service will send a POST request to the Rails app for permission to add a device. On the Developer Dashboard, you can set up the endpoint URL. A detailed explanation on how the trusted registration webhook endpoint works can be found in the previous blog post.

We will make an endpoint for user verification at /users/trust, which checks if a given user and a challenge token (i.e., the pairing code for this sample website) match with our database records.

# app/controllers/users_controller.rb
# add these lines right under "class UsersController < ApplicationController"

  skip_before_action :authorized
  skip_before_action :verify_authenticity_token, only: :trust
  http_basic_authenticate_with name: Rails.application.credentials.unifyid[:basic_username],
    password: Rails.application.credentials.unifyid[:basic_password],
    only: :trust
    
  def trust
    @user = User.find_by(username: params[:user])
    if @user && @user.consume_verification_code(params[:challenge].to_i)
      head :ok
    else
      head :unauthorized
    end
  end 

Let’s take a look at these in a bit more detail:

  skip_before_action :verify_authenticity_token, only: :trust

By default, Rails has cross-site request forgery protection for forms, which means that a form that submits data via POST will have an authenticity token that makes it difficult for attackers to manipulate your browser into performing unauthorized requests. You can read more about this before_action here.
For our use case, however, the UnifyID service will not know the proper authenticity token, so we relax that requirement on the trust action.

  http_basic_authenticate_with name: Rails.application.credentials.unifyid[:basic_username],
    password: Rails.application.credentials.unifyid[:basic_password],
    only: :trust

This adds HTTP Basic Authentication to the /user/trust endpoint in order to make sure the request is from the UnifyID PushAuth™ service. Note that you will have to add basic_username and basic_password entries to the Rails application credentials (bundle exec rails credentials:edit).

Finally, we can add our route:

# config/routes.rb

  resource :users, only: [:new, :create] do
    post "trust"

    get "post_signup"
  end

And there you have it! We’ve added user signup, and we have secured mobile client registration to to UnifyID PushAuth™ service. The end result should look like our pushauth-sample-server-reg project in GitHub, which was introduced in the Power of Trusted Registration blog post.

This concludes the Power of PushAuth™ blog series. Thanks for following along, and feel free to reach out to us if you have any questions, comments, or suggestions!

The Power of Trusted Device Registration

Welcome once again to the Power of PushAuth™ blog series! This post builds on the previous posts by providing an extension of our sample project with feature and security enhancements.

The Problem of Unchecked Device Registration

In the “Is push authentication perfect?” section of the first post in the series, we pointed out some shortcomings of relying on push authentication as a means to authenticate users. Recall the following from that section:

“There’s also the issue of trusting a device to be associated with its true user during registration, as well as not allowing attackers to register devices under the true user’s account.”

The first iteration of the open source sample project did not provide a solution to this problem. With that initial, simple implementation of PushAuth, there was virtually no confidence in the devices registered under a given username. If an attacker were able to obtain the SDK key and one or more usernames registered on the server, they could configure the app on their phone and fraudulently accept or reject push notification login attempts. This security hole allows attackers to impersonate true users and access their resources in the website, or to lock a true user out of accessing their own resources. Not great, considering this is supposed to be a method of increasing the security of the authentication flow.

Trusted Device Registration

The extension presented in this post, however, does include trusted device registration! It also provides the added feature of user registration in the website. Remember that the initial project required users to be created in the console. Only users whose username and password were inserted to the database in that manner were able to log in to the website and receive push notifications. Now users register in the website directly. Once a user signs up in the website, they configure the sample app on their phone in order to authorize future login attempts. This is where trusted device registration comes into play.

What’s different for the user?

After dictating what username and password to associate with a new user, the website displays a four-digit pairing code. The user is instructed to enter that pairing code in the sample app with their chosen username. If the values match, then the user will receive push notifications during login attempts and be able to respond accordingly. If the username and pairing code do not match, then registration of that mobile device fails. This means they will not be able to receive push notifications to confirm or deny login attempts. An attacker will not know that unique pairing code, so they are unable to impersonate a user in the app.

How does this work?

The PushAuth sample server now has a verification endpoint, /users/trust, which accepts webhook requests from UnifyID’s PushAuth service. UnifyID’s server makes an HTTP POST request to a configured target URL, which is set in the project’s dashboard. The request body contains the username and the pairing code. The /users/trust endpoint looks up the given username and determines if the provided pairing code matches the server-generated code displayed during that user’s registration. If the request returns 200, then the user is considered verified and the device trusted. However, if the request returns anything else, the user is not considered verified and device registration fails.

The webhook target URL must use the https scheme and requests use the ‘Basic’ HTTP Authentication scheme, which can be used to validate requests. Further, the four-digit alphanumeric code generated by the server is single-use. These details collectively make it harder for an attacker to impersonate users. An attacker does not have access to the pairing code during initial sign up and they also cannot reuse the pairing code, thus ensuring that devices registered with users are trusted to belong to the true user.

Note: The way we chose to implement trusted device registration with a pairing code is by no means the only method of implementing trusted registration via webhook.

What are the limitations?

If you remember from our first post in the series, there will seemingly always be security holes in a login flow. This enhancement is no exception. For one, the pairing code is only displayed during user registration. What if the user accidentally closes out the tab and then has no way to pair their device? Or what if something happens with the device or app on their device and they have no way to reconfigure an app to be tied to their user? There’s also the concern of in-person over-the-shoulder access to the pairing code, where an attacker could see the four digits and enter the code on their phone before the true user is able to do so. However, just like before, there are solutions to those problems as well; they just aren’t included here. The extension we provide is certainly a big security improvement and patches one of the larger holes that existed in the initial sample project.

Developer Changes

Now that we’ve gone over the concept of trusted device registration and the value it adds to the PushAuth sample project, we’ll walk you through the actual changes from a developer’s perspective. The new repositories can be found here:

We have videos on the UnifyID YouTube channel to walk you through the full end-to-end flow of adding PushAuth to a website login flow with trusted registration.

Trusted Registration Webhook

First and foremost, you’ll need to enable trusted registration for your project in the dashboard. Reference the Setting up Trusted Registration section of our documentation to do so.

The server needs to be publicly available for the target URL to be used. If you want to follow along the tutorial without hosting the web server, we suggest using ngrok to expose the sample rails server running locally on your machine to the internet. The steps shown in this post will do that.

$ ngrok http 3000

Session Status                online
Account                       morganfrisby (Plan: Free)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://9c6b07ea6861.ngrok.io -> http://localhost:3000
Forwarding                    https://9c6b07ea6861.ngrok.io -> http://localhost:3000

The target URL is the https URL forwarding to your local server, with /users/trust appended. For example:

Once you add the target URL, trusted registration is enabled for your project. The username and password displayed are used in HTTP Basic Authentication. The username value is set to unifyid, and the password is an auto-generated random string. This value can be rotated, should it become compromised.

Now that the trusted registration webhook is set up in the dashboard, you’re ready to move on to setting up the server.

Server Setup

Other than that, server setup more or less follows the instructions from the first tutorial. Since users are now registered on the website, there is no need to create users in the console during server setup. The other change is the addition of basic_username and basic_password to the credentials file, which are the values found in your project dashboard under the Trusted Registration Webhook section, shown above.

The streamlined steps to spin up the server are now:

$ git clone https://github.com/UnifyID/pushauth-sample-server-reg.git
$ cd pushauth-sample-server-reg
$ bundle install
$ yarn install --check-files
$ bundle exec rails db:migrate

$ EDITOR=vim rails credentials:edit
unifyid:
  server_api_key: <your_key_goes_here>
  basic_username: <basic auth username for /users/trust webhook endpoint>
  basic_password: <basic auth password for /users/trust webhook endpoint>

$ bundle exec rails server

Feel free to reference the detailed tutorial in our previous post for more context around those commands, or the technical details blog post to understand how the server was built. Our next post in the series will provide the technical details of the trusted registration extension of the sample server.

You can see the new website screenshots here (notice the pairing code in the third screenshot):

Sample App Setup

Nothing changes from the developer’s perspective for the iOS and Android sample apps; simply clone the updated repositories and follow the same instructions from earlier in the blog series. Those two posts can be found here:

The iOS sample app screens will look like:

The Android sample app screens now look like:

That’s it! Thanks for following along with the Power of PushAuth™ blog series. The next, and final, post will include the technical details of adding trusted registration to the sample project. Until then, feel free to reach out to us with questions or comments at https://unify.id/contact-us/.