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:
- The Power of Trusted Device Registration blog post
- The Set Up Trusted Registration documentation in the UnifyID Developer Portal.
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 formcreate
: the endpoint to which the signup data is sentpost_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
Add links to the signup page
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!