passwordless-bb
Version:
Helper package for bringing FIDO2 authentication to Client Application
531 lines (476 loc) • 14.2 kB
JavaScript
const App = (() => {
let clientId;
let baseUrl;
/**
*
* @param {String} url
* @param {String} id
*/
const init = (url, id) => {
baseUrl = url;
clientId = id;
};
/**
*
* @returns {Object}
*/
const getApplicationNameAndLogo = async () => {
console.log("getting application logo");
try {
const response = await fetch(
baseUrl + "/app/applicationDetails/" + clientId
);
const responseJson = await response.json();
if (responseJson.errorCode === -1)
throw new Error(responseJson.errorMessage);
return responseJson;
} catch (error) {
throw new Error(error.message);
}
};
const checkRemoteAuthentication = async () => {
try {
const resp = await fetch(baseUrl + `/app/viewApp/${clientId}`);
const data = await resp.json();
if (data.errorCode === -1) throw new Error(data.errorMessage);
if (data.suspended) throw new Error("App is suspended");
return data;
} catch (error) {
throw new Error(error);
}
};
/**
*
* @param {string} transactionId
* @returns {Object}
*/
const getTransactionStatusOnChange = async (transactionId) => {
try {
const poll = async function (fn, ms) {
let response = await fn();
let responseJSON = await response.json();
while (responseJSON.status === "PENDING") {
await wait(ms);
response = await fn();
responseJSON = await response.json();
}
return responseJSON;
};
const wait = function (ms = 2000) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
const transaction = () => {
return fetch(baseUrl + `/transaction/getTransaction/${transactionId}`);
};
let response = await poll(transaction, 2000);
return response;
} catch (error) {
throw error;
}
};
/**
*
* @param {Object} data
* @returns {Object}
*/
const sendPushNotification = async (data) => {
try {
const remote = await checkRemoteAuthentication();
if (!remote.remotePlatformAuth)
throw new Error("Remote platform authentication is not enabled");
const transactionResponse = await fetch(
baseUrl + "/transaction/createTransaction",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: data.username, appId: clientId }),
}
);
const transactionResponseJSON = await transactionResponse.json();
const { transactionId } = transactionResponseJSON;
data.id = transactionId;
const response = await fetch(baseUrl + "/sendPush/client", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: data.username,
clientId: clientId,
data: { ...data, clientID: clientId },
type: "login",
}),
});
const responseJSON = await response.json();
responseJSON.transactionId = transactionId;
return responseJSON;
} catch (error) {
throw new Error(error);
}
};
/**
*
* @param {String} transactionId
* @param {Object} data
* @returns {Object}
*/
const declineTransaction = async (transactionId) => {
try {
const response = await fetch(
baseUrl + `/transaction/updateTransactionStatus/${transactionId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
transactionStatus: "FAILED",
message: "User Declined Authentication !",
}),
}
);
const responseJSON = await response.json();
return responseJSON;
} catch (error) {
throw error;
}
};
/**
*
* @param {String} username
* @returns
*/
const getAllAudits = async (username) => {
try {
const audit = await fetch(
`${baseUrl}/getAllAudits/${username}/${clientId}`
);
const AuditDataJson = await audit.json();
if (AuditDataJson.errorCode === -1)
throw new Error(AuditDataJson.errorMessage);
else return AuditDataJson;
} catch (error) {
throw new Error(error.message);
}
};
/**
*
* @param {Object} userDetails
* @returns {Object}
*/
const generateQR = async (userDetails) => {
try {
const remote = await checkRemoteAuthentication();
if (!remote.remotePlatformAuth)
throw new Error("Remote platform authentication is not enabled");
const transactionResponse = await fetch(
baseUrl + "/transaction/createTransaction",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: userDetails.username,
appId: clientId,
}),
}
);
const transactionResponseJSON = await transactionResponse.json();
const { transactionId } = transactionResponseJSON;
userDetails.id = transactionId;
const data = await fetch(baseUrl + "/generateQrCode", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ...userDetails, clientID: clientId }),
});
const dataJson = await data.json();
dataJson.transactionId = transactionId;
return dataJson;
} catch (error) {
throw new Error(error);
}
};
/**
*
* @param {username:"","id":""} data
* @returns {Object}
*/
const register = async ({ username, id }) => {
if (!baseUrl || !clientId) {
throw new Error("BaseURL and ClientID is not added");
}
try {
const resp = await fetch(baseUrl + "/registerUser", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
clientId: clientId,
}),
});
const data = await resp.json();
console.log("data", data);
if (data.errorCode === -1) throw new Error(data.errorMessage);
let attResp;
try {
attResp = await startAttestation(data);
// console.log(attResp);
} catch (error) {
throw new Error(error);
}
const responseData = {
credential: attResp,
username,
challenge: data.challenge,
clientId: clientId,
transactionId: id,
};
console.log(responseData);
const verificationResp = await fetch(
baseUrl + "/verify-registerUser-attestation",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(responseData),
}
);
const verificationJSON = await verificationResp.json();
if (verificationJSON.errorCode === -1)
throw new Error(verificationJSON.errorMessage);
if (verificationJSON && verificationJSON.verified) {
return verificationJSON;
} else {
let error = JSON.stringify(verificationJSON, null, 4);
throw new Error(error);
}
} catch (error) {
throw new Error(error);
}
};
/**
*
* @param {Object} data
* @returns {Object}
*/
const login = async ({ username, id }) => {
// console.log(username);
if (!baseUrl || !clientId) {
throw new Error("BaseURL and ClientID is not added");
}
const resp = await fetch(baseUrl + "/LoginUser", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
clientId: clientId,
}),
});
const opts = await resp.json();
if (opts.errorCode === -1) {
throw new Error(opts.errorMessage);
}
let asseResp;
try {
asseResp = await startAssertion(opts);
// console.log("asseRep", asseResp);
} catch (error) {
if (error.name === "TypeError")
throw new Error("UserName is not Registered for FIDO");
else throw new Error(error.message);
}
const responseData = {
...asseResp,
username,
challenge: opts.challenge,
clientId: clientId,
transactionId: id,
};
const verificationResp = await fetch(
baseUrl + "/verify-loginUser-assertion",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(responseData),
}
);
const verificationJSON = await verificationResp.json();
if (verificationJSON && verificationJSON.verified) {
return verificationJSON;
} else {
const error = JSON.stringify(verificationJSON, null, 4);
// console.log("new error: ", error);
throw new Error(error);
}
};
const addDevice = async ({ username, id }) => {
if (!baseUrl || !clientId) {
throw new Error("BaseURL and ClientID is not added");
}
try {
const resp = await fetch(baseUrl + "/device/addDevice", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
clientId: clientId,
}),
});
const data = await resp.json();
if (data.errorCode === -1) throw new Error(data.errorMessage);
let attResp;
try {
attResp = await startAttestation(data);
// console.log(attResp);
} catch (error) {
throw new Error(error);
}
const responseData = {
credential: attResp,
username,
challenge: data.challenge,
clientId: clientId,
transactionId: id,
};
// console.log(responseData);
const verificationResp = await fetch(
baseUrl + "/verify-registerUser-attestation",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(responseData),
}
);
const verificationJSON = await verificationResp.json();
if (verificationJSON.errorCode === -1)
throw new Error(verificationJSON.errorMessage);
if (verificationJSON && verificationJSON.verified) {
return verificationJSON;
} else {
let error = JSON.stringify(verificationJSON, null, 4);
throw new Error(error);
}
} catch (error) {
throw new Error(error);
}
};
function n(e) {
const t = e.replace(/-/g, "+").replace(/_/g, "/"),
n = (4 - (t.length % 4)) % 4,
r = t.padEnd(t.length + n, "="),
o = window.atob(r),
i = new ArrayBuffer(o.length),
a = new Uint8Array(i);
for (let e = 0; e < o.length; e++) a[e] = o.charCodeAt(e);
return i;
}
function t(e) {
const t = new Uint8Array(e);
let n = "";
for (const e of t) n += String.fromCharCode(e);
return btoa(n).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function o(e) {
const { id: t } = e;
return { ...e, id: n(t) };
}
function r() {
return (
void 0 !==
(null === window || void 0 === window
? void 0
: window.PublicKeyCredential) &&
"function" == typeof window.PublicKeyCredential
);
}
const startAssertion = async (e) => {
var i, a;
if (!r()) throw new Error("WebAuthn is not supported in this browser");
let s;
0 !==
(null === (i = e.allowCredentials) || void 0 === i ? void 0 : i.length) &&
(s =
null === (a = e.allowCredentials) || void 0 === a ? void 0 : a.map(o));
const l = { ...e, challenge: n(e.challenge), allowCredentials: s },
d = await navigator.credentials.get({ publicKey: l });
if (!d) throw new Error("Assertion was not completed");
const { id: c, rawId: u, response: p, type: f } = d;
let w;
var g;
return (
p.userHandle &&
((g = p.userHandle), (w = new TextDecoder("utf-8").decode(g))),
{
id: c,
rawId: t(u),
response: {
authenticatorData: t(p.authenticatorData),
clientDataJSON: t(p.clientDataJSON),
signature: t(p.signature),
userHandle: w,
},
type: f,
clientExtensionResults: d.getClientExtensionResults(),
}
);
};
const startAttestation = async (e) => {
if (!r()) throw new Error("WebAuthn is not supported in this browser");
const publicKey = {
...e,
challenge: n(e.challenge),
user: { ...e.user, id: ((a = e.user.id), new TextEncoder().encode(a)) },
excludeCredentials: e.excludeCredentials.map(o),
};
var a;
const s = await navigator.credentials.create({ publicKey: publicKey });
if (!s) throw new Error("Attestation was not completed");
const { id: l, rawId: d, response: c, type: u } = s,
p = {
id: l,
rawId: t(d),
response: {
attestationObject: t(c.attestationObject),
clientDataJSON: t(c.clientDataJSON),
},
type: u,
clientExtensionResults: s.getClientExtensionResults(),
};
return p;
};
return {
init,
getApplicationNameAndLogo,
getTransactionStatusOnChange,
sendPushNotification,
declineTransaction,
getAllAudits,
generateQR,
login,
register,
addDevice,
};
})();
if (typeof exports != "undefined") {
exports.Passwordless = App;
} else {
var Passwordless = App;
}