Skip to content

Commit 141b8b5

Browse files
committed
Init
1 parent 114cdb3 commit 141b8b5

36 files changed

+34097
-1
lines changed

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
1-
# ProcessWire-TfaWebAuthn
1+
# Processwire-TfaWebAuthn
2+
![Security Key](https://raw.githubusercontent.com/adamxp12/Processwire-TfaU2F/master/assets/bluekeyside.png)
3+
4+
This module is essentially an update to my existing [U2F Module](https://github.com/adamxp12/Processwire-TfaU2F) but upgraded to use WebAuthn instead of U2F so it will continue to work in Chrome after Febuary 2022.
5+
6+
7+
Note this is very much a proof of concept. it does work but I cant guarantee its reliability. its also sadly limited to a single non cross platform credential (Windows Hello, Apple TouchID/FaceID) due to the Tfa class which only allows one Tfa method at a time and also locks out the options once setup.
8+
9+
Its easy to setup just install the module then enable Tfa under your user profile. Enroll your key then next time you need to login you will be asked to use your WebAuthn Credential. Add as many as you like (though the will be a limit I have tested 4x succesfully)
10+
11+
Unlike the previous U2F libary this has many advantages. Not only do you get on device credentials like Windows Hello but you also get far better cross platform support includng NFC/Bluetooth support. I have tested it with a YubiKey on an iPhone. registered from USB on my laptop and authenticated via NFC on my phone.
12+
13+
This module is not a direct upgrade from the previous U2F module and will require setting up all your keys again but the was no easy way to transistion from the old U2F data. It is also a stop-gap solution until ProcessWire adds native WebAuthn support in the futue.
14+
15+
16+
The Yubikey Security Key Graphic was sourced from Pixabay https://pixabay.com/illustrations/google-secure-key-u2f-security-key-3598222/
17+
18+
## Demo
19+
![Demo](https://adamxp12blob.blob.core.windows.net/sharex-share/2019-08-28_12-06-18.gif)

TfaWebAuthn.info.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php namespace ProcessWire;
2+
$info = array(
3+
'title' => 'WebAuthn two-factor authentication ',
4+
'summary' => 'For modern two factor authentication with U2F keys and on device credentials like Fingerprint',
5+
'version' => 100,
6+
'singular' => true,
7+
'requires' => 'ProcessWire>=3.0.109'
8+
);

TfaWebAuthn.module

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
namespace ProcessWire;
3+
4+
/**
5+
* TfaWebAuthn (1.0.0)
6+
* Adds WebAuthn/FIDO2 as a TFA option
7+
*
8+
* @author Adam Blunt
9+
*
10+
* ProcessWire 3.x
11+
* Copyright (C) 2011 by Ryan Cramer
12+
* Licensed under GNU/GPL v2, see LICENSE.TXT
13+
*
14+
* http://www.processwire.com
15+
* http://www.ryancramer.com
16+
*
17+
*/
18+
require_once 'WebAuthn/WebAuthn.php';
19+
//use lbuchs\WebAuthn\Binary\ByteBuffer;
20+
class TfaWebAuthn extends Tfa implements Module, ConfigurableModule
21+
{
22+
public function __construct()
23+
{
24+
parent::__construct();
25+
$this->WebAuthn = new \lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $this->wire('config')->httpHost); // $formats
26+
$this->userVerification = "discouraged";
27+
$this->crossPlatformAttachment = null; // True only crossplatfrm aka USB keys, False only single device like TPM/Winows Hellow, Null allow both
28+
$this->typeUsb = true;
29+
$this->typeNfc = true;
30+
$this->typeBle = true;
31+
$this->typeInt = true;
32+
}
33+
34+
public function init()
35+
{
36+
if(config()->version('3.0.165')) {
37+
// 3.0.165 and newer have an init() function in the Tfa class. previous versions did not
38+
// Strangely this init() function while not needed before is now required in new versions even if you dont use the auto-enable feature
39+
parent::init();
40+
}
41+
$this->addHookBefore('Tfa::getUserSettingsInputfields', $this, 'addScripts');
42+
$this->addHookBefore('Tfa::render', $this, 'addScripts');
43+
}
44+
45+
46+
public function addScripts()
47+
{
48+
$this->config->scripts->add($this->wire('config')->urls->$this . "WebAuthn.js");
49+
}
50+
51+
public function enabledForUser(User $user, array $settings)
52+
{
53+
return $settings['enabled'] === true;
54+
}
55+
56+
57+
public function isValidUserCode(User $user, $code, array $settings)
58+
{
59+
if (!strlen($code)) {
60+
return false;
61+
}
62+
63+
$authreg = $this->session->authreg;
64+
$authreq = $this->session->authreq;
65+
$this->session->authreq = null;
66+
$this->session->authreg = null;
67+
68+
$code = json_decode($code);
69+
70+
71+
$clientDataJSON = base64_decode($code->clientDataJSON);
72+
$authenticatorData = base64_decode($code->authenticatorData);
73+
$signature = base64_decode($code->signature);
74+
$userHandle = base64_decode($code->userHandle);
75+
$id = base64_decode($code->id);
76+
$challenge = $this->session->authchallange;
77+
$credentialPublicKey = null;
78+
// Get the public key for the given credential
79+
if (is_array($authreg)) {
80+
foreach ($authreg as $reg) {
81+
$o = (object) $reg;
82+
if ($o->credentialId === bin2hex($id)) {
83+
$credentialPublicKey = $o->credentialPublicKey;
84+
break;
85+
}
86+
}
87+
}
88+
89+
try {
90+
if ($credentialPublicKey === null) {
91+
exit('Public Key for credential ID not found!');
92+
}
93+
// process the get request. throws WebAuthnException if it fails
94+
$this->WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, null, $this->userVerification === 'required');
95+
return true;
96+
} catch (Exception $e) {
97+
return false;
98+
}
99+
100+
return false;
101+
}
102+
103+
104+
// Create settings page for server
105+
public function ___getUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, $settings)
106+
{
107+
parent::___getUserSettingsInputfields($user, $fieldset, $settings);
108+
109+
if ($this->enabledForUser($user, $settings)) {
110+
} elseif ($this->wire('input')->requestMethod('POST')) {
111+
$fieldset->new('text', 'regdata')
112+
->attr('maxlength', 20480);
113+
} else {
114+
$createArgs = $this->WebAuthn->getCreateArgs(\hex2bin($user->id), $user->name, $user->name, 20, false, $this->userVerification, $this->crossPlatformAttachment);
115+
$fieldset->new('hidden', 'createData')
116+
->attr('id', 'TfaWebAuthn_createData')
117+
->attr('value', json_encode($createArgs));
118+
119+
$fieldset->new('hidden', 'regdata')
120+
->attr('id', 'TfaWebAuthn_regdata')
121+
->attr('maxlength', 20480);
122+
123+
$fieldset->new('button', 'addKey', 'Enable two-factor authentication')
124+
->attr('id', 'TfaWebAuthn_button')
125+
->attr('onclick', "TfaWebAuthn_addkey()")
126+
->value("Add WebAuthn Credential");
127+
128+
$fieldset->new('markup')
129+
->attr('value', "<span id='TfaWebAuthn_msg'>Click the button to register a new credential.</span>");
130+
$_SESSION["chal"] = (string) $this->WebAuthn->getChallenge();
131+
}
132+
}
133+
134+
// Save registration data from the User settings
135+
public function ___processUserSettingsInputfields(User $user, InputfieldWrapper $fieldset, $settings, $settingsPrev)
136+
{
137+
$settings = parent::___processUserSettingsInputfields($user, $fieldset, $settings, $settingsPrev);
138+
try {
139+
$challenge = $_SESSION["chal"];
140+
141+
142+
143+
$aryreg = json_decode("[".$settings['regdata']."]");
144+
$data = array();
145+
foreach ($aryreg as $reg) {
146+
147+
$clientdata = base64_decode($reg->clientDataJSON);
148+
$attestationObject = base64_decode($reg->attestationObject);
149+
$d = $this->WebAuthn->processCreate($clientdata, $attestationObject, $challenge, $this->userVerification === 'required', true, false);
150+
array_push($data, (array) $d );
151+
}
152+
$sd = serialize($data);
153+
$settings['regkeys'] = $sd;
154+
$settings['enabled'] = true;
155+
$settings['challenge'] = null;
156+
$settings['regdata'] = null;
157+
$this->message("Success! Your account is now secured with two-factor authentication");
158+
} catch (Exception $e) {
159+
$settings['enabled'] = false;
160+
$this->error("That did not work " . $e);
161+
}
162+
163+
return $settings;
164+
}
165+
166+
167+
protected function getDefaultUserSettings(User $user)
168+
{
169+
return array(
170+
'enabled' => false,
171+
'regkeys' => ''
172+
);
173+
}
174+
175+
// Display the tFA form
176+
public function buildAuthCodeForm()
177+
{
178+
$user = $this->getUser();
179+
$settings = $this->getUserSettings($user);
180+
181+
$authreg = unserialize($settings['regkeys']);
182+
$ids = array();
183+
$this->session->authreg = $authreg;
184+
185+
186+
$ids = array();
187+
if (is_array($authreg)) {
188+
foreach ($authreg as $reg) {
189+
$o = (object) $reg;
190+
array_push($ids, $o->credentialId);
191+
}
192+
}
193+
if (count($ids) === 0) {
194+
throw new Exception('no WebAuthn registrations for userId ' . $user->id);
195+
}
196+
197+
198+
/*
199+
* Check if the authreq is null. if so make a new challenge
200+
* ProcessWire annoingly calls builtAuthCodeForm twice. once to build the form and a 2nd time when the user submits the form
201+
* thus validation was failing as the was non-matching challanges
202+
*/
203+
if (is_null($this->session->authreq)) {
204+
$req = $this->WebAuthn->getGetArgs($ids, 20, $this->typeUsb, $this->typeNfc, $this->typeBle, $this->typeInt, $this->userVerification);
205+
$this->session->authreq = json_encode($req);
206+
$this->session->authchallange = (string) $this->WebAuthn->getChallenge();
207+
}
208+
209+
210+
211+
$form = $this->modules->get('InputfieldForm');
212+
$form->attr('action', "./?$this->keyName=" . $this->getSessionKey(true))
213+
->attr('id', "tfaform");
214+
$form->new('markup')
215+
->attr('value', "<img style='height:80px' onload='TfaWebAuthn_authKey()' src='" . $this->wire('config')->urls->$this . "assets\bluekeyside.png'><br>
216+
You need to use your security key to login.<br>
217+
Insert it now and tap/click the key to verify its you. If your key does not have a button just unplug it and then plug it back in.<br>
218+
<div uk-alert class='uk-alert-danger' style='display:none' id='TfaWebAuthn_error'></div>");
219+
$form->new('hidden', 'authreq')
220+
->attr('id', 'TfaWebAuthn_authreq')
221+
->attr('value', $this->session->authreq);
222+
$form->new('hidden', 'tfa_code')
223+
->attr('id', 'TfaWebAuthn_authresponse')
224+
->attr('required', 'required');
225+
$form->new('button', 'authKey', 'Start two-factor authentication')
226+
->attr('id', 'TfaWebAuthn_button')
227+
->attr('onclick', "TfaWebAuthn_authKey()")
228+
->value("Use Security Key");
229+
return $form;
230+
}
231+
}

WebAuthn.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
var Tfa_count = 1;
2+
3+
function TfaWebAuthn_addkey() {
4+
var createDataRaw = document.getElementById('TfaWebAuthn_createData').value;
5+
var regdata = document.getElementById('TfaWebAuthn_regdata');
6+
7+
var json = JSON.parse(createDataRaw);
8+
9+
recursiveBase64StrToArrayBuffer(json);
10+
11+
navigator.credentials.create(json).then(function(f) {
12+
console.log(f);
13+
14+
var data = {
15+
clientDataJSON: f.response.clientDataJSON ? arrayBufferToBase64(f.response.clientDataJSON) : null,
16+
attestationObject: f.response.attestationObject ? arrayBufferToBase64(f.response.attestationObject) : null
17+
};
18+
19+
if(regdata.value === "") {
20+
regdata.value = JSON.stringify(data);
21+
document.getElementById('TfaWebAuthn_msg').textContent = 'Credential added. You can add more now or save this page to finish setup';
22+
} else {
23+
regdata.value = regdata.value+", "+JSON.stringify(data);
24+
document.getElementById('TfaWebAuthn_msg').textContent = 'Credential '+Tfa_count+' added. You can add even more or save this page to finish setup';
25+
}
26+
Tfa_count++;
27+
document.getElementById('TfaWebAuthn_button').classList.remove('ui-state-active');
28+
29+
console.log(regdata.value);
30+
});
31+
32+
}
33+
34+
function TfaWebAuthn_authKey() {
35+
var authreq = JSON.parse(document.getElementById('TfaWebAuthn_authreq').value);
36+
recursiveBase64StrToArrayBuffer(authreq);
37+
navigator.credentials.get(authreq).then(function(f){
38+
39+
var data = {
40+
id: f.rawId ? arrayBufferToBase64(f.rawId) : null,
41+
clientDataJSON: f.response.clientDataJSON ? arrayBufferToBase64(f.response.clientDataJSON) : null,
42+
authenticatorData: f.response.authenticatorData ? arrayBufferToBase64(f.response.authenticatorData) : null,
43+
signature: f.response.signature ? arrayBufferToBase64(f.response.signature) : null,
44+
userHandle: f.response.userHandle ? arrayBufferToBase64(f.response.userHandle) : null
45+
};
46+
47+
console.log(data);
48+
document.getElementById('TfaWebAuthn_error').style.display = "none";
49+
document.getElementById('TfaWebAuthn_authresponse').value = JSON.stringify(data);
50+
document.getElementById('tfaform').submit();
51+
}).catch(function(err) {
52+
document.getElementById('TfaWebAuthn_error').textContent = "authentication failed with error: " + err;
53+
document.getElementById('TfaWebAuthn_error').style.display = "block";
54+
});
55+
}
56+
57+
58+
59+
60+
61+
function recursiveBase64StrToArrayBuffer(obj) {
62+
let prefix = '=?BINARY?B?';
63+
let suffix = '?=';
64+
if (typeof obj === 'object') {
65+
console.log("object");
66+
for (let key in obj) {
67+
if (typeof obj[key] === 'string') {
68+
let str = obj[key];
69+
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
70+
str = str.substring(prefix.length, str.length - suffix.length);
71+
72+
let binary_string = window.atob(str);
73+
let len = binary_string.length;
74+
let bytes = new Uint8Array(len);
75+
for (let i = 0; i < len; i++) {
76+
bytes[i] = binary_string.charCodeAt(i);
77+
}
78+
obj[key] = bytes.buffer;
79+
}
80+
} else {
81+
recursiveBase64StrToArrayBuffer(obj[key]);
82+
}
83+
}
84+
}
85+
}
86+
87+
function arrayBufferToBase64(buffer) {
88+
let binary = '';
89+
let bytes = new Uint8Array(buffer);
90+
let len = bytes.byteLength;
91+
for (let i = 0; i < len; i++) {
92+
binary += String.fromCharCode( bytes[ i ] );
93+
}
94+
return window.btoa(binary);
95+
}

WebAuthn/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Netbeans project
2+
nbproject/
3+
/index.php
4+
5+
6+
# .pem files from FIDO Alliance Metadata Service (MDS)
7+
_test/rootCertificates/mds/*.pem
8+
_test/rootCertificates/mds/lastMdsFetch.txt

0 commit comments

Comments
 (0)