The Instant2FA Developer Hub

Welcome to the Instant2FA developer hub. You'll find comprehensive guides and documentation to help you start working with Instant2FA as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    

Integrating Instant2FA

Here's how to get 2FA in your web app in half an hour.

Integrating Instant2FA takes 3 API calls, no database schema changes, and no additional UX work. Let's get started!

Overview

The integration has two parts:

  1. We'll embed Instant2FA's hosted settings page in your app's user settings.

    The hosted settings page lets your users enable two-factor, set up backup codes, and manage their two-factor devices. We handle all of this — you don't need to do any backend work beyond requesting an instance of the page.

  2. We'll handle the 2FA verification.

    You'll request a hosted page that lets a user enter a two-factor code and then handle the response. This page will automatically provide the user with additional verification options like backup codes, with no extra work on your part.

Before we begin, grab your favorite hot beverage and generate a pair of API credentials by visiting our developer dashboard.

One more thing: we have client libraries for Ruby, JavaScript, and Python that make integration even easier. To install them, run:

npm install instant2fa
gem install instant2fa
pip install instant2fa
// The SDK is available on jcenter.
// build.gradle:
compile 'com.instant2fa:instant2fa:1.0.1'

// build.sbt:
libraryDependencies += "com.instant2fa" % "instant2fa" % "1.0.1",
composer require clef/instant2fa

Embedding the settings page

The settings page lets one of your users enable two-factor, set up backup codes, and manage their devices. You'll make a request to generate a unique settings page for a given user, and then you'll embed it on a rendered page.

With an access key and access secret in hand, let's make the request to generate a settings page for an already-logged in user.

const Instant2FA = require('instant2fa');
// Or, if you are using ES6 modules:
// import Instant2FA from 'instant2fa';

const instant2fa = new Instant2FA({ 
  accessKey: YOUR_ACCESS_KEY,
  accessSecret: YOUR_ACCESS_SECRET 
});

// Now, in the route that renders your user settings page: 

instant2fa.createSettings({ distinctID: user.id }).then((mfaSettingsURL) => {
  res.render('settings', { url: mfaSettingsURL });
});
require 'instant2fa'

Instant2FA.configure do |config|
  config.access_key = "YOUR_ACCESS_KEY"
  config.access_secret = "YOUR_ACCESS_SECRET"
end

# Now, in the route that renders your user settings page:

get '/settings/security' do
  @hosted_url = Instant2FA.create_settings(@user.id)
  erb :security
end
import instant2fa

instant2fa.access_key = 'YOUR_ACCESS_KEY'
instant2fa.access_secret = 'YOUR_ACCESS_SECRET'

# Now, in the route that renders your user settings page:
@app.route('user_settings')
def settings(user):
  hosted_page_url = instant2fa.create_settings(user.id)
  return render_template(
    'user_settings.html',
    url=hosted_page_url
  )

// These examples are written as if you're using
// the Play framework for Java / Scala.
//
// In the route that shows your user settings page: 
public Result settingsPage() {
    Instant2FA instant2fa = new Instant2FA(ACCESS_KEY, ACCESS_SECRET);
    User user = findUser(request().username());
    String hostedPageURL = null;
    try {
        hostedPageURL = instant2fa.createSettings(user.id.toString());
    } catch (IOException e) {
        // Handle network errors
    } catch (APIException e) {
        // Handle API errors
    }
    return ok(settings.render(user, hostedPageURL));
}
$instant2fa = new \Instant2FA\Instant2FA([
    'access_key' => ACCESS_KEY,
    'access_secret' => ACCESS_SECRET
]);

// Now, in the route that renders your user settings page: 
$mfa_settings_url = $instant2fa->create_settings($this->user->id);
return view('account.settings')
  ->with('url', $mfa_settings_url)
  ->withAccount($this->user);

First, we initialized the SDK using the access key and access secret. Then, we created the hosted settings page and passed it to our settings page template.

You'll note that we passed in a parameter, distinct ID, to the call. Distinct ID is any ID that uniquely identifies your user. Here we used user.id but it could easily be any other value you'd like, as long as it uniquely identifies the user.

Now, let's take the URL we just generated and embed it on the frontend. We've provided a drop-in script tag that makes this a one-liner. Include this script tag wherever you'd like the page to render:

<script
    class="instant2fa-page"
    src="https://js.instant2fa.com/hosted.js"
    data-uri="{{ url }}"
></script>
import Instant2FAComponent from 'instant2fa-component'

return (
	<Instant2FAComponent
		uri={url} // this is the URL previously retrieved from the server
	></Instant2FAComponent>
)
<div class="instant2fa"></div>
<script src="https://js.instant2fa.com/hosted.js"></script>
<script>
    Instant2FAPage.configure({
    	element: document.querySelector('.instant2fa'),
        uri: url // this is the URL previously retrieved from the server
    });
