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

Add 2 factor authentication as optional feature #70

Merged
merged 11 commits into from
Apr 13, 2019
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pytest >= 3.7
pytest-asyncio
notebook==5.7.2
bcrypt
onetimepass
Binary file added docs/_static/login-two-factor-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/signup-two-factor-auth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion docs/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ Native Authenticator is based on username and password only. But if you need ext

c.Authenticator.ask_email_on_signup = True


Import users from FirstUse Authenticator
----------------------------------------

Expand Down Expand Up @@ -101,3 +100,22 @@ You can also remove FirstUse's database file after the importation to Native Aut

c.Authenticator.delete_firstuse_db_after_import = True


Add two factor authentication obligatory for users
--------------------------------------------------

You can increase security making two factor authentication obligatory for all users.
To do so, add the following line on the config file:

.. code-block:: python

c.Authenticator.add_two_factor_authentication = True

Users will receive a message after signup with the two factor authentication code:

.. image:: _static/signup-two-factor-auth.png

And login will now require the two factor authentication code as well:


.. image:: _static/login-two-factor-auth.png
13 changes: 12 additions & 1 deletion nativeauthenticator/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async def get(self):
html = self.render_template(
'signup.html',
ask_email=self.authenticator.ask_email_on_signup,
two_factor_auth=self.authenticator.allow_2fa,
)
self.finish(html)

Expand Down Expand Up @@ -68,16 +69,25 @@ async def post(self):
'username': self.get_body_argument('username', strip=False),
'pw': self.get_body_argument('pw', strip=False),
'email': self.get_body_argument('email', '', strip=False),
'has_2fa': bool(self.get_body_argument('2fa', '', strip=False)),
}
user = self.authenticator.get_or_create_user(**user_info)

alert, message = self.get_result_message(user)

otp_secret, user_2fa = '', ''
if user:
otp_secret = user.otp_secret
user_2fa = user.has_2fa

html = self.render_template(
'signup.html',
ask_email=self.authenticator.ask_email_on_signup,
result_message=message,
alert=alert
alert=alert,
two_factor_auth=self.authenticator.allow_2fa,
two_factor_auth_user=user_2fa,
two_factor_auth_value=otp_secret,
)
self.finish(html)

Expand Down Expand Up @@ -135,6 +145,7 @@ def _render(self, login_error=None, username=None):
login_error=login_error,
custom_html=self.authenticator.custom_html,
login_url=self.settings['login_url'],
two_factor_auth=self.authenticator.allow_2fa,
authenticator_login_url=url_concat(
self.authenticator.login_url(self.hub.base_url),
{'next': self.get_argument('next', '')},
Expand Down
18 changes: 15 additions & 3 deletions nativeauthenticator/nativeauthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ class NativeAuthenticator(Authenticator):
"the system without needing admin authorization")
)
ask_email_on_signup = Bool(
False,
config=True,
default_value=False,
help="Asks for email on signup"
)
import_from_firstuse = Bool(
False,
config=True,
default_value=False,
help="Import users from FirstUse Authenticator database"
)
firstuse_db_path = Unicode(
Expand All @@ -69,6 +69,11 @@ class NativeAuthenticator(Authenticator):
default_value=False,
help="Deletes FirstUse Authenticator database after the import"
)
allow_2fa = Bool(
False,
config=True,
help=""
)

