Skip to content

Commit

Permalink
Add Passwordless
Browse files Browse the repository at this point in the history
  • Loading branch information
EC2 Default User committed May 20, 2020
1 parent ff63510 commit 90d2b61
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 21 deletions.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,24 @@ For situations where you are not logging in (like in the "Authenticating in a Me
<h2 id="client-api">Client</h2>
`import MFA from 'meteor/ndev:mfa';`

#### MFA.login(username, password)<promise>
#### MFA.login(email/username, password)<promise>
Resolves when logged in, catches on error. This function is a wrapper for the `MFA.loginWithMFA` function. It attempts to login the user. If it receives an `mfa-required` error, it uses `MFA.loginWithMFA`. If you prefer to customize this, you can use the `MFA.loginWithMFA` function

#### MFA.loginWithMFA(username, password)<promise>
#### MFA.loginWithMFA(email/username, password)<promise>
Requests a login challenge, solves it, then logs in. This function will fail if the user doesn't have MFA enabled.

#### MFA.loginWithPasswordless(email/username)<promise:(passwordNeeded)>
Attempts a passwordless login. Resolves with a single boolean `passwordNeeded`. If true, the user doesn't have passwordless turned on, so you must use the regular login flow. If false, the user is now logged in.

#### MFA.finishLogin(finishLoginParams)<promise>
Completes a login

#### MFA.registerU2F()<promise>
Registers the user's U2F device and enables MFA
#### MFA.registerU2F(params)<promise>
Registers the user's U2F device and enables MFA. To just enable MFA, call without any arguments. To enable MFA and passwordless, call with the following params:

````
{passwordless:true, password:"..."}
````

#### MFA.registerTOTP()<promise>
Generates a TOTP secret and a registrationId. Resolves with `{secret, registrationId}`.
Expand Down Expand Up @@ -312,7 +319,10 @@ Returns a boolean of whether the device supports u2f login
See the config options section below

#### MFA.disableMFA(userId)
Disables MFA for a user. This is an internal method. If you'd like the user to authenticate before you disable, see [Authenticating in a Method](#method-authentication).
Disables MFA for a user. This is an internal method. If you'd like the user to authenticate before you disable, see [Authenticating in a Method](#method-authentication). Note: if the user has passwordless enabled, this will also disable passwordless.

#### MFA.disablePasswordless(userId)
Disables passwordless for a user. When this method is called, MFA will remain enabled. To disable passwordless and MFA, call `MFA.disableMFA`.

#### MFA.generateChallenge(userId, type)
Generates a challenge. This is then sent to the client and passed into `MFA.solveChallenge()`
Expand Down
47 changes: 45 additions & 2 deletions mfa-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ let solveU2FChallenge = async function (c) {
return {challengeId, challengeSecret, credentials};
};

let registerMFA = () => new Promise((resolve, reject) => {
Meteor.call(registrationChallengeHandlerU2F(), async (err, res) => {

let registerMFA = (params) => new Promise((resolve, reject) => {
if(!params) {
params = {passwordless:false};
}
else {
if(typeof(params.password) === "string") {
params.password = Accounts._hashPassword(params.password);
}
}

Meteor.call(registrationChallengeHandlerU2F(), params, async (err, res) => {
if(err) {
return reject(err);
}
Expand Down Expand Up @@ -187,6 +197,37 @@ let finishLogin = (finishLoginParams, code) => new Promise(async (resolve, rejec
});
});

let loginWithPasswordless = (query) => new Promise((resolve, reject) => {
Meteor.call(loginChallengeHandler(), query, {passwordless:true}, async (err, res) => {
if(err) {
if(err.code === "not-passwordless") {
resolve(true);
}
else {
reject(err);
}
}
else {
let methodName = loginCompletionHandler();
let methodArguments = await assembleChallengeCompletionArguments(res.finishLoginParams);

Accounts.callLoginMethod({
methodName,
methodArguments,
userCallback:(err) => {
if(err) {
reject(err);
}
else {
resolve();
}
}
});
}
});
});


let loginWithMFA = (username, password) => new Promise((resolve, reject) => {
Meteor.call(loginChallengeHandler(), username, Accounts._hashPassword(password), async (err, res) => {
if(err) {
Expand Down Expand Up @@ -258,6 +299,8 @@ let authorizeAction = (type) => new Promise((resolve, reject) => {
});

export default {
loginWithPasswordless,

authorizeAction,
useU2FAuthorizationCode,
supportsU2FLogin,
Expand Down
61 changes: 48 additions & 13 deletions mfa-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ let _defaults = {
requireResetPasswordMFA:true,
allowU2FAuthorization:true,
authorizationDisabledMethods:[],
keepChallenges:false
keepChallenges:false,
passwordless:false
};

let config = Object.assign({}, _defaults);
Expand Down Expand Up @@ -157,7 +158,12 @@ let verifyAssertion = function (type, {challengeId, credentials}) {

let disableMFA = function (userId) {
check(userId, String);
Meteor.users.update({_id:userId}, {$unset:{"services.mfapublickey":true, "services.mfamethod":true}, $set:{"services.mfaenabled":false, [config.mfaDetailsField + ".enabled"]:false, [config.mfaDetailsField + ".type"]:null}});
Meteor.users.update({_id:userId}, {$unset:{"services.passwordlessenabled":true, "services.mfapublickey":true, "services.mfamethod":true}, $set:{"services.mfaenabled":false, [config.mfaDetailsField + ".enabled"]:false, [config.mfaDetailsField + ".type"]:null}});
};

let disablePasswordless = function (userId) {
check(userId, String);
Meteor.users.update({_id:userId}, {$unset:{"services.passwordlessenabled":true}, $set:{[config.mfaDetailsField + ".passwordless"]:false}});
};

let enableOTP = function (userId) {
Expand Down Expand Up @@ -319,9 +325,17 @@ Meteor.methods({
return 200;
},

[registrationChallengeHandlerU2F()]: async function () {
[registrationChallengeHandlerU2F()]: async function (params) {
if(!config.enableU2F) return;

try {
check(params, {passwordless:true, password:{digest:String, algorithm:"sha-256"}});
check(config.passwordless, true);
}
catch(e) {
check(params, {passwordless:false});
}

if(!this.userId) {
throw new Meteor.Error(403);
}
Expand All @@ -338,10 +352,19 @@ Meteor.methods({
user:config.getUserDetails(this.userId)
});

if(params.passwordless) {
let user = Meteor.users.findOne({_id:this.userId});
let checkPassword = Accounts._checkPassword(user, params.password);
if (checkPassword.error) {
throw new Meteor.Error(403, strings.incorrectPasswordError);
}
}

MFARegistrations.insert({
challenge:challengeResponse.challenge,
userId:this.userId,
method:"u2f"
method:"u2f",
passwordless:params.passwordless
});

return challengeResponse;
Expand All @@ -361,17 +384,18 @@ Meteor.methods({
throw new Meteor.Error(404);
}

let user = Meteor.users.findOne({_id:this.userId}, {fields:{"services.mfaenabled":1}});
let user = Meteor.users.findOne({_id:this.userId}, {fields:{"services.mfaenabled":1, "services.passwordlessenabled":1}});

if(user.services.mfaenabled === true) {
if(user.services.mfaenabled === true && (!registration.passwordless || user.services.passwordlessenabled)) {
throw new Meteor.Error(400, strings.mfaAlreadyEnabledError);
}

Meteor.users.update({_id:this.userId}, {$set:{
[config.mfaDetailsField]:({enabled:true, type:"u2f"}),
[config.mfaDetailsField]:({enabled:true, type:"u2f", passwordless:registration.passwordless}),
"services.mfapublickey":key,
"services.mfaenabled":true,
"services.mfamethod":"u2f"
"services.mfamethod":"u2f",
"services.passwordlessenabled":registration.passwordless
}});

MFARegistrations.remove({_id:registration._id});
Expand Down Expand Up @@ -425,7 +449,7 @@ Meteor.methods({
}

check(username, userQueryValidator);
check(password, Object);
check(password, Match.OneOf({passwordless:true}, {digest:String, algorithm:"sha-256"}));

let user = Accounts._findUserByQuery(username);

Expand All @@ -437,9 +461,20 @@ Meteor.methods({
throw new Meteor.Error(400);
}

let checkPassword = Accounts._checkPassword(user, password);
if (checkPassword.error) {
throw new Meteor.Error(403, strings.incorrectPasswordError);
if(password.passwordless) {
try {
check(user.services.passwordlessenabled, true);
check(config.passwordless, true);
}
catch(e) {
throw new Meteor.Error("not-passwordles", "Not Passwordless");
}
}
else {
let checkPassword = Accounts._checkPassword(user, password);
if (checkPassword.error) {
throw new Meteor.Error(403, strings.incorrectPasswordError);
}
}

let challengeConnectionHash = createConnectionHash(this.connection);
Expand Down Expand Up @@ -569,4 +604,4 @@ let getCurrentTOTP = function (secret) {
return authenticator.generate(secret);
};

export default { verifyChallenge, getCurrentTOTP, enableOTP, setConfig, setStrings, disableMFA, generateChallenge, verifyAssertion, verifyAttestation:verifyAssertion };
export default { disablePasswordless, verifyChallenge, getCurrentTOTP, enableOTP, setConfig, setStrings, disableMFA, generateChallenge, verifyAssertion, verifyAttestation:verifyAssertion };
2 changes: 1 addition & 1 deletion package.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package.describe({
name: 'ndev:mfa',
version: '0.0.8',
version: '0.0.10',
summary: 'Multi Factor Authentication for Meteor (supporting U2F, TOTP, and OTP)',
git: 'https://github.com/TheRealNate/meteor-mfa',
documentation: 'README.md'
Expand Down

0 comments on commit 90d2b61

Please sign in to comment.