</script>

Where should I put 2FA settings?

We recommend embedding it right below the option to change a user's password.

Congratulations! You just embedded a settings page that lets your users set up 2FA. Now, we'll walk through how to perform a 2FA verification and log your users in.

Handling verification

To handle a verification, you'll make a request to generate a unique verification page for a given user, embed it on a page, and then handle the response on your backend.

Let's start with the request to generate a verification page. Two-factor verification happens after a user has already successfully entered in their username and password, so we'll make the request in the route that handles a POST with a username and password:

app.post('/login', function (req, res) {
  checkLogin(req.body.username, req.body.password).then((user) => {
    return instant2fa.createVerification({ distinctID: user.id });
  }).then((mfaVerificationURL) => {
    req.session.distinctID = user.id;
    res.render('mfa_verification', { url: mfaVerificationURL });
  }).catch((e) => {
    if (e.name == "MFANotEnabled") {
      recordTheLogin(user);
      res.render('logged_in', { user })
    } else {
      handleError(e);
    }
  });
})
require 'instant2fa'

post '/login' do
  username = params['username']
  password = params['password']
  user = verify_password(username, password)
  if user
    begin
    	@url = Instant2FA.create_verification(user.id)
    	session['distinct_id'] = user.id	
      erb :verification
    rescue Instant2FA::Errors::MFANotEnabled
      # user is logged in because they do not have 2FA
      # enabled
      erb :logged_in
    end
  else
    erb :login
  end
end
import instant2fa

@app.route('login', methods=['POST'])
def login():
  username = request.json.get('username')
  password = request.json.get('password')
  user = verify_password(username, password)
  try:
    hosted_page_url = instant2fa.create_verification(user.id)
    session['distinct_id'] = user.id
  except instant2fa.errors.MFANotEnabled:
    return render_template('logged_in.html', user=user)
  else:
    return render_template(
      'mfa_verification', url=hosted_page_url
    )
    
    
public Result logIn() {
    Form<Login> loginForm = form(Login.class).bindFromRequest();

    if (loginForm.hasErrors()) {
        return badRequest(index.render(loginForm));
    } else {
        try {
            String distinctID = loginForm.get().user.id;
            String hostedPageURL = instant2FA.createVerification(distinctID);
            session("distinct_id", loginForm.get().user.id);
            return verify.render(hostedPageURL);
        } catch (IOException e) {
            return badRequest(index.render(loginForm));
        } catch (MFANotEnabledException e) {
            recordTheLogin(loginForm.get().user);
            return redirect(routes.Dashboard.index());
        } catch (APIException e) {
            return badRequest(index.render(loginForm));
        }
    }
}
// In the route that handles the POST of your login form,
// first check the login credentials as normal.
// Note: we use a laravel-style auth flow here.
if (Auth::attempt($credentials)) {
  try {
    $mfa_verification_url = $instant2fa->create_verification(Auth::user()->id);
    $request->session()->put('distinct_id', Auth::user()->id);
    // Don't log the user in yet, since they need to verify the second factor
    Auth::logout();
    return view('auth.verify')->with('url', $mfa_verification_url);
  } catch (\Instant2FA\Error\MFANotEnabled $e) {
    // Redirect the logged in user
    return redirect()->intended($this->redirectPath());
  }
}

Let's break that down. First, we check login credentials in the usual way. Then, we request a 2FA verification URL. Once we have that, we pass the URL off to a template.

Two things to note:

  1. If we get back a verification URL, it means the user has 2FA enabled and so we should continue the 2FA flow. Don't set the user in the session yet since they have additional verification to do.

    If a user doesn't have 2FA enabled, the MFA Not Enabled exception gets thrown. You should catch this exception and then log the user in normally by setting something in the session.

  2. Once we get back a verification URL, we also set the distinct ID in the session. We need to save this value so we can check it later, once we're confirming a verification.

Let's switch over to the template. As with embedding the settings page, our client-side JavaScript library makes rendering the verification page a one-liner:

<form action="/two-factor-verification" method="POST">
    <script
        class="instant2fa-page"
        src="https://js.instant2fa.com/hosted.js"
        data-uri="{{ url }}"
    ></script>
</form>
import Instant2FAComponent from 'instant2fa-component';

const onEvent = (event) => {
  if (event.type === "verificationSuccess") {
    var token = event.payload.token;
    console.log("Verification token is: " + token);

    $.ajax({
        method: 'POST',
        url: '/two-factor-verification',
        data: {
            instant2faToken: event.payload.token
        }
    });
  }
}

