Idea
We've recently attended the AWS London summit and this year it seemed like the focus was AI and machine learning.
One of the services I've been meaning to play with is Rekognition, whichh can do face detection and compare two faces among many other things.
While sitting in one of the sessions at the summit I had the idea to build a Drupal 8 module which uses the Rekognition module to authenticate a user by detecting their face from a webcam shot and comparing it with their uploaded profile picture.
The module uses the webcamjs library (not actively maintained any more but for the purposes of this exercise it works fine) to capture a shot from the user's webcam/mobile camera, and then the AWS PHP SDK to compare the images.
Note: most modern browsers will only allow websites to access a user's webcam if they are on secure origins (using https). You can issue yourself a self-signed certificate if you want to test this locally (or use `localhost`)
If the faces match with a similarity of more than 90% (or any other custom threshold that you set) then the user is authenticated and logged in.
The form
I started by creating a basic Drupal 8 route at `/face/login` to display a form containing an empty `div` and a text field for the `username`.
# face_login/face_login.routing.yml
face_login.face_login:
path: 'face/login'
defaults:
_form: '\Drupal\face_login\Form\FaceLogin'
_title: 'Face Login'
requirements:
_access: 'TRUE'
// face_login/src/Form/FaceLogin.php
$form['webcam'] = [
'#markup' => '<div id="webcam"></div><div id="webcam_image"></div>',
];
$form['username'] = [
'#type' => 'textfield',
'#title' => 'Username',
'#required' => TRUE,
];
$form['target'] = [
'#type' => 'hidden',
'#value' => '',
];
$form['#validate'][] = '::validateFaces';
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
$form['#attached']['library'][] = 'face_login/webcamjs';
$form['#attached']['library'][] = 'face_login/face_login';
The module has the webcamjs library and a custom js file declared as dependencies. The custom js file loads up the webcam code and attaches the camera stream to the empty div on the form.
# face_login/face_login.libraries.yml
webcamjs:
version: 1.x
js:
js/webcam.min.js: {}
face_login:
version: 1.x
js:
js/face_login.js: {}
dependencies:
- core/jquery
// face_login/js/face_login.js
Webcam.set({
width: 320,
height: 240,
image_format: 'jpeg',
jpeg_quality: 90
});
Webcam.attach( '#webcam' );
Attached to the camera div is a click event, so when the user taps/clicks on the image a screenshot is taken and displayed alongside.
// face_login/js/face_login.js
$('video').click(function() {
Webcam.snap(function(data_uri) {
$('#webcam_image').html("<img src='" + data_uri + "'/>" );
var base64result = data_uri.split(',')[1];
$('input[name=target]').val(base64result);
});
});
Rekognition
The most important part of the module takes place in a validate handler where the camera shot is loaded into a variable and the user profile is loaded based on the provided username.
The user's profile picture is sent alongside the camera shot to the Rekognition Compare Faces API with a threshold of 90.
public function validateFaces(array &$form, FormStateInterface $form_state) {
// Load user by username.
$accounts = $this->userStorage->loadByProperties(['name' => $form_state->getValue('username')]);
$account = reset($accounts);
// Load user profile picture.
$user_picture = $account->user_picture->first();
$target_id = $user_picture->getValue('target_id')['target_id'];
$image_profile = \Drupal\file\Entity\File::load($target_id);
// Webcam shot is saved in base64 string so need to decode that into image bytes.
$image_login = $form_state->getUserInput()['target'];
$image_login = base64_decode($image_login);
// If both images have been provided then proceed,
if ($image_profile && $image_login) {
// Load the profile picture into image bytes.
$image_profile = file_get_contents($image_profile->getFileUri());
// Prepare some AWS config.
$options = [
'region' => 'eu-west-1',
'version' => '2016-06-27',
'profile' => \Drupal::config('aws')->get('profile'),
];
try {
$client = new RekognitionClient($options);
$result = $client->compareFaces([
'SimilarityThreshold' => 90,
'SourceImage' => [
'Bytes' => $image_profile,
],
'TargetImage' => [
'Bytes' => $image_login,
],
]);
if (count($result['FaceMatches']) == 0) {
$form_state->setErrorByName('form', $this->t('Login unsuccessful.'));
}
else {
$similarity = $result['FaceMatches'][0]['Similarity'];
$markup = 'Successful login. Login image is ' . $similarity . '% similar to the Profile picture';
drupal_set_message($markup);
// Save the user account id to be used in submit handler.
$form_state->set('uid', $account->id());
}
}
catch (\Exception $e) {
$form_state->setErrorByName('form', $this->t('An error occurred.'));
}
}
}
public function submitForm(array &$form, FormStateInterface $form_state) {
// Load the user account.
$account = $this->userStorage->load($form_state->get('uid'));
// Log the user in.
user_login_finalize($account);
// Redirect to user profile page.
$form_state->setRedirect('user.page');
}
If the two faces match up then the login is finalised and the user is logged in and redirected to their user profile.
Note: more validation can be done here - if the image provided does not contain a face then an Exception is thrown, so you could first use Detect Faces API to make sure a face is actually present.
Conclusion
This was just an exercise to learn more about this service, so I wouldn't recommend using this module without an additional security step - remember someone could print a photo and hold it in front of the webcam and be able to log in as that person on a site as long as they know that user's email address or username on the site.
You could use this module in conjunction with a 2FA module or with some extra security questions that a user would set in their profile.
To view the full module source code go to this repo.
Note: the module depends on the `aws/aws-sdk-php` package. The module could be tidied up a bit more and a composer file included, etc.