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

Allow logging in by IP address #308

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions dmoj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
VNOJ_ORG_PP_SCALE = 1

VNOJ_OFFICIAL_CONTEST_MODE = False
VNOJ_ALLOW_LOGIN_BY_IP_ADDRESS = False

# Contribution points function
# Both should be int
Expand Down Expand Up @@ -690,6 +691,7 @@
'social_core.backends.facebook.FacebookOAuth2',
'judge.social_auth.GitHubSecureEmailOAuth2',
'django.contrib.auth.backends.ModelBackend',
'judge.ip_based_auth.IPBasedAuthBackend',
)

SOCIAL_AUTH_PIPELINE = (
Expand Down
46 changes: 43 additions & 3 deletions judge/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import webauthn
from django import forms
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -47,6 +48,15 @@
}


def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip


class ProfileForm(ModelForm):
if newsletter_id is not None:
newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False)
Expand Down Expand Up @@ -428,13 +438,43 @@ def _has_social_auth(self, key):
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None))

def clean(self):
username = self.cleaned_data.get('username')
is_ip_login = settings.VNOJ_ALLOW_LOGIN_BY_IP_ADDRESS and self.request.POST.get('ip-login', 'false') == 'true'

try:
user = User.objects.get(username=username)
except User.DoesNotExist:
if is_ip_login:
ip = get_client_ip(self.request)
profile = Profile.objects.filter(ip=ip).order_by('-last_access').select_related('user').first()

if profile is None:
raise Profile.DoesNotExist

user = profile.user
else:
username = self.cleaned_data.get('username')
user = User.objects.get(username=username)
except (Profile.DoesNotExist, User.DoesNotExist):
user = None

if user is not None:
self.confirm_login_allowed(user)

if is_ip_login:
# a hack to remove the username/password errors, since we don't need them
del self.errors['username']
del self.errors['password']

# set the password to the user's password to pass the form_valid check
if user is not None:
self.cleaned_data['password'] = user.password

# super.clean() will skip the check, since there is no username/password when logging in by IP address,
# so this is a hack to process that manually
self.user_cache = authenticate(self.request, ip_address=ip)
if self.user_cache is None:
raise self.get_invalid_login_error()
else:
self.confirm_login_allowed(self.user_cache)

return super(CustomAuthenticationForm, self).clean()

def confirm_login_allowed(self, user):
Expand Down
15 changes: 15 additions & 0 deletions judge/ip_based_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.contrib.auth.backends import ModelBackend

from judge.models import Profile


class IPBasedAuthBackend(ModelBackend):
def authenticate(self, request, ip_address=None):
try:
profile = Profile.objects.filter(ip=ip_address).order_by('-last_access').select_related('user').first()
if profile is None:
return None
return profile.user
except Profile.DoesNotExist:
user = None
return user
5 changes: 4 additions & 1 deletion judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ def get(self, request, *args, **kwargs):

class CustomLoginView(LoginView):
template_name = 'registration/login.html'
extra_context = {'title': gettext_lazy('Login')}
extra_context = {
'title': gettext_lazy('Login'),
'allow_login_by_ip_address': settings.VNOJ_ALLOW_LOGIN_BY_IP_ADDRESS,
}
authentication_form = CustomAuthenticationForm
redirect_authenticated_user = True

Expand Down
16 changes: 16 additions & 0 deletions templates/registration/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
</style>
{% endblock %}

{% block js_media %}
{% if allow_login_by_ip_address %}
<script>
$(document).ready(function () {
$("#ip-login-button").click(function () {
$("input[name='ip-login']").val("true");
});
});
</script>
{% endif %}
{% endblock %}

{% block body %}
<div class="auth-flow-form">
<form action="" method="post" class="form-area">
Expand Down Expand Up @@ -57,7 +69,11 @@
</table>
<hr>
<button style="float:right;" type="submit">{{ _('Login!') }}</button>
{% if allow_login_by_ip_address %}
<button style="float:right; margin-right: 0.5em" type="submit" id="ip-login-button" formnovalidate>{{_('Login by IP Address!')}}</button>
{% endif %}
<input type="hidden" name="next" value="{{ next }}">
<input type="hidden" name="ip-login" value="false">
</form>
<a href="{{ url('password_reset') }}">{{ _('Forgot your password?') }}</a>

Expand Down