Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 66 additions & 11 deletions api/src/org/labkey/api/security/AuthenticationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,32 @@ public static boolean isAutoCreateAccountsEnabled()

public static boolean isSelfServiceEmailChangesEnabled() { return getAuthSetting(SELF_SERVICE_EMAIL_CHANGES_KEY, false);}

public static boolean isLoginAttemptControlEnabled()
{
return getAuthSetting(LOGIN_ATTEMPT_ENABLED_KEY, false);
}

public static int getLoginAttemptLimit()
{
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
String value = props.get(LOGIN_ATTEMPT_LIMIT_KEY);
return value == null ? 3 : Integer.parseInt(value);
}

public static int getLoginAttemptPeriod()
{
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
String value = props.get(LOGIN_ATTEMPT_PERIOD_KEY);
return value == null ? 30 : Integer.parseInt(value);
}

public static int getLoginAttemptResetTime()
{
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
String value = props.get(LOGIN_ATTEMPT_RESET_TIME_KEY);
return value == null ? 5 : Integer.parseInt(value);
}

public static @NotNull String getDefaultDomain()
{
Map<String, String> props = PropertyManager.getProperties(AUTHENTICATION_CATEGORY);
Expand Down Expand Up @@ -308,6 +334,22 @@ public static void saveAuthSettings(User user, Map<String, Boolean> map)
.forEach(e->saveAuthSetting(user, e.getKey(), e.getValue()));
}

public static void addLoginAttemptSettingsListener(Runnable listener)
{
_loginAttemptSettingsListeners.add(listener);
}

public static void saveLoginAttemptSettings(User user, boolean enabled, int limit, int period, int resetTime)
{
WritablePropertyMap props = PropertyManager.getWritableProperties(AUTHENTICATION_CATEGORY, true);
props.put(LOGIN_ATTEMPT_ENABLED_KEY, String.valueOf(enabled));
props.put(LOGIN_ATTEMPT_LIMIT_KEY, String.valueOf(limit));
props.put(LOGIN_ATTEMPT_PERIOD_KEY, String.valueOf(period));
props.put(LOGIN_ATTEMPT_RESET_TIME_KEY, String.valueOf(resetTime));
props.save();
_loginAttemptSettingsListeners.forEach(Runnable::run);
}

public static void reorderConfigurations(User user, String name, int[] rowIds)
{
if (null != rowIds && rowIds.length != 0)
Expand Down Expand Up @@ -635,15 +677,22 @@ public static void setAcceptOnlyFicamProviders(User user, boolean enable)
AuthenticationConfigurationCache.clear();
}

// Used by start-up properties
private static final String AUTHENTICATION_CATEGORY = "Authentication";
// Used by start-up properties and upgrade code
public static final String AUTHENTICATION_CATEGORY = "Authentication";

public static final String SELF_REGISTRATION_KEY = "SelfRegistration";
public static final String AUTO_CREATE_ACCOUNTS_KEY = "AutoCreateAccounts";
public static final String DEFAULT_DOMAIN = "DefaultDomain";
public static final String SELF_SERVICE_EMAIL_CHANGES_KEY = "SelfServiceEmailChanges";
public static final String ACCEPT_ONLY_FICAM_PROVIDERS_KEY = "AcceptOnlyFicamProviders";

public static final String LOGIN_ATTEMPT_ENABLED_KEY = "LoginAttemptEnabled";
public static final String LOGIN_ATTEMPT_LIMIT_KEY = "LoginAttemptLimit";
public static final String LOGIN_ATTEMPT_PERIOD_KEY = "LoginAttemptPeriod";
public static final String LOGIN_ATTEMPT_RESET_TIME_KEY = "LoginAttemptResetTime";

private static final List<Runnable> _loginAttemptSettingsListeners = new CopyOnWriteArrayList<>();

public enum AuthenticationSettings implements StartupProperty
{
SelfRegistration("Allow self sign up"),
Expand Down Expand Up @@ -1165,17 +1214,23 @@ public static PrimaryAuthenticationResult finalizePrimaryAuthentication(HttpServ

// limit one bad login per second averaged out over 60sec
private static final Cache<Integer, RateLimiter> addrLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "Login limiter");
private static final Cache<Integer, RateLimiter> userLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "User limiter");
private static final Cache<Integer, RateLimiter> pwdLimiter = CacheManager.getCache(1001, TimeUnit.MINUTES.toMillis(5), "Password limiter");
private static final CacheLoader<Integer, RateLimiter> addrLoader = (key, request) -> new RateLimiter("Addr limiter: " + key, new Rate(60, TimeUnit.MINUTES));
private static final CacheLoader<Integer, RateLimiter> pwdLoader = (key, request) -> new RateLimiter("Pwd limiter: " + key, new Rate(20, TimeUnit.MINUTES));
private static final CacheLoader<Integer, RateLimiter> userLoader = (key, request) -> new RateLimiter("User limiter: " + key, new Rate(20, TimeUnit.MINUTES));