return (
  <Instant2FAComponent
    uri='YOUR_VERIFICATION_PAGE_URI'
    onEvent={onEvent}
  ></Instant2FAComponent>
)
<div class="instant2fa"></div>
<script src="https://js.instant2fa.com/hosted.js"></script>
<script>
    Instant2FAPage.configure({
        element: document.querySelector('.instant2fa'),
        uri: url // this is the URL previously retrieved from the server
    }, function(event) {
        if (event.type === "verificationSuccess") {
            var token = event.payload.token;
            console.log("Verification token is: " + token);
            
            $.ajax({
                method: 'POST',
                url: '/two-factor-verification',
                data: {
                    instant2faToken: event.payload.token
                }
            });
        }
    });
</script>

We add an empty form with a script tag inside that renders the verification page. When a user submits a two-factor code, that form will be submitted automatically with a hidden input field containing a token. You'll use that token to see whether the verification was successful.

What should the template look like?

Just copy the template from your login page, replacing the username and password form with the form we just defined above.

Let's use the token to see whether the verification was successful. On the server, add this endpoint:

app.post('/two-factor-verification', function (req, res) {
  let distinctID = req.session.distinctID;
  delete req.session.distinctID;
  instant2fa.confirmVerification({ 
    distinctID: distinctID, 
    token: req.body.instant2faToken
  }).then((verificationSucceeded) => {
    if (verificationSucceeded) {
      return getUser(distinctID)
        .then(recordTheLogin)
        .then((user) => res.render('logged_in', { user }));
    }
  }).catch((e) => {
    if (e.name == "VerificationMismatch") {
      handleMismatchError(e)
    } else if (e.name == "VerificationFailed") {
    	handleFailedVerification(e);
    } else {
      handlerError(e);
    }
  });
})
post '/two-factor-verification' do
  token = params['token']
  distinct_id = session['distinct_id']
  session['distinct_id'] = nil
  
  begin
    confirmed_verification = Instant2FA.confirm_verification(distinct_id, token)
    @user = User.find(distinct_id)
    login(user)
    erb :logged_in
  rescue Instant2FA::Errors::VerificationMismatch
    # verification did not match the user who was logging in
    erb :login
  rescue Instant2FA::Errors::VerificationFailed
    # user failed verification
    erb :login
  end
end
import instant2fa

@app.route('/two-factor-verification', methods=['POST'])
def two_factor_verification():
  token = request.json.get('instant2faToken')
  distinct_id = session.pop('distinct_id', None)
  try:
    confirmed_verification = instant2fa.confirm_verification(distinct_id, token)
  except instant2fa.errors.VerificationMismatch:
    return handle_mismatch_error()
  except instant2fa.errors.VerificationFailed:
    return handle_error()
  user = get_user(distinct_id)
  record_login(user)
  return render_template('logged_in', user=user)
public Result verify() {
    String distinctID = session().get("distinct_id");
    Form<Verification> form = form(Verification.class).bindFromRequest();
  
    session().remove("distinct_id");

    try {
        instant2FA.confirmVerification(distinctID, form.get().instant2faToken);
    } catch (IOException e) {
        return badRequest(index.render());
    } catch (VerificationMismatchException e) {
        return badRequest(index.render());
    } catch (VerificationFailedException e) {
        return badRequest(index.render());
    } catch (APIException e) {
        return badRequest(index.render());
    }

    User user = findUser(distinctID);
    recordTheLogin(user);
    return redirect(routes.Dashboard.index());
}
public function postVerify(Request $request) {
  $distinct_id = $request->session()->get('distinct_id');
  try {
    $request->session()->forget('distinct_id');

    $succeeded = $instant2fa->confirm_verification(
      $distinct_id, 
      $request->input('instant2faToken')
    );
    Auth::loginUsingId($distinct_id);
    return redirect()->intended($this->redirectPath());
  } catch (\Instant2FA\Error\VerificationMismatchException $e) {
    return $this->handleMismatchException($e);
  } catch (\Instant2FA\Error\VerificationFailedException $e) {
    return $this->handledFailedException($e);
  }
}

First, we grab the token, which is submitted as instant2faToken, and the distinct ID we stored in the session earlier. Then, we confirm the verification, passing both of them as arguments.

If verification was successful (the user typed a correct two-factor code), we'll get back a boolean that's always true. We log the user in like normal, since they have now completed the two-factor flow.

If verification failed, the function will throw an error. Above, we handle two different types of errors:

  1. VerificationMismatch. This happens if the distinct ID in the session doesn't match the value Instant2FA knows about on its side. This check is very similar to the state parameter in OAuth, so if it doesn't match, you should show an error.
  2. VerificationFailed. This happens if verification fails, likely because the user entered the wrong token too many times. You should show an error and redirect back to the login page to let them try again.

You did it!

Congratulations! you've implemented the entire 2FA flow for your web app. Give yourself a pat on the back and sip your beverage, which should still be hot.

Questions?

Send us an email at support@instant2fa.com and we'll help you out ASAP.

Integrating Instant2FA

Here's how to get 2FA in your web app in half an hour.