Using Authy for 2FA
March 31, 2017
The Problem:
Storing a User’s Social Security number in your database is one of the most sensitive pieces of information you can store. We needed to make sure the data was not only encrypted and secure, but that if an admin’s account were to be compromised, there were security measures in place to prevent the attacker from getting social security numbers.
The Solution:
Two Factor Authentication for any Admin that has the privileges to see legal data. This ensures that even if their Admin account is compromised, the attacker would also have to have access to their physical device in order to access social security information.
The Authy Dashboard:
The service we chose for 2FA is Authy. It is very straightforward to implement and they provide helper libraries to make the process even simpler.
OneTouch
When most people think of Two-Factor Authentication they think of a text message or email being sent to them containing a code that they need to enter. Authy provides OneTouch capabilities which means all the Admin has to do is download the Authy app and Kickfurther.com uses their phone number to generate an account with Authy directly on their app. Then once a OneTouch request is sent, the Admin gets a push notification and all they need to do is hit accept or deny.
Sending the OneTouch Request:
Once an Admin clicks the View SSN button, a request is sent to Authy to tell it to send a OneTouch notification to the corresponding Admin. The Callback URL for OneTouch events needs to be set in the Authy dashboard to whatever endpoint your code is using.
This is the Authy Middleware component for Laravel for the route that gets set as the callback. View the Gist.
namespace App\Http\Middleware;
use Closure;
class ValidateAuthyRequest {
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
protected function check_bool($value) {
if(is_bool($value)) {
$value = ($value) ? 'true' : 'false';
} else {
$value = (is_null($value)) ? '' : $value;
}
return $value;
}
protected function sort_params($params) {
$new_params = array();
foreach ($params as $k => $v) {
if (is_array($v)) {
ksort($v);
$new_params[$k] = $v;
foreach ($v as $k2 => $v2) {
if (is_array($v2)) {
ksort($v2);
$new_params[$k][$k2] = $v2;
foreach ($v2 as $k3 => $v3) {
$v3 = $this->check_bool($v3);
$new_params[$k][$k2][$k3] = $v3;
}
} else {
$v2 = $this->check_bool($v2);
$new_params[$k][$k2] = $v2;
}
}
} else {
$v = $this->check_bool($v);
$new_params[$k] = $v;
}
}
ksort($new_params);
return $new_params;
}
public function handle($request, Closure $next)
{
$key = env('AUTHY_API_KEY');
$uri = $request->path();
$params = $request->all();
$nonce = $request->header("X-Authy-Signature-Nonce");
$theirs = $request->header('X-Authy-Signature');
$sorted_params = $this->sort_params($params);
$query = http_build_query($sorted_params);
$message = $nonce . '|' . $request->method() . '|' . env('AUTH_URL', env('APP_URL')) .'/'. $uri . '|' . $query;
$s = hash_hmac('sha256', $message, $key, true);
$mine = base64_encode($s);
if ($theirs != $mine) {
return "Not a valid Authy request.";
} else {
return $next($request);
}
}
}
Notice the handle method which verifies that the expected hash based on the nonce, app URL, and individual query all match. This is the defense to prevent CSRF.
A Successful Verification:
Once our application gets the callback that the user approved the request we can successfully return the sensitive data via a one time api call from the Vue component to display the sensitive data.
Conclusion:
Security is not an absolute, it’s a continuous process and should be managed as such. Security is about risk reduction, not risk elimination, and risk will never be zero. Defense of Depth subscribes to the concept that there is no single solution capable of addressing all security concerns. Instead, it promotes the use of a layered approach to complementary security solutions each designed to address each others shortfalls.