Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2e8a799
check disable_totp config before totp authentication
RabiaSajjad Sep 1, 2022
8b61855
add helper for checking disable_totp configuration
RabiaSajjad Sep 1, 2022
3bf339f
add security_disable_totp helper to plugin
RabiaSajjad Sep 1, 2022
6749758
hide display of 2fa fieldset if disable_totp is set
RabiaSajjad Sep 1, 2022
2042950
disable_totp is False by default
RabiaSajjad Sep 1, 2022
984b1dd
add ckanext.security.disable_totp to readme
RabiaSajjad Sep 1, 2022
f9b6d3d
Update edit_user_form.html
RabiaSajjad Sep 1, 2022
6a2371d
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
8b6de1f
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
80d6be8
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
ef25c37
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
09a867d
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
0b9c7de
replace disable_totp with enable_totp
RabiaSajjad Sep 1, 2022
992c55d
replace ckan.common with ckan.plugins.toolkit
RabiaSajjad Sep 2, 2022
0e059d4
improve code readability by removing double negative if not...else co…
RabiaSajjad Sep 2, 2022
791e4e4
use security_enable_totp helper in authenticator
RabiaSajjad Dec 29, 2022
67a3c41
reset throttle if security_enable_totp is false
RabiaSajjad Jan 4, 2023
c80f378
import ckan config in authenticator
RabiaSajjad Jan 20, 2023
dbff926
check security_enable_totp config in login form
RabiaSajjad Jan 24, 2023
668783d
Enable email login
saj-derilinx Nov 17, 2023
f04118c
Get user name from user model object
saj-derilinx Nov 17, 2023
e8f0fc6
Fix form label and disable help link
saj-derilinx Nov 17, 2023
20b2c50
Change text
saj-derilinx Dec 4, 2023
913e14b
Add the validations as get_validators instead of monkey patching
EricSoroos Dec 11, 2023
f561c23
add validation for too many repeated characters
EricSoroos Dec 20, 2023
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ ckanext.security.brute_force_key = user_name # Detect brute force attempts by u
# You can disable the fix in this plugin by:
ckanext.security.disable_password_reset_override = true

# Two factor authentication is enabled for all users by default
# optional configuration to disable 2fa
ckanext.security.enable_totp = true # set to false to disable 2fa

# Provide a help page to allow 2fa users to contact support or get more information
# Shows up as 'Need help?' on the 2fa entry form beside the submit button. Does not display a link if none provided
ckanext.security.mfa_help_link = https://data.govt.nz/catalogue-guide/releasing-data-on-data-govt-nz/how-do-i-set-up-two-factor-authentication/
Expand Down
15 changes: 14 additions & 1 deletion ckanext/security/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import pylons
from ckan.lib.cli import MockTranslator

