Skip to content

Commit

Permalink
resolves #361
Browse files Browse the repository at this point in the history
  • Loading branch information
Bo Motlagh committed Sep 27, 2023
1 parent b7cda4e commit d873a05
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 23 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ue-auth",
"altName": "UE-Auth",
"version": "1.35.4",
"version": "1.36.0",
"description": "UE Auth is a multi-tenant OIDC Provider, User Management, B2B Product Access, and Roles/Permissions Management system intended to create a single hybrid solution to serve as Identity and Access for both self-registered B2C Apps and Enterprise B2B Solutions",
"private": false,
"license": "SEE LICENSE IN ./LICENSE.md",
Expand Down
68 changes: 57 additions & 11 deletions src/api/accounts/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const cryptoRandomString = require('crypto-random-string');
const config = require('../../config');

export default {
async importAccounts(authGroup, global, array, creator, customDomain) {
async importAccounts(authGroup, global, array, creator) {
let failed = [];
let success = [];
let ok = 0;
Expand Down Expand Up @@ -46,9 +46,53 @@ export default {
// todo - event based bulk notification system
return { warning: 'Auto verify does not work with bulk imports. You will need to send password reset notifications or direct your users to the self-service password reset page.', attempted, ok, failed, success };
},
async writeAccount(data, creator = undefined) {
async passwordPolicy(ag, policy, password) {
const p = policy;
// example of a custom regex if you want to test it out
// custom = '(?=.{10,})(?=.*?[^\\w\\s])(?=.*?[0-9])(?=.*?[A-Z]).*?[a-z].*'
if(p.enabled) {
let policy;
let custom = false;
const standard = (pP) => {
const pVal = `(?=.{${pP.characters},})${(pP.special) ? '(?=.*?[^\\w\\s])' : ''}${(pP.number ? '(?=.*?[0-9])' : '')}${(pP.caps ? '(?=.*?[A-Z])' : '' )}.*?[a-z].*`;
return new RegExp(pVal);
};

try {
if(p.pattern.custom) {
try {
policy = new RegExp(p.pattern.custom);
console.info('this worked');
custom = true;
} catch(e) {
const message = `Custom Password Policy did not compile - ${p.pattern.custom}. Defaulted to standard.`;
if(ag) ueEvents.emit(ag, 'ue.account.error', message);
else console.error();
}
}
if(!policy) {
policy = standard(p.pattern);
}
} catch (error) {
const message = `Unexpected error with password policy validation - ${error.message}`;
if(ag) ueEvents.emit(ag, 'ue.account.error', message);
else console.error(message);
throw Boom.expectationFailed('Password validation is enabled but there was an unexpected error. Contact the admin and try again later.');
}

if(!policy.test(password)) {
const message = (custom) ? 'Password must follow the policy. Contact your administrator' :
`Password must follow the policy: At least ${p.pattern.characters} characters${(p.pattern.caps) ? ', at least one capital' : ''}${(p.pattern.number) ? ', at least one number' : ''}${(p.pattern.special) ? ', at least one special character' : ''}.`;
throw Boom.badRequest(message);
}
}
},
async writeAccount(data, policyPattern, creator = undefined) {
data.email = data.email.toLowerCase();
if(!data.username) data.username = data.email;
if(data.password) {
await this.passwordPolicy(data.authGroup, policyPattern, data.password);
}
const output = await dal.writeAccount(data);
ueEvents.emit(data.authGroup, 'ue.account.create', output);
if(data.profile) {
Expand Down Expand Up @@ -119,6 +163,7 @@ export default {
const patched = jsonPatch.apply_patch(account.toObject(), update);
if(patched.password !== account.password) {
password = true;
await this.passwordPolicy(authGroup.id, authGroup.config.passwordPolicy, patched.password);
}
if(patched.active === false) {
if (authGroup.owner === id) throw Boom.badRequest('You can not deactivate the owner of the auth group');
Expand All @@ -134,13 +179,14 @@ export default {
return (account?.mfa?.enabled === true);
},

async updatePassword(authGroupId, id, password, modifiedBy) {
async updatePassword(authGroup, id, password, modifiedBy) {
const update = {
modifiedBy,
password
};
const output = await dal.updatePassword(authGroupId, id, update);
ueEvents.emit(authGroupId, 'ue.account.edit', output);
await this.passwordPolicy(authGroup.id, authGroup.config.passwordPolicy, password);
const output = await dal.updatePassword(authGroup.id, id, update);
ueEvents.emit(authGroup.id, 'ue.account.edit', output);
return output;
},

Expand Down Expand Up @@ -227,14 +273,14 @@ export default {
apiMethod: 'PATCH',
apiBody: [
{
"op": "replace",
"path": "/password",
"value": 'NEW-PASSWORD-HERE'
'op': 'replace',
'path': '/password',
'value': 'NEW-PASSWORD-HERE'
},
{
"op": "replace",
"path": "/verified",
"value": true
'op': 'replace',
'path': '/verified',
'value': true
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/accounts/accountOidcInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class Account {

}

account = await acct.writeAccount(accData);
account = await acct.writeAccount(accData, {});
} else {
let ident = [];
ident = account.identities.filter((identity) => {
Expand Down
12 changes: 7 additions & 5 deletions src/api/accounts/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ const api = {
},
async writeAccount(req, res, next) {
try {
req.generatePassword = false;
if (req.body.generatePassword === true) {
req.body.password = cryptoRandomString({length: 16, type: 'url-safe'});
req.generatePassword = true;
delete req.body.generatePassword;
}
if (req.groupActivationEvent === true) return api.activateGroupWithAccount(req, res, next);
Expand All @@ -53,7 +55,7 @@ const api = {
}
// force recovery code creation as a second step
if (req.body.recoverCodes) delete req.body.recoverCodes;
const result = await acct.writeAccount(req.body, user);
const result = await acct.writeAccount(req.body, (req.generatePassword) ? {} : req.authGroup.config.passwordPolicy, user);
try {
if (req.globalSettings.notifications.enabled === true &&
req.authGroup.pluginOptions.notification.enabled === true &&
Expand Down Expand Up @@ -154,7 +156,7 @@ const api = {
const user = await acct.getAccountAccessByEmailOrUsername(req.authGroup.id, req.body.email);
let result;
let newUser;
let password = cryptoRandomString({length: 32, type: 'url-safe'});
const password = cryptoRandomString({length: 32, type: 'url-safe'});
if(user) {
const checkForOrg = user.access.filter((ac) => {
return (ac.organization.id === req.organization.id);
Expand Down Expand Up @@ -191,7 +193,7 @@ const api = {
picture: req.body.profile.picture
};
}
newUser = await acct.writeAccount(newData);
newUser = await acct.writeAccount(newData, {});
if(!newUser) throw new Error('Could not create user');
result = await access.defineAccess(req.authGroup, req.organization, newUser._id, {}, req.globalSettings, req.user.sub, 'created', true, req.customDomainUI, req.customDomain);
try {
Expand Down Expand Up @@ -245,7 +247,7 @@ const api = {
let client;
try {
req.body.verified = true;
account = await acct.writeAccount(req.body);
account = await acct.writeAccount(req.body, (req.generatePassword) ? {} : req.authGroup.config.passwordPolicy);
if(!account) throw Boom.expectationFailed('Account not created due to unknown error. Try again later');
try {
client = await cl.generateClient(req.authGroup);
Expand Down Expand Up @@ -993,7 +995,7 @@ async function userOperation(req, user, password) {
throw error;
}
case 'generate_password':
result = await acct.updatePassword(req.authGroup.id, req.params.id, password, (req.user) ? req.user.sub : undefined, req.customDomain);
result = await acct.updatePassword(req.authGroup, req.params.id, password, (req.user) ? req.user.sub : undefined, req.customDomain);
return say.ok(result, RESOURCE);
default:
throw Boom.badRequest('Unknown operation');
Expand Down
12 changes: 11 additions & 1 deletion src/api/authGroup/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ const api = {
if(req.body.setupCode !== config.ONE_TIME_PERSONAL_ROOT_CREATION_KEY) return next(Boom.unauthorized());
if(!config.ROOT_EMAIL) return next(Boom.badData('Root Email Not Configured'));
if(!req.body.password) return next(Boom.badData('Need to provide a password for initial account'));
const policy = {
enabled: config.PASSWORD_POLICY.enabled,
pattern: {
characters: config.PASSWORD_POLICY.characters,
special: config.PASSWORD_POLICY.special,
number: config.PASSWORD_POLICY.number,
caps: config.PASSWORD_POLICY.caps
}
};
await acct.passwordPolicy(undefined, policy, req.body.password);
const check = await group.getOneByEither('root');
if(check) return next(Boom.forbidden('root is established, this action is forbidden'));
// finished security and data checks, proceeding
Expand All @@ -50,7 +60,7 @@ const api = {
};
g = await group.write(gData);
aData.authGroup = g.id;
account = await acct.writeAccount(aData);
account = await acct.writeAccount(aData, {});
client = await cl.generateClient(g);
final = JSON.parse(JSON.stringify(await group.activateNewAuthGroup(g, account, client.client_id)));
if(final.config) {
Expand Down
12 changes: 12 additions & 0 deletions src/api/authGroup/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,18 @@ async function standardPatchValidation(original, patched) {
throw Boom.badRequest('aliasDnsOIDC cannot be edited form this API');
}

if(patched.config?.passwordPolicy?.enabled) {
if(patched.config?.passwordPolicy?.pattern?.custom) {
if(original.config?.passwordPolicy?.pattern?.custom !== patched.config?.passwordPolicy?.pattern?.custom) {
try {
new RegExp(patched.config.passwordPolicy.pattern.custom);
} catch (error) {
throw Boom.badRequest('A custom regular expression password policy was added but did not compile');
}
}
}
}

const groupSchema = Joi.object().keys(definition);
const main = await groupSchema.validateAsync(patched, {
allowUnknown: true
Expand Down
25 changes: 25 additions & 0 deletions src/api/authGroup/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,31 @@ const authGroup = new mongoose.Schema({
config: {
keys: Array,
cookieKeys: Array,
passwordPolicy: {
enabled: {
type: Boolean,
default: config.PASSWORD_POLICY.enabled
},
pattern: {
characters: {
type: Number,
default: config.PASSWORD_POLICY.characters
},
special: {
type: Boolean,
default: config.PASSWORD_POLICY.special
},
number: {
type: Boolean,
default: config.PASSWORD_POLICY.number
},
caps: {
type: Boolean,
default: config.PASSWORD_POLICY.caps
},
custom: String
}
},
requireVerified: {
type: Boolean,
default: false
Expand Down
7 changes: 7 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ const config = {
process.env.SECURITY_FRAME_ANCESTORS.split(',') :
// eslint-disable-next-line quotes
(envVars.SECURITY_FRAME_ANCESTORS) ? envVars.SECURITY_FRAME_ANCESTORS.split(',') : [`'self'`]
},
PASSWORD_POLICY: {
enabled: true,
characters: 6,
special: true,
number: true,
caps: true
}
};

Expand Down
21 changes: 21 additions & 0 deletions swagger.clean.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9063,6 +9063,27 @@ components:
type: array
items:
$ref: '#/components/schemas/federatedOAuth2'
passwordPolicy:
type: object
description: this will be enforced for all password creates and updates in your authgroup
properties:
enabled:
type: boolean
pattern:
type: object
properties:
characters:
type: number
example: 10
special:
type: boolean
number:
type: boolean
caps:
type: boolean
custom:
type: string
description: a regular expression. make sure it will compile
ui:
type: object
properties:
Expand Down
21 changes: 21 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9698,6 +9698,27 @@ components:
type: array
items:
$ref: '#/components/schemas/federatedOAuth2'
passwordPolicy:
type: object
description: this will be enforced for all password creates and updates in your authgroup
properties:
enabled:
type: boolean
pattern:
type: object
properties:
characters:
type: number
example: 10
special:
type: boolean
number:
type: boolean
caps:
type: boolean
custom:
type: string
description: a regular expression. make sure it will compile
ui:
type: object
properties:
Expand Down
Loading

0 comments on commit d873a05

Please sign in to comment.