private static Integer _toKey(String s)
private static final Cache<String, RateLimiter> userLimiter = CacheManager.getCache(10000, TimeUnit.MINUTES.toMillis(5), "User limiter");
private static final CacheLoader<String, RateLimiter> userLoader = (key, request) -> new RateLimiter("User limiter: " + key, new Rate(20, TimeUnit.MINUTES));

private static Integer getIntCacheKey(String s)
{
return null==s ? 0 : s.toLowerCase().hashCode() % 1000;
}

public static String getEmailCacheKey(String s)
{
return StringUtils.trimToEmpty(s).toLowerCase();
}

private static PrimaryAuthenticationResult _beforeAuthenticate(HttpServletRequest request, String id, String pwd)
{
if (null == id || null == pwd)
Expand All @@ -1184,10 +1239,10 @@ private static PrimaryAuthenticationResult _beforeAuthenticate(HttpServletReques
long delay = 0;

// slow down login attempts when we detect more than 20/minute bad attempts per user, password, or ip address
rl = addrLimiter.get(_toKey(request == null ? null : request.getRemoteAddr()));
rl = addrLimiter.get(getIntCacheKey(request == null ? null : request.getRemoteAddr()));
if (null != rl)
delay = Math.max(delay,rl.add(0, false));
rl = pwdLimiter.get(_toKey(pwd));
rl = pwdLimiter.get(getIntCacheKey(pwd));
if (null != rl)
delay = Math.max(delay, rl.add(0, false));

Expand Down Expand Up @@ -1217,7 +1272,7 @@ private static long getUserLoginDelay(String id) throws LoginDisabledException

private static long getDefaultUserLoginDelay(String id)
{
RateLimiter rl = userLimiter.get(_toKey(id));
RateLimiter rl = userLimiter.get(getEmailCacheKey(id));
if (null != rl)
return rl.add(0, false);
return 0;
Expand All @@ -1230,9 +1285,9 @@ private static void _afterAuthenticate(HttpServletRequest request, String id, St
if (result.getStatus() == AuthenticationStatus.BadCredentials || result.getStatus() == AuthenticationStatus.InactiveUser)
{
RateLimiter rl;
rl = addrLimiter.get(_toKey(request.getRemoteAddr()),request, addrLoader);
rl = addrLimiter.get(getIntCacheKey(request.getRemoteAddr()),request, addrLoader);
rl.add(1, false);
rl = pwdLimiter.get(_toKey(pwd),request, pwdLoader);
rl = pwdLimiter.get(getIntCacheKey(pwd),request, pwdLoader);
rl.add(1, false);

addUserLoginDelay(request, id);
Expand Down Expand Up @@ -1261,7 +1316,7 @@ private static void addUserLoginDelay(HttpServletRequest request, String id)

private static void addDefaultUserLoginDelay(HttpServletRequest request, String id)
{
RateLimiter rl = userLimiter.get(_toKey(id),request, userLoader);
RateLimiter rl = userLimiter.get(getEmailCacheKey(id),request, userLoader);
rl.add(1, false);
}

Expand Down
2 changes: 1 addition & 1 deletion core/module.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Name: Core
ModuleClass: org.labkey.core.CoreModule
SchemaVersion: 26.004
SchemaVersion: 26.005
Label: Administration and Essential Services
Description: The Core module provides central services such as login, \
security, administration, folder management, user management, \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Migrate login attempt settings from the compliance module's property store to core authentication settings.
SELECT core.executeJavaUpgradeCode('migrateLoginAttemptSettings');
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Migrate login attempt settings from the compliance module's property store to core authentication settings.
EXEC core.executeJavaUpgradeCode 'migrateLoginAttemptSettings';
96 changes: 84 additions & 12 deletions core/src/client/components/GlobalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ interface GlobalSettingFieldData {
tip: string;
}

const LOGIN_ATTEMPT_LIMIT_OPTIONS = ['3', '5', '10', '100'];
const LOGIN_ATTEMPT_PERIOD_OPTIONS = ['5', '15', '30', '60'];
const LOGIN_ATTEMPT_RESET_TIME_OPTIONS = ['5', '10', '30', '60'];

const FIELD_DATA: GlobalSettingFieldData[] = [
{
id: 'SelfRegistration',
Expand Down Expand Up @@ -76,25 +80,32 @@ export const GlobalSettings: FC<Props> = memo(({ canEdit, authCount, onChange, g
[onChange]
);

const onLoginAttemptEnabledChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
onChange('LoginAttemptEnabled', event.target.checked);
},
[onChange]
);

const onLoginAttemptSelectChange = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
onChange(event.target.name, event.target.value);
},
[onChange]
);

const loginAttemptEnabled = !!globalSettings?.LoginAttemptEnabled;
const loginAttemptLimit = globalSettings?.LoginAttemptLimit ?? '3';
const loginAttemptPeriod = globalSettings?.LoginAttemptPeriod ?? '30';
const loginAttemptResetTime = globalSettings?.LoginAttemptResetTime ?? '5';

return (
<div className="panel panel-default">
<div className="panel-heading">
Global Settings
</div>

<div className="panel-body">
{fieldData.map(data => (
<GlobalSetting
key={data.id}
canEdit={canEdit}
id={data.id}
onChange={onChange}
value={globalSettings[data.id]}
text={data.text}
tip={data.tip}
/>
))}

<div className="global-settings__default-domain">
<span>System Default Domain</span>

Expand All @@ -117,6 +128,67 @@ export const GlobalSettings: FC<Props> = memo(({ canEdit, authCount, onChange, g
/>
</span>
</div>

<hr/>

{fieldData.map(data => (
<GlobalSetting
key={data.id}
canEdit={canEdit}
id={data.id}
onChange={onChange}
value={globalSettings[data.id]}
text={data.text}
tip={data.tip}
/>
))}

<div className="global-settings__text-row">
<label>
<input
checked={loginAttemptEnabled}
disabled={!canEdit}
onChange={onLoginAttemptEnabledChange}
type="checkbox"
/>
Limit unsuccessful login attempts
<LabelHelpTip title="Tip">
<div>
This does not apply to site and application administrators. <HelpLink topic="complianceSettings#Login">More info</HelpLink>
</div>
</LabelHelpTip>
</label>
<div style={{marginLeft: '17px', marginTop: '5px'}}>
<span>Disable user login if </span>
<select
disabled={!canEdit || !loginAttemptEnabled}
name="LoginAttemptLimit"
onChange={onLoginAttemptSelectChange}
value={loginAttemptLimit}
>
{LOGIN_ATTEMPT_LIMIT_OPTIONS.map(v => <option key={v} value={v}>{v}</option>)}
</select>
<span> consecutive invalid logins are attempted in a </span>
<select
disabled={!canEdit || !loginAttemptEnabled}
name="LoginAttemptPeriod"
onChange={onLoginAttemptSelectChange}
value={loginAttemptPeriod}
>
{LOGIN_ATTEMPT_PERIOD_OPTIONS.map(v => <option key={v} value={v}>{v}</option>)}
</select>
<span> second period. Automatically allow users to login again after </span>
<select
disabled={!canEdit || !loginAttemptEnabled}
name="LoginAttemptResetTime"
onChange={onLoginAttemptSelectChange}
value={loginAttemptResetTime}
>
{LOGIN_ATTEMPT_RESET_TIME_OPTIONS.map(v => <option key={v} value={v}>{v}</option>)}
</select>
<span> minutes.</span>
</div>
</div>
</div>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions core/src/client/components/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface AuthConfigProvider {
export interface GlobalSettingsOptions {
AutoCreateAccounts?: boolean;
DefaultDomain?: string;
LoginAttemptEnabled?: boolean;
LoginAttemptLimit?: string;
LoginAttemptPeriod?: string;
LoginAttemptResetTime?: string;
SelfRegistration?: boolean;
SelfServiceEmailChanges?: boolean;
}
Expand Down
7 changes: 5 additions & 2 deletions core/src/org/labkey/core/CoreModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
import org.labkey.core.dialect.PostgreSqlVersion;
import org.labkey.core.junit.JunitController;
import org.labkey.core.login.DbLoginAuthenticationProvider;
import org.labkey.core.login.LoginAttemptDisableLoginProvider;
import org.labkey.core.login.DbLoginManager;
import org.labkey.core.login.LoginController;
import org.labkey.core.metrics.SimpleMetricsServiceImpl;
Expand Down Expand Up @@ -446,15 +447,16 @@ protected void init()
addController("notification", NotificationController.class);
addController("product", ProductController.class);

WarningService.setInstance(new WarningServiceImpl());

AuthenticationManager.registerProvider(new DbLoginAuthenticationProvider(), Priority.Low);
AuthenticationManager.addLoginAttemptSettingsListener(LoginAttemptDisableLoginProvider::reloadCache);
AttachmentService.setInstance(new AttachmentServiceImpl());
AnalyticsService.setInstance(new AnalyticsServiceImpl());
RhinoService.register();
CacheManager.addListener(RhinoService::clearCaches);
NotificationService.setInstance(NotificationServiceImpl.getInstance());

WarningService.setInstance(new WarningServiceImpl());

ViewService.setInstance(ViewServiceImpl.getInstance());
OptionalFeatureService.setInstance(new OptionalFeatureServiceImpl());
ThumbnailService.setInstance(new ThumbnailServiceImpl());
Expand Down Expand Up @@ -975,6 +977,7 @@ public void startupAfterSpringConfig(ModuleContext moduleContext)
FolderTypeManager.get().registerFolderType(this, FolderType.NONE);
FolderTypeManager.get().registerFolderType(this, new CollaborationFolderType());

AuthenticationManager.registerProvider(new LoginAttemptDisableLoginProvider());
AnalyticsServiceImpl.get().resetCSP();

if (moduleContext.isNewInstall() && ModuleLoader.getInstance().shouldInsertData())
Expand Down
Loading
Loading