Leveraging Google Apps email to set up two-factor authentication for VPN

Two-factor authentication/2FA has become a necessary first step in keeping our network secure. As somone who VPNs a few times daily, using traditional 2FA solutions with hardware tokens or phone tokens like Google Authenticator would have been a major hassle. Plus having to work with every user to register yet another token would have been another burden on IT.

I wanted to explore options that could provide the same level of 2FA security without the hassle factor. Since all our users were already using Google Apps for email and had two-factor enabled on it, wouldn’t it be nice to just leverage that!

Normal VPN Auth flow

  1. User -> VPN Device
  2. VPN Device —radius—> Auth Server
  3. Auth Server OK -> VPN Device

VPN Auth flow w/ Google Apps 2FA

  1. User 1click visits a Google App Engine hosted site https://xxxxxxxx.trialpay.com which auto logs you in on browsers where you already read your Google Apps email.  This page just shows a 60 second timer for the user to complete the rest of the VPN login process.
  2. User -> VPN Device
  3. VPN Device —radius—> Auth Server
  4. Auth Server –curl–> https://xxxxxxxx.trialpay.com/?didulogin=$user
  5. Auth Server OK -> VPN Device


Auth Server config:

You must have a working freeradius2 server Authentication setup already. We’re just going to add an extra Authorization layer. Finally get to use one more A in AAA!

yum install perl-WWW-Curl freeradius2-perl

1. In  /etc/radb/sites-enabled/default  add “perl” to the authorize section

 authorize {
     perl

2. In /etc/raddb/modules/perl define the Perl file

perl {
      module = ${confdir}/perlauth.pl

3. perlauth.pl

use strict;
use warnings;
use WWW::Curl::Easy;
use vars qw(%RAD_REQUEST);
use constant RLM_MODULE_REJECT=>0;
use constant RLM_MODULE_OK=>2;
sub authorize {

    my $curl = WWW::Curl::Easy->new;
    $curl->setopt(CURLOPT_TIMEOUT, 5);
    $curl->setopt(CURLOPT_URL, 'http://xxxxxxxx.trialpay.com/didulogin?name='.$RAD_REQUEST{'User-Name'});
    my $response_body = '';
    open(my $fileb, ">", \$response_body);
    $curl->setopt(CURLOPT_WRITEDATA,$fileb);
    my $retcode = $curl->perform;

    #user didn't do 2FA first
    if ($response_body =~ /negative/) { return RLM_MODULE_REJECT; }

    # optional ip matching logic to allow and email InfoSec team of ip mismatch allowed access.
    #if ($response_body =~ /$RAD_REQUEST{'Calling-Station-Id'}/) { return RLM_MODULE_OK; }

    #allow all other errors to pass through as ok
    return RLM_MODULE_OK;
}

Google App Engine setup for https://xxxxxxxx.trialpay.com

Set up App Engine to require and only accept logins from your domain, and upload the Python code and template html file below. The code simply gets the username from google and creates a 60 sec memcache key with the user’s remote IP as value.

  • main.py
#!/usr/bin/env python
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from google.appengine.dist import use_library
use_library('django', '1.2')
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.api import memcache
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.ext.webapp.util import login_required

class MainHandler(webapp.RequestHandler):
  @login_required
  def get(self):
    tmout = 60
    ip = self.request.remote_addr
    user = users.get_current_user()
    template_values = { 'tmout': tmout }
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.headers.add_header("Set-Cookie", "SACSID=deleted; Expires=Thu, 01-Jan-2001 00:00:00 GMT")
    self.response.out.write(template.render(path, template_values))
    memcache.add(key=user.nickname(), value=ip, time=tmout)

class diduloginHandler(webapp.RequestHandler):
  def get(self):
    name = self.request.get("name")
    data = memcache.get(name)
    if data is not None:
        self.response.out.write(data)
    else:
       self.response.out.write("negative") 

def main():
  application = webapp.WSGIApplication([('/', MainHandler),
                                        ('/didulogin', diduloginHandler)],
                                       debug=True)
  run_wsgi_app(application)

if __name__ == '__main__':
  main()
  • Sample index.html file
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
    var count = {{ tmout }} -1 ;
    countdown = setInterval(function(){
    $(".countdown").html(count);
    count--;
    }, 1000);
});
</script>
</head> <body> <span class="countdown">{{ tmout }}</span> </body>

FAQ & Considerations

  • IP Matching  –  An optional security enhancement.  IP used at xxxxxxxx website occasionally won’t match the vpn client ip – so it’s better to email user and infosec instead of denying access.
  • High Availability – Google App Engine being a PaaS should in theory be highly available, but as with any service it can go down (once in 6 months so far). The Perl auth layer should be designed to be fail-safe. Even then partial failures, such as only memcache layer being unavailable, could happen, so you should have a planned way to disable this.
  • Cost – We choose the SNI SSL cert. option which kept our total cost at $9/month.
This entry was posted in Engineering. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s