from ckan import model
from ckan.lib.authenticator import UsernamePasswordAuthenticator
from ckan.model import User
from ckan.common import config
from webob.request import Request
import ckan.plugins as p
from ckan.plugins.toolkit import config
from ckanext.security.cache.login import LoginThrottle
from ckanext.security.helpers import security_enable_totp
from ckanext.security.model import SecurityTOTP, ReplayAttackException

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,6 +75,10 @@ def authenticate(self, environ, identity):
to log into a specific account within a period of time."""
try:
user_name = identity['login']
user = model.User.by_name(user_name)
if not user:
user = model.User.by_email(user_name)
user_name = user.name
except KeyError:
return None

Expand Down Expand Up @@ -105,6 +111,13 @@ def authenticate(self, environ, identity):
# Increment the throttle counter if the login failed.
throttle.increment()

# totp authentication is enabled by default for all users
# totp can be disabled, if needed, by setting
# ckanext.security.enable_totp to false in configurations
if not security_enable_totp():
throttle.reset()
return auth_user_name

# if the CKAN authenticator has successfully authenticated
# the request and the user wasn't locked out above,
# then check the TOTP parameter to see if it is valid
Expand Down
5 changes: 5 additions & 0 deletions ckanext/security/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ckan.plugins.toolkit import asbool, config


def security_enable_totp():
return asbool(config.get('ckanext.security.enable_totp', True))
12 changes: 12 additions & 0 deletions ckanext/security/plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
validate_upload_type, validate_upload_presence
)
from ckanext.security.logic import auth, action
from ckanext.security.helpers import security_enable_totp
from ckanext.security import validators

try:
tk.requires_ckan_version("2.9")
Expand All @@ -26,6 +28,7 @@ class CkanSecurityPlugin(MixinPlugin, p.SingletonPlugin):
p.implements(p.IActions)
p.implements(p.IAuthFunctions)
p.implements(p.ITemplateHelpers)
p.implements(p.IValidators)

# BEGIN Hooks for IConfigurer

Expand All @@ -50,6 +53,14 @@ def update_config(self, config):

# END Hooks for IConfigurer

# BEGIN hooks for IValidators
def get_validators(self):
return {
'user_password_validator': validators.user_password_validator,
'old_username_validator': validators.old_username_validator,
}
# END hooks for IValidators

# BEGIN Hooks for IResourceController

def before_create(self, context, resource):
Expand Down Expand Up @@ -105,4 +116,5 @@ def get_auth_functions(self):
def get_helpers(self):
return {
'check_ckan_version': tk.check_ckan_version,
'security_enable_totp': security_enable_totp,
}
18 changes: 10 additions & 8 deletions ckanext/security/templates/user/edit_user_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,16 @@
{{ form.input('password2', type='password', label=_('Confirm Password'), id='field-password-confirm', value=data.password2, error=errors.password2, classes=['control-medium'], attrs={'autocomplete': 'off'}) }}
</fieldset>

<fieldset>
<legend>{{_('Two factor authentication')}}</legend>
{% if h.check_ckan_version('2.9') %}
{% link_for _('Manage two factor authentication'), controller='mfa_user', action='configure_mfa', id=data.id, class_='btn btn-default pull-left', icon='cog' %}
{% else %}
{% link_for _('Manage two factor authentication'), controller='ckanext.security.controllers:MFAUserController', action='configure_mfa', id=data.id, class_='btn btn-default pull-left', icon='cog' %}
{% endif %}
</fieldset>
{% if h.security_enable_totp() %}
<fieldset>
<legend>{{_('Two factor authentication')}}</legend>
{% if h.check_ckan_version('2.9') %}
{% link_for _('Manage two factor authentication'), controller='mfa_user', action='configure_mfa', id=data.id, class_='btn btn-default pull-left', icon='cog' %}
{% else %}
{% link_for _('Manage two factor authentication'), controller='ckanext.security.controllers:MFAUserController', action='configure_mfa', id=data.id, class_='btn btn-default pull-left', icon='cog' %}
{% endif %}
</fieldset>
{% endif %}

<div class="form-actions">
{% block delete_button %}
Expand Down
52 changes: 28 additions & 24 deletions ckanext/security/templates/user/snippets/login_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
{% set username_error = true if error_summary %}
{% set password_error = true if error_summary %}

{% if h.check_ckan_version('2.9') %}
{% asset 'security/mfa_login' %}
{% else %}
{% resource 'security/login_ajax.js' %}
{% resource 'security/qrious.js' %}
{% if h.security_enable_totp() %}
{% if h.check_ckan_version('2.9') %}
{% asset 'security/mfa_login' %}
{% else %}
{% resource 'security/login_ajax.js' %}
{% resource 'security/qrious.js' %}
{% endif %}
{% endif %}


Expand All @@ -29,7 +31,7 @@
<div id="login-form">

<div id="login-fields">
{{ form.input('login', label=_("Username"), id='field-login', value="", error=username_error, classes=["control-medium"]) }}
{{ form.input('login', label=_("Username or Email"), id='field-login', value="", error=username_error, classes=["control-medium"]) }}

{{ form.input('password', label=_("Password"), id='field-password', type="password", value="", error=password_error, classes=["control-medium"]) }}
</div>
Expand All @@ -43,27 +45,29 @@
</div>
</div>

<div id="mfa-form" style="display: none;">
<fieldset id="mfa-setup" style="display: none;">
<legend>{{_('Scan this QR code with your two factor authentication app')}}</legend>
<p>{% trans %}If you don't already have an authenticator app, you could try Google Authenticator.{% endtrans %}</p>
{% if h.security_enable_totp() %}
<div id="mfa-form" style="display: none;">
<fieldset id="mfa-setup" style="display: none;">
<legend>{{_('Scan this QR code with your two factor authentication app')}}</legend>
<p>{% trans %}If you don't already have an authenticator app, you could try Google Authenticator.{% endtrans %}</p>

<div>
<canvas class="radius-lg padding-sm margin-b-sm border-solid" id="qr-code-container"></canvas>
</div>
<p>
{% trans %}If you are not able to scan the QR code, you can manually enter this secret into your authenticator app: {% endtrans %}<code id="totp-secret"></code>
</p>
</fieldset>
<div>
<canvas class="radius-lg padding-sm margin-b-sm border-solid" id="qr-code-container"></canvas>
</div>
<p>
{% trans %}If you are not able to scan the QR code, you can manually enter this secret into your authenticator app: {% endtrans %}<code id="totp-secret"></code>
</p>
</fieldset>

<p>{{_('Please enter your authenticator app generated 6-digit verification code.')}}</p>
{{ form.input('mfa', label=_("Verification code"), id='field-mfa', type="text", value="", error=mfa_error, classes=["control-medium"], attrs={"autocomplete": "off"}) }}
<p>{{_('Please enter your authenticator app generated 6-digit verification code.')}}</p>
{{ form.input('mfa', label=_("Verification code"), id='field-mfa', type="text", value="", error=mfa_error, classes=["control-medium"], attrs={"autocomplete": "off"}) }}

<input id="mfa-form-active" name="mfa-form-active" type="hidden" value="" />
<div class="form-actions">
<a id="mfa-help-link" href="/" style="display: none; margin-right: 20px;">{{_('Need help?')}}</a>
<button class="btn btn-primary" type="submit">{{ _('Submit') }}</button>
<input id="mfa-form-active" name="mfa-form-active" type="hidden" value="" />
<div class="form-actions">
{# <a id="mfa-help-link" href="/" style="display: none; margin-right: 20px;">{{_('Need help?')}}</a> #}
<button class="btn btn-primary" type="submit">{{ _('Submit') }}</button>
</div>
</div>
</div>
{% endif %}

</form>
3 changes: 2 additions & 1 deletion ckanext/security/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def login():

user_name = identity['login']
user = model.User.by_name(user_name)
if not user:
user = model.User.by_email(user_name)

login_throttle_key = get_login_throttle_key(request, user_name)
if login_throttle_key is None:
Expand Down Expand Up @@ -174,7 +176,6 @@ def login():
log.info('User %s supplied invalid 2fa code', user_name)
response_status = 403
throttle.increment()

return (response_status, json.dumps(res))

except Exception as err:
Expand Down
21 changes: 19 additions & 2 deletions ckanext/security/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# encoding: utf-8
import six
import string
import collections

from ckan import authz
from ckan.common import _
Expand All @@ -10,9 +11,24 @@
MIN_LEN_ERROR = (
'Your password must be {} characters or longer, and consist of at least '
'three of the following character sets: uppercase characters, lowercase '
'characters, digits, punctuation & special characters.'
'characters, digits, punctuation & special characters, and not contain '
'too many repeated characters.'
)

def _too_many_repeated_characters(value):
""" does the password contain too many repeated characters

Returns True if the most frequent character is >= 1/3 of the characters.
e.g. "password" is false: ct(s)==2 < 8/3
"aaaaword" is true: ct(a)==4 > 8/3

:param s: proposed password
:returns: boolean, True if password is ok by this criteria
"""
char_counts = collections.Counter(value)
# note, will fail on empty password, but caller checks MIN_PASSWORD_LENGTH
return (char_counts.most_common(1)[0][1] >= (len(value)/3))


def user_password_validator(key, data, errors, context):
value = data[key]
Expand All @@ -31,7 +47,8 @@ def user_password_validator(key, data, errors, context):
any(x.isdigit() for x in value),
any(x in string.punctuation for x in value)
]
if len(value) < MIN_PASSWORD_LENGTH or sum(rules) < 3:
if len(value) < MIN_PASSWORD_LENGTH or sum(rules) < 3 \
or _too_many_repeated_characters(value):
raise Invalid(_(MIN_LEN_ERROR.format(MIN_PASSWORD_LENGTH)))


Expand Down