Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: implement password policies to avoid weak passwords #4008

Merged
merged 2 commits into from
Oct 4, 2021

Conversation

WillardHu
Copy link
Contributor

@WillardHu WillardHu commented Sep 30, 2021

Signed-off-by: WillardHu wei.hu@daocloud.io

What's the purpose of this PR

Closes #3948

Which issue(s) this PR fixes:

  • Add password verification rule in UserInfoController.createOrUpdateUser(..) method;
    • Password needs a number and lowercase letter and between 8~20 characters;
    • Password cannot be a commonly used;
  • Add random password generation button in user-manage.html;

Brief changelog

Implement password policies to avoid weak passwords

Follow this checklist to help us incorporate your contribution quickly and easily:

  • Read the Contributing Guide before making this pull request.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Write necessary unit tests to verify the code.
  • Run mvn clean test to make sure this pull request doesn't break anything.
  • Update the CHANGES log.

@codecov-commenter
Copy link

codecov-commenter commented Sep 30, 2021

Codecov Report

Merging #4008 (c7dc03f) into master (348bee5) will increase coverage by 0.15%.
The diff coverage is 92.59%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #4008      +/-   ##
============================================
+ Coverage     51.31%   51.46%   +0.15%     
- Complexity     2504     2524      +20     
============================================
  Files           482      484       +2     
  Lines         14772    14798      +26     
  Branches       1528     1532       +4     
============================================
+ Hits           7580     7616      +36     
+ Misses         6662     6652      -10     
  Partials        530      530              
Impacted Files Coverage Δ
...o/portal/util/checker/AuthUserPasswordChecker.java 87.50% <87.50%> (ø)
...k/apollo/portal/controller/UserInfoController.java 55.00% <100.00%> (+23.75%) ⬆️
...mework/apollo/portal/util/checker/CheckResult.java 100.00% <100.00%> (ø)
.../apollo/internals/RemoteConfigLongPollService.java 78.31% <0.00%> (+0.60%) ⬆️
...ervice/service/ReleaseMessageServiceWithCache.java 87.05% <0.00%> (+1.17%) ⬆️
...work/apollo/biz/message/ReleaseMessageScanner.java 85.52% <0.00%> (+2.63%) ⬆️
...trip/framework/apollo/portal/entity/po/UserPO.java 29.16% <0.00%> (+25.00%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 348bee5...c7dc03f. Read the comment docs.

CHANGES.md Outdated Show resolved Hide resolved
apollo-portal/src/main/resources/static/i18n/zh-CN.json Outdated Show resolved Hide resolved
user.setUsername("username");
user.setPassword("9sivg8hg3wjz8");
userInfoController.createOrUpdateUser(user);
Assert.assertTrue(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pointless

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, this test case has been removed. Thanks

Copy link
Member

@kezhenxu94 kezhenxu94 Sep 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @WillardHu , I don't mean to remove the case, I mean the assertTrue(true) is pointless, removing this line should be enough

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

}

/**
* @return The passwrod contains code fragment or is blank.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return The passwrod contains code fragment or is blank.
* @return The password contains code fragment or is blank.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks.

Comment on lines 38 to 40
public static boolean notMatchRegex(String password) {
return !PWD_PATTERN.matcher(password).matches();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static boolean notMatchRegex(String password) {
return !PWD_PATTERN.matcher(password).matches();
}
public static boolean isWeakPassword(String password) {
return !PWD_PATTERN.matcher(password).matches() || isCommonlyUsed(password);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

var alpha_chars =[ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];

function randomAlphanumeric(len) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether we should provide this functionality, because most modern browsers (like Safari) or password manager (like 1password) has this ability to generate strong random password, this function defined here is rather naive IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you if it's self-registration, but in this scenario, it is the administrator manage the account. This is just a convenience for administrators to generate default passwords for members if he wanted. And the browser's password generation extension may only work for the password component. If you don't want it, I'll remove it. Thanks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WillardHu Let's wait for other reviewers' opinions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove that functionality since strong password generation can be done by the browser.
Additionally a custom password generator would be harder to maintain (for example if the password policy changes etc) with little to none benefit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also suggest we don't provide this functionality as it is not a core function of apollo and there are many plugins or browsers doing this job much better. What we need to do though is to make sure the password form is compatible with those plugins or browsers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the frontend modifications have been rollback.

Copy link
Contributor

@DiegoKrupitza DiegoKrupitza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest looks good to me.

if (UserPasswordChecker.isWeakPassword(user.getPassword())) {
throw new BadRequestException(
"Password needs a number and lowercase letter and between 8~20 characters. "
+ "And cannot be a weak password.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should adapt the message. Makes it more useable and clear to the user what the real reason is.
It can be not matching to the pattern, weak password or both.
If it is just one and you display both reasons in the error message the user might not understand it why you get both.

PS: you should also explain what a weak password is in the exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've split the error message, and wrap it with CheckResult.


public class UserPasswordChecker {

private static final Pattern PWD_PATTERN = Pattern
Copy link
Contributor

@DiegoKrupitza DiegoKrupitza Sep 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice if you could write few test cases that check if this pattern really works as intended. Regex is often a reason for bugs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

"1q2w", "2w3e", "3e4r", "5t6y", "abcd", "qwer", "asdf", "zxcv"
);

public static boolean isWeakPassword(String password) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest we use instance method instead of static method so that we could mock this method easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I used Spring IOC to register the bean instance of UserPasswordChecker interface’s implementation class AuthUserPasswordChecker.

/**
* @return The password contains code fragment or is blank.
*/
private static boolean isCommonlyUsed(String password) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest we use instance method instead of static method so that we could mock this method easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

try {
userInfoController.createOrUpdateUser(user);
} catch (BadRequestException e) {
Assert.assertEquals(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better we mock password checker here instead of testing the password logic here, as it is not core logic of UserInfoController.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

import org.junit.Assert;
import org.junit.Test;

public class CommonlyUsedPwdCheckerTest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be renamed as something like UserPasswordCheckerTest so that we will test all kinds of weak password scenarios.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

Comment on lines 66 to 70
CheckResult pwdCheckRes;
if (!(pwdCheckRes = passwordChecker.checkWeakPassword(user.getPassword())).isSuccess()) {
throw new BadRequestException(pwdCheckRes.getMessage());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you assign pwdCheckRes inside the if and not before when you declare the var pwdCheckRes?

Otherwise to make the code more readable I would suggest to change it to

CheckResult pwdCheckRes = passwordChecker.checkWeakPassword(user.getPassword());
if (!pwdCheckRes.isSuccess()) {
  throw new BadRequestException(pwdCheckRes.getMessage());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

Signed-off-by: WillardHu <wei.hu@daocloud.io>
@DiegoKrupitza
Copy link
Contributor

Good work! 👍🏽:shipit:

Copy link
Member

@nobodyiam nobodyiam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kezhenxu94 kezhenxu94 merged commit 4a28fbd into apolloconfig:master Oct 4, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Oct 4, 2021
@nobodyiam nobodyiam added this to the 1.10.0 milestone Oct 6, 2021
@WillardHu WillardHu deleted the feat/issue-3948 branch October 8, 2021 23:49
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement password policies to avoid weak passwords
5 participants