NOTE: If you are planning to test the client and the server component of this web application on the same computer—designated COMBINED in this document—then make sure you have a version of the browser that supports FIDO2.
If you plan to use multiple computers to test this web application, the computer on which the server part of this web application is running is designated as APPSERVER while the computer running the browser(s) is designated APPCLIENT.
The computer running the FIDO2 server is designated FIDO2SERVER.
CentOS 8
sudo yum install -y gcc-c++ make
sudo yum install nodejs
sudo yum install sqlite
CentOS 7
Install any version 10.x.x or higher of Node.js from the following link:
https://nodejs.org/en/download/
sudo yum install npm
sudo yum install sqlite
Ubuntu
sudo apt-get update
sudo apt install nodejs
sudo apt install npm
sudo apt install sqlite
Windows 10
Mac OS
tar zxvf sqlite-autoconf-[version number].tar.gz
cd sqlite-autoconf-[version number]
./configure --prefix=/usr/local
make
make install
If using a single computer for testing the client and server portions of this tutorial on COMBINED, modify /etc/hosts or C:\Windows\System32\drivers\etc\hosts file (depending on whether you are using Linux/OS-X or Windows) to include fido2tutorial.strongkey.com as an alias for localhost (the entry with 127.0.0.1).
If you plan to test the tutorial web application with a browser from a different client computer (APPCLIENT) while running the server component of the tutorial web application on APPSERVER, then identify the IP address of your APPSERVER and add the fido2tutorial.strongkey.com alias to APPSERVER’s IP address within the hosts file on the APPCLIENT:
CentOS/Ubuntu/Mac
sudo vi /etc/hosts
Windows
Ping fido2tutorial.strongkey.com.
NOTE: If you have a firewall on the APPSERVER/COMBINED, add a rule to open port 3001 so network connections can reach the web application.
CentOS (If you are using Ubuntu use apt instead of yum.)
sudo yum install firewalld
sudo firewall-cmd --zone=public --add-port=3001/tcp --permanent
sudo firewall-cmd --complete-reload
Deploy the project.
a. Download the prefido2 source code. If you are using Ubuntu use apt instead of yum.
sudo yum install wget
wget https://sourceforge.net/projects/strongkeyfido/files/v4.8.0/tutorial/tutorial.tgz
If using Windows, download to the following file:
https://sourceforge.net/projects/strongkeyfido/files/v4.8.0/tutorial/tutorial.tgz/download
b. Unzip the StrongKey FIDO2 Tutorial:
tar zxvf prefido2.tgz
c. Change directory into prefido2:
cd prefido2/
d. Install the required node modules.
npm install
e. Install pm2. This is the process manager we will use to run the application:
sudo npm install pm2@latest -g
f. For Windows only: install node-gyp and sqlite3 manually.
npm install -g node-gyp
npm install sqlite3
g. Start the project.
pm2 start main.js
h. Take a snapshot of your currently running Node applications; this allows pm2 to restart your application automatically upon restart of pm2. Ignore this step on Windows/Mac OS.
sudo pm2 startup systemd
i. Take a snapshot of your currently running Node applications which allows pm2 to restart your application automatically upon restart of pm2.
pm2 save
j. Open a browser to https://fido2tutorial.strongkey.com:3001. When prompted, add an exception for a self-signed certificate. Messages will vary by browser.
NOTE: The default certificate is a 1-year certificate.
The home page displays. Register a new user by clicking Sign Up. Enter the required registration information and click Sign Up.
Login using the credentials you just registered. The sample application Quote Boat displays.
Do whatever floats your boat, then Logout.
Copy the PREFIDO2 web application folder (prefido2) and rename the copy to postfido2.
cp -r prefido2 postfido2
Delete old users from the database.
sqlite3 postfido2/db/aftdb.db
delete from users;
.exit
Open postfido2/templates/register.html in your preferred text editor.
Copy this snippet of code (between the AAAAA... lines, but NOT including the AAAAA…. lines) and paste it between the AAAAA lines in register.html, replacing the existing content in the HTML file.
Here we are removing the action and method attributes of the form to remove the old registration post request. This will be replaced by a call to a function in functions.js.
Copy this snippet of code (between the BBBBB... lines, but NOT including the BBBBB…. lines) and paste between the BBBBB lines in register.html, replacing the existing content in the HTML file.
Here we are removing the passcontainer and password input and replacing it with id displayname input, which is used to identify the name of the FIDO2 Token used in registration.
<input class="input-out" type="text" id="displayname" name="displayname" placeholder="Display Name">
Copy this snippet of code (between the CCCCC... lines) and paste between the CCCCC lines in register.html, replacing the existing content in the HTML file.
Here we remove the type, form, and value from the button and add onclick="submitForm('registration')". This will call the function that will be added to functions.js.
<button id="user-content-regbutton" onclick="submitForm('registration')">Sign Up</button>
Copy this snippet of code (between the DDDDD... lines) and paste between the DDDDD lines in register.html.
<script type="text/javascript" src="/js/common.js"></script>
Open postfido2/templates/login.html in your preferred text editor.
Copy this snippet of code (between the EEEEE... lines) and paste between the EEEEE lines in login.html, replacing the existing content in the HTML file.
Here we remove the action and method aspects of the form to remove the old login post request. This will be replaced by a call to a function in functions.js.
Delete the snippet of code (between the FFFFF... lines). Here we delete the passcontainer and password input. They are no longer needed thanks to FIDO2!
Copy this snippet of code (between the GGGGG... lines) and paste between the GGGGG lines in login.html, replacing the existing content in the HTML file.
Here we remove the type, form, and value attributes from the button and add onclick="submitForm('authentication')". This will call the function that will be added to to functions.js.
<button onclick="submitForm('authentication')">Sign In</button>
Copy this snippet of code (between the HHHHH... lines) and paste between the HHHHH lines in register.html.
<script type="text/javascript" src="/js/common.js"></script>
Open postfido2/templates/js/functions.js in your preferred text editor.
Copy this snippet of code (between the IIII... lines) and paste between the IIII lines in functions.js.
Here we add functions to the APPCLIENT that are used to request challenges from the APPSERVER, pass the challenges to the FIDO2 Token, and submit challenge results to the APPSERVER.
//IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
function submitForm(intent){
if(intent=="registration"){
$.post('/getChallenge', {
'intent' : intent,
'username': $('#regusername').val(),
'displayname': $('#displayname').val(),
'firstname': $('#firstname').val(),
'lastname': $('#lastname').val()
}).done(resp => {
if(resp.Response == "sqlite-error"){
console.log(resp.Response);
location.reload();
}else if(resp.Response == "skfs-error"){
console.log(resp.Response);
} else {
document.getElementById("failed").style.display = "none";
document.getElementById("failedbreak").style.display = "none";
callFIDO2Token(intent,resp.Response);
}
}).fail((jqXHR, textStatus, errorThrown) => {
alert(jqXHR, textStatus, errorThrown);
});
} else if(intent=="authentication")
$.post('/getChallenge', {
'intent' : intent,
'username': $('#username').val()
})
.done((resp) => {
if(!resp.Response.toString().toLowerCase().includes("error")){
callFIDO2Token(intent,resp.Response);
} else {
alert("Username not registered");
}
})
.fail((jqXHR, textStatus, errorThrown) => {
alert(jqXHR, textStatus, errorThrown);
});
}
function callFIDO2Token(intent,challenge) {
let challengeBuffer = challengeToBuffer(challenge);
let credentialsContainer = window.navigator;
if(intent=="registration"){
credentialsContainer.credentials.create({ publicKey: challengeBuffer.Response })
.then(credResp => {
let credResponse = responseToBase64(credResp);
credResponse.intent = intent;
$.post('/submitChallengeResponse', credResponse)
.done(regResponse => onResult(intent,regResponse))
.fail((jqXHR, textStatus, errorThrown) => {
console.log(jqXHR, textStatus, errorThrown);
});
})
.catch(error => {
alert(error);
});
} else if (intent=="authentication"){
credentialsContainer.credentials.get({ publicKey: challengeBuffer.Response })
.then(credResp => {
let credResponse = responseToBase64(credResp);
credResponse.intent = intent;
$.post('/submitChallengeResponse', credResponse)
.done(authResponse => onResult(intent,authResponse))
.fail((jqXHR, textStatus, errorThrown) => {
alert(jqXHR, textStatus, errorThrown);
});
})
.catch(error => {
alert(error);
});
}
}
function onResult(intent,response){
if(intent=="registration"){
if(!response.Response.toString().toLowerCase().includes("error")){
window.location.replace(window.location.protocol + "//" + window.location.host + "/login");
} else {
alert(response.Response);
}
} else if(intent=="authentication"){
if(response.Response.toString().includes("Successfully processed sign response")){
window.location.replace(window.location.protocol + "//" + window.location.host + "/dashboard");
} else {
alert(response.Response);
}
}
}
//IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII
Create the file postfido2/templates/js/common.js.
//JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
function challengeToBuffer(input) {
input = JSON.parse(input);
input.Response.challenge = decode(input.Response.challenge);
if(typeof input.Response.user !== 'undefined'){
input.Response.user.id = decode(input.Response.user.id);
}
if (input.Response.excludeCredentials) {
for (let i = 0; i < input.Response.excludeCredentials.length; i++) {
input.Response.excludeCredentials[i].id = input.Response.excludeCredentials[i].id.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
input.Response.excludeCredentials[i].id = decode(input.Response.excludeCredentials[i].id);
}
}
if (input.Response.allowCredentials) {
for (let i = 0; i < input.Response.allowCredentials.length; i++) {
input.Response.allowCredentials[i].id = input.Response.allowCredentials[i].id.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
input.Response.allowCredentials[i].id = decode(input.Response.allowCredentials[i].id);
}
}
return input;
}
function responseToBase64(input) {
let copyOfDataResponse = {};
copyOfDataResponse.id = input.id;
copyOfDataResponse.rawId = encode(input.rawId);
if(typeof input.response.attestationObject !== 'undefined'){
copyOfDataResponse.attestationObject = encode(input.response.attestationObject);
}
if(typeof input.response.authenticatorData !== 'undefined'){
copyOfDataResponse.authenticatorData = encode(input.response.authenticatorData);
copyOfDataResponse.signature = encode(input.response.signature);
copyOfDataResponse.userHandle = encode(input.response.userHandle);
}
copyOfDataResponse.clientDataJSON = encode(input.response.clientDataJSON);
copyOfDataResponse.type = input.type;
return copyOfDataResponse;
}
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
let lookup = new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
let encode = function (arraybuffer) {
let bytes = new Uint8Array(arraybuffer),
i, len = bytes.length, base64url = '';
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2];
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64url += chars[bytes[i + 2] & 63];
}
if ((len % 3) === 2) {
base64url = base64url.substring(0, base64url.length - 1);
} else if (len % 3 === 1) {
base64url = base64url.substring(0, base64url.length - 2);
}
return base64url;
};
let decode = function (base64string) {
let bufferLength = base64string.length * 0.75,
len = base64string.length, i, p = 0,
encoded1, encoded2, encoded3, encoded4;
let bytes = new Uint8Array(bufferLength);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64string.charCodeAt(i)];
encoded2 = lookup[base64string.charCodeAt(i + 1)];
encoded3 = lookup[base64string.charCodeAt(i + 2)];
encoded4 = lookup[base64string.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return bytes.buffer
};
//JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
Create the file postfido2/constants.js.
//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
const DID = 1;
const PROTOCOL = "FIDO2_0";
const AUTHTYPE = "PASSWORD";
const SVCUSERNAME = "svcfidouser";
const SVCPASSWORD = "Abcd1234!";
exports.SKFS_HOSTNAME = "demo4.strongkey.com";
exports.SKFS_PORT="8181";
exports.SVCINFO = {
did: DID,
protocol: PROTOCOL,
authtype: AUTHTYPE,
svcusername: SVCUSERNAME,
svcpassword: SVCPASSWORD
};
exports.SKFS_PREAUTHENTICATE_PATH = '/skfs/rest/preauthenticate'
exports.SKFS_AUTHENTICATE_PATH = '/skfs/rest/authenticate'
exports.SKFS_PREREGISTRATION_PATH = '/skfs/rest/preregister'
exports.SKFS_REGISTRATION_PATH = '/skfs/rest/register'
exports.SKFS_GET_KEYS_INFO_PATH = '/skfs/rest/getkeysinfo'
exports.SKFS_DEREGISTER_PATH = '/skfs/rest/deregister'
exports.METADATA_VERSION = "1.0"
exports.METADATA_LOCATION = "Cupertino, CA"
//KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
Open postfido2/routes.js in your preferred text editor.
Copy this snippet of code (between the LLLL... lines) and paste between the LLLL lines in routes.js. Here we add include the HTTPS module and the constants file that will be used to call the FIDO2SERVER.
//LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
const https = require('https');
const CONSTANTS = require('./constants');
var useragent = require('express-useragent');
router.use(useragent.express());
//LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
Copy this snippet of code (between the MMMM... lines) and paste between the MMMM lines in routes.js, replacing the existing content in the JavaScript file. Replace the /loginSubmit post listener /registerSubmit with /getChallenge and /submitChallengeResponse listeners. The /getChallenge listener is used to request challenges from the FIDO2SERVER to send to the APPCLIENT for registration and authentication. The /submitChallengeResponse listener is used to send the responses received from the FIDO2 Token, sent by the APPCLIENT, to the FIDO2SERVER.
//MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
router.post("/getChallenge", (req,res) =>{
var intent = req.body.intent;
var username = req.session.username = req.body.username;
if(intent=="authentication"){
if(username == ""){
res.redirect("/login");
return;
}
var db = getDB();
db.get(`select * from users where username = ? `,[username],
(err, row) => {
if (err) {
log("ERROR: "+ err.message);
}
if (row) {
req.session.possibleuserid = row.id;
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const data =JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
username: username,
options: {}
}
});
const options = {
hostname: CONSTANTS.SKFS_HOSTNAME,
port: CONSTANTS.SKFS_PORT,
path: CONSTANTS.SKFS_PREAUTHENTICATE_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const fido2Req = https.request(options, fido2Res => {
log(`statusCode: ${fido2Res.statusCode}`);
fido2Res.on('data', d => {
log("challengeBuffer=");
log(d);
res.json({Response:d.toString()});
})
});
fido2Req.on('error', error => {
log(error);
res.json({Response:"skfs-error"});
});
fido2Req.write(data);
fido2Req.end();
} else {
res.json({Response:"sqlite-error"});
}
});
} else if(intent=="registration"){
var firstname = req.session.firstname = req.body.firstname;
var lastname = req.session.lastname = req.body.lastname;
var displayname = req.session.displayname= req.body.displayname;
if(username == "" | displayname=="" | firstname == "" | lastname == ""){
res.redirect("/register");
return;
}
var db = getDB();
db.get(`select * from users where username = ? `,[username],
(err, row) => {
if (err) {
log("ERROR: "+ err.message);
}
if (!row) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const data = JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
username: username,
displayname: displayname,
options: {"attestation":"direct"},
extensions: "{}"
}
});
const options = {
hostname: CONSTANTS.SKFS_HOSTNAME,
port: CONSTANTS.SKFS_PORT,
path: CONSTANTS.SKFS_PREREGISTRATION_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const fido2Req = https.request(options, fido2Res => {
log(`statusCode: ${fido2Res.statusCode}`);
fido2Res.on('data', d => {
log("challengeBuffer=");
log(d);
res.json({Response:d.toString()});
})
});
fido2Req.on('error', error => {
log(error);
res.json({Response:"skfs-error"});
});
fido2Req.write(data);
fido2Req.end();
} else {
failedRegistration=true;
res.json({Response:"sqlite-error"});
}
});
}
});
router.post("/submitChallengeResponse", (req,res) =>{
var intent = req.body.intent;
var username = req.session.username;
var credResponse = req.body;
var reqOrigin = req.get('host');
let data = "";
let path = "";
if(intent=="authentication"){
var metadataJSON = {
version: CONSTANTS.METADATA_VERSION,
last_used_location: CONSTANTS.METADATA_LOCATION,
username: username,
origin: "https://"+reqOrigin,
clientUserAgent: req.useragent.source
};
var responseJSON = {
id: credResponse.id,
rawId: credResponse.rawId,
response: {
authenticatorData: credResponse.authenticatorData,
signature: credResponse.signature,
userHandle: credResponse.userHandle,
clientDataJSON: credResponse.clientDataJSON
},
type: "public-key"};
data = JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
strongkeyMetadata: metadataJSON,
publicKeyCredential: responseJSON,
}
});
path = CONSTANTS.SKFS_AUTHENTICATE_PATH;
} else if(intent=="registration"){
var firstname = req.session.firstname;
var lastname = req.session.lastname;
var db = getDB();
var metadataJSON = {
version: CONSTANTS.METADATA_VERSION,
create_location: CONSTANTS.METADATA_LOCATION,
username: username,
origin: "https://"+reqOrigin
};
var responseJSON = {
id: credResponse.id,
rawId: credResponse.rawId,
response: {
attestationObject: credResponse.attestationObject,
clientDataJSON: credResponse.clientDataJSON
},
type: "public-key"};
data = JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
strongkeyMetadata: metadataJSON,
publicKeyCredential: responseJSON,
}
});
path = CONSTANTS.SKFS_REGISTRATION_PATH;
}
const options = {
hostname: CONSTANTS.SKFS_HOSTNAME,
port: CONSTANTS.SKFS_PORT,
path: path,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const fido2Req = https.request(options, fido2Res => {
log(`statusCode: ${fido2Res.statusCode}`);
fido2Res.on('data', d => {
if(d.toString().toLowerCase().includes("error")){
res.json({Response:d.toString()});
return;
}
if(intent == "registration"){
db.run('insert into users(username,first_name,last_name) values(?,?,?)',[username,firstname,lastname], function(err) {
if (err) {log("ERROR: "+ err.message);}
log("user added: \nfirst name: "+firstname+"\nlast name: "+lastname+"\nusername: "+username);
req.session.justReg=true;
log(d);
res.json({Response:d.toString()});
});
} else if(intent == "authentication"){
req.session.userid = req.session.possibleuserid;
log(d);
res.json({Response:d.toString()});
}
})
});
fido2Req.on('error', error => {
log(error);
res.json({Response:"error"});
});
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
fido2Req.write(data);
fido2Req.end();
});
//MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
Copy this snippet of code (between the NNNN... lines) and paste between the NNNN lines in routes.js, replacing the existing content in the JavaScript file.
Modify the deleteUser listener by replacing the code below with two post requests to FIDO2SERVER. The first post request is to /skfs/rest/getkeysinfo, which is used to retrieve the user's FIDO2 Token's keyid. The second request is to /skfs/rest/deregister, which deletes the FIDO2 Token registration information. This deletes the user’s FIDO2 Token from the FIDO2SERVER’s database at the same time the user’s info is deleted from the APPSERVER database.
//NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
var username = req.session.username;
req.session.userid=null;
log("logout user " +id +" "+username);
db.run(`delete from users where id = ?`, id, function(err) {
if (err) {
return console.error(err.message);
}
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const data = JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
username: username
}
});
const options = {
hostname: CONSTANTS.SKFS_HOSTNAME,
port: CONSTANTS.SKFS_PORT,
path: CONSTANTS.SKFS_GET_KEYS_INFO_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const fido2Req = https.request(options, fido2Res => {
log(`statusCode: ${fido2Res.statusCode}`);
fido2Res.on('data', d => {
log("keyInfo=");
log(d);
const dataDel = JSON.stringify({
svcinfo: CONSTANTS.SVCINFO,
payload: {
"keyid": JSON.parse(d).Response.keys[0].randomid
}
});
const optionsDel = {
hostname: CONSTANTS.SKFS_HOSTNAME,
port: CONSTANTS.SKFS_PORT,
path: CONSTANTS.SKFS_DEREGISTER_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
};
const fido2ReqDel = https.request(optionsDel, fido2ResDel => {
log(`statusCode: ${fido2ResDel.statusCode}`);
fido2ResDel.on('data', dDel => {
log(dDel);
log("deleted user " +id);
req.session.justUserDeleted = true;
res.redirect("/login");
})
});
fido2ReqDel.on('error', errorDel => {
log(errorDel);
res.json({Response:"skfs-error"});
});
fido2ReqDel.write(dataDel);
fido2ReqDel.end();
})
});
fido2Req.on('error', error => {
log(error);
res.json({Response:"skfs-error"});
});
fido2Req.write(data);
fido2Req.end();
});
//NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN
Deploy the POSTFIDO2 web application. Open a terminal and navigate to the /postfido2 directory. Run the following commands:
CentOS/Ubuntu.
pm2 delete main
pm2 start main.js
sudo pm2 startup systemd
pm2 save
tail -f log
Windows/Mac.
pm2 delete main
pm2 start main.js
pm2 save
Run this command if the website fails to start:
node main.js
Browse to https://fido2tutorial.strongkey.com:3001.