def __init__(self, add_new_table=True, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -131,7 +136,14 @@ def authenticate(self, handler, data):
if self.is_blocked(username):
return

if user.is_authorized and user.is_valid_password(password):
validations = [
user.is_authorized,
user.is_valid_password(password)
]
if user.has_2fa:
validations.append(user.is_valid_token(data.get('2fa')))

if all(validations):
self.successful_login(username)
return username

Expand Down
13 changes: 13 additions & 0 deletions nativeauthenticator/orm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import base64
import bcrypt
import os
import re
from jupyterhub.orm import Base

import onetimepass
from sqlalchemy import Boolean, Column, Integer, String, LargeBinary
from sqlalchemy.orm import validates

Expand All @@ -13,6 +16,13 @@ class UserInfo(Base):
password = Column(LargeBinary, nullable=False)
is_authorized = Column(Boolean, default=False)
email = Column(String)
has_2fa = Column(Boolean, default=False)
otp_secret = Column(String(10))
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the '10' refer to here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the token is created with size 10, so I limited here


def __init__(self, **kwargs):
super(UserInfo, self).__init__(**kwargs)
if not self.otp_secret:
self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')

@classmethod
def find(cls, db, username):
Expand Down Expand Up @@ -40,3 +50,6 @@ def validate_email(self, key, address):
assert re.match(r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$",
address)
return address

def is_valid_token(self, token):
return onetimepass.valid_totp(token, self.otp_secret)
89 changes: 41 additions & 48 deletions nativeauthenticator/templates/autorization-area.html
Original file line number Diff line number Diff line change
@@ -1,53 +1,46 @@
{% extends "page.html" %}
{% block main %}


<div class="container">

<div class="container">
<h1>Authorization Area</h1>

<table class="table">
<thead>
<tr>
<th>Username</th>
{% if ask_email %}
<th>Email</th>
{% endif %}
<th>Is Authorized?</th>
</tr>
</thead>
<tbody>
{% for user in users %}
{% if user.is_authorized %}
<tr class="success">
<td>{{ user.username }}</td>
{% if ask_email %}
<td>{{ user.email }}</td>
{% endif %}
<td>Yes</td>
<td>
<a class="btn btn-default"
href="/hub/authorize/{{ user.username }}"
role="button">Unauthorize</a>
</td>
{% else %}
<tr>
<td>{{ user.username }}</td>
{% if ask_email %}
<td>{{ user.email }}</td>
{% endif %}
<td>No</td>
<td>
<a class="btn btn-jupyter"
href="/hub/authorize/{{ user.username }}"
role="button">Authorize</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
<thead>
<tr>
<th>Username</th>
{% if ask_email %}
<th>Email</th>
{% endif %}
<th>Has 2fa?</th>
<th>Is Authorized?</th>
</tr>
</thead>
<tbody>
{% for user in users %}
{% if user.is_authorized %}
<tr class="success">
<td>{{ user.username }}</td>
{% if ask_email %}
<td>{{ user.email }}</td>
{% endif %}
<td>{{ user.has_2fa }}</td>
<td>Yes</td>
<td>
<a class="btn btn-default" href="/hub/authorize/{{ user.username }}" role="button">Unauthorize</a>
</td>
{% else %}
<tr>
<td>{{ user.username }}</td>
{% if ask_email %}
<td>{{ user.email }}</td>
{% endif %}
<td>{{ user.has_2fa }}</td>
<td>No</td>
<td>
<a class="btn btn-jupyter" href="/hub/authorize/{{ user.username }}" role="button">Authorize</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>


{% endblock %}
</div>
{% endblock %}
8 changes: 7 additions & 1 deletion nativeauthenticator/templates/native-login.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
{% endif %}
<label for="username_input">Username:</label>
<input id="username_input" type="text" autocapitalize="off" autocorrect="off" class="form-control" name="username" val="{{username}}" tabindex="1" autofocus="autofocus" />
<p></p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the empty

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A hack in the design the give more space between things 😅 I can take it off

<label for='password_input'>Password:</label>
<div class="input-group">
<input type="password" class="form-control" name="password" id="password_input" tabindex="2" />
Expand All @@ -42,9 +43,14 @@
</button>
</span>
</div>
{% if two_factor_auth %}
<p></p>
<label for="2fa_input">Two Factor Authentication:</label>
<input id="2fa_input" type="text" autocapitalize="off" autocorrect="off" class="form-control" name="2fa" tabindex="1" autofocus="autofocus" placeholder="If you don't have 2FA, leave empty" />
{% endif %}
<input type="submit" id="login_submit" class='btn btn-jupyter' value='Sign In' tabindex="3" />
<div style="padding-top: 25px;">
Don't have an user? <a href="/signup">Signup!</a>
<p>Don't have an user? <a href="/signup">Signup!</a></p>
</div>
</div>
</form>
Expand Down
16 changes: 15 additions & 1 deletion nativeauthenticator/templates/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,26 @@
</button>
</span>
</div>
<p></p>
{% if two_factor_auth %}
<label>Add two factor authentication:</label>
<input type="checkbox" name="2fa" value="2fa">
{% endif %}
<input type="submit" id="signup_submit" class='btn btn-jupyter' value='Create User' tabindex="3" />
<div style="padding-top: 25px;">
Already have an user? <a href="/login">Login!</a>
</div>
{% if alert %}
<div class="alert {{alert}}" style="margin-top: 15px;" role="alert">{{result_message}}</div>
<div class="alert {{alert}}" style="margin-top: 15px;" role="alert">
{{result_message}}
{% if two_factor_auth_user %}
<p>
<p />
<strong>Attention!</strong> You have configured two factor authentication.
<br />
Your two factor authentication code is <strong>{{ two_factor_auth_value }}</strong>. Be sure to keep it safe :)
{% endif %}
</div>
{% endif %}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
author_email='leportella@protonmail.com',
license='3 Clause BSD',
packages=find_packages(),
install_requires=['jupyterhub>=0.8', 'bcrypt'],
install_requires=['jupyterhub>=0.8', 'bcrypt', 'onetimepass'],
include_package_data=True,
)