Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE
/
cloud
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
94
Merge Requests
10
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
21207ef7
authored
Aug 09, 2016
by
Kálmán Viktor
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dashboard: 2fa auth after saml2/password login
parent
3e213c2d
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
278 additions
and
25 deletions
+278
-25
circle/circle/urls.py
+8
-2
circle/dashboard/forms.py
+16
-0
circle/dashboard/models.py
+0
-3
circle/dashboard/views/user.py
+159
-16
circle/dashboard/views/util.py
+71
-4
circle/templates/registration/two-factor-login.html
+24
-0
No files found.
circle/circle/urls.py
View file @
21207ef7
...
@@ -25,7 +25,9 @@ from django.shortcuts import redirect
...
@@ -25,7 +25,9 @@ from django.shortcuts import redirect
from
circle.settings.base
import
get_env_variable
from
circle.settings.base
import
get_env_variable
from
dashboard.views
import
circle_login
,
HelpView
,
ResizeHelpView
from
dashboard.views
import
(
CircleLoginView
,
HelpView
,
ResizeHelpView
,
TwoFactorLoginView
)
from
dashboard.forms
import
CirclePasswordResetForm
,
CircleSetPasswordForm
from
dashboard.forms
import
CirclePasswordResetForm
,
CircleSetPasswordForm
from
firewall.views
import
add_blacklist_item
from
firewall.views
import
add_blacklist_item
...
@@ -52,8 +54,12 @@ urlpatterns = patterns(
...
@@ -52,8 +54,12 @@ urlpatterns = patterns(
{
'password_reset_form'
:
CirclePasswordResetForm
},
{
'password_reset_form'
:
CirclePasswordResetForm
},
name
=
"accounts.password-reset"
,
name
=
"accounts.password-reset"
,
),
),
url
(
r'^accounts/login/?$'
,
circle_login
,
name
=
"accounts.login"
),
url
(
r'^accounts/login/?$'
,
CircleLoginView
.
as_view
(),
name
=
"accounts.login"
),
url
(
r'^accounts/'
,
include
(
'django.contrib.auth.urls'
)),
url
(
r'^accounts/'
,
include
(
'django.contrib.auth.urls'
)),
url
(
r'^two-factor-login/$'
,
TwoFactorLoginView
.
as_view
(),
name
=
"two-factor-login"
),
url
(
r'^info/help/$'
,
HelpView
.
as_view
(
template_name
=
"info/help.html"
),
url
(
r'^info/help/$'
,
HelpView
.
as_view
(
template_name
=
"info/help.html"
),
name
=
"info.help"
),
name
=
"info.help"
),
url
(
r'^info/policy/$'
,
url
(
r'^info/policy/$'
,
...
...
circle/dashboard/forms.py
View file @
21207ef7
...
@@ -1686,3 +1686,19 @@ class DisableTwoFactorForm(ModelForm):
...
@@ -1686,3 +1686,19 @@ class DisableTwoFactorForm(ModelForm):
totp
=
pyotp
.
TOTP
(
self
.
instance
.
two_factor_secret
)
totp
=
pyotp
.
TOTP
(
self
.
instance
.
two_factor_secret
)
if
not
totp
.
verify
(
self
.
cleaned_data
.
get
(
'confirmation_code'
)):
if
not
totp
.
verify
(
self
.
cleaned_data
.
get
(
'confirmation_code'
)):
raise
ValidationError
(
_
(
"Invalid confirmation code."
))
raise
ValidationError
(
_
(
"Invalid confirmation code."
))
class
TwoFactorAuthForm
(
forms
.
Form
):
confirmation_code
=
forms
.
CharField
(
label
=
_
(
'Confirmation code'
),
help_text
=
_
(
"Get the code from your authenticator to disable "
"two-factor authentication."
))
def
__init__
(
self
,
user
,
*
args
,
**
kwargs
):
self
.
user
=
user
super
(
TwoFactorAuthForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
def
clean_confirmation_code
(
self
):
totp
=
pyotp
.
TOTP
(
self
.
user
.
profile
.
two_factor_secret
)
if
not
totp
.
verify
(
self
.
cleaned_data
.
get
(
'confirmation_code'
)):
raise
ValidationError
(
_
(
"Invalid confirmation code."
))
circle/dashboard/models.py
View file @
21207ef7
...
@@ -399,9 +399,6 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
...
@@ -399,9 +399,6 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
pre_user_save
.
connect
(
save_org_id
)
pre_user_save
.
connect
(
save_org_id
)
else
:
logger
.
debug
(
"Do not register save_org_id to djangosaml2 pre_user_save"
)
def
update_store_profile
(
sender
,
**
kwargs
):
def
update_store_profile
(
sender
,
**
kwargs
):
profile
=
kwargs
.
get
(
'instance'
)
profile
=
kwargs
.
get
(
'instance'
)
...
...
circle/dashboard/views/user.py
View file @
21207ef7
...
@@ -23,9 +23,8 @@ import pyotp
...
@@ -23,9 +23,8 @@ import pyotp
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib
import
messages
from
django.contrib
import
messages
from
django.contrib.auth
import
login
from
django.contrib.auth
import
login
,
authenticate
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.views
import
login
as
login_view
from
django.contrib.messages.views
import
SuccessMessageMixin
from
django.contrib.messages.views
import
SuccessMessageMixin
from
django.core
import
signing
from
django.core
import
signing
from
django.core.exceptions
import
PermissionDenied
,
SuspiciousOperation
from
django.core.exceptions
import
PermissionDenied
,
SuspiciousOperation
...
@@ -38,7 +37,7 @@ from django.templatetags.static import static
...
@@ -38,7 +37,7 @@ from django.templatetags.static import static
from
django.utils.translation
import
ugettext
as
_
from
django.utils.translation
import
ugettext
as
_
from
django.views.decorators.http
import
require_POST
from
django.views.decorators.http
import
require_POST
from
django.views.generic
import
(
from
django.views.generic
import
(
TemplateView
,
View
,
UpdateView
,
CreateView
,
TemplateView
,
View
,
UpdateView
,
CreateView
,
FormView
)
)
from
django_sshkey.models
import
UserKey
from
django_sshkey.models
import
UserKey
...
@@ -51,14 +50,15 @@ from vm.models import Instance, InstanceTemplate
...
@@ -51,14 +50,15 @@ from vm.models import Instance, InstanceTemplate
from
..forms
import
(
from
..forms
import
(
CircleAuthenticationForm
,
MyProfileForm
,
UserCreationForm
,
UnsubscribeForm
,
CircleAuthenticationForm
,
MyProfileForm
,
UserCreationForm
,
UnsubscribeForm
,
UserKeyForm
,
CirclePasswordChangeForm
,
ConnectCommandForm
,
UserKeyForm
,
CirclePasswordChangeForm
,
ConnectCommandForm
,
UserListSearchForm
,
UserEditForm
,
TwoFactorForm
,
DisableTwoFactorForm
UserListSearchForm
,
UserEditForm
,
TwoFactorForm
,
DisableTwoFactorForm
,
TwoFactorAuthForm
,
)
)
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
from
..tables
import
(
from
..tables
import
(
UserKeyListTable
,
ConnectCommandListTable
,
UserListTable
,
UserKeyListTable
,
ConnectCommandListTable
,
UserListTable
,
)
)
from
.util
import
saml_available
,
DeleteViewBase
from
.util
import
saml_available
,
DeleteViewBase
,
LoginView
logger
=
logging
.
getLogger
(
__name__
)
logger
=
logging
.
getLogger
(
__name__
)
...
@@ -98,17 +98,29 @@ class NotificationView(LoginRequiredMixin, TemplateView):
...
@@ -98,17 +98,29 @@ class NotificationView(LoginRequiredMixin, TemplateView):
return
response
return
response
def
circle_login
(
request
):
class
CircleLoginView
(
LoginView
):
authentication_form
=
CircleAuthenticationForm
form_class
=
CircleAuthenticationForm
extra_context
=
{
'saml2'
:
saml_available
,
def
get_context_data
(
self
,
**
kwargs
):
'og_image'
:
(
settings
.
DJANGO_URL
.
rstrip
(
"/"
)
+
ctx
=
super
(
CircleLoginView
,
self
)
.
get_context_data
(
**
kwargs
)
static
(
"dashboard/img/og.png"
))
ctx
.
update
({
}
'saml2'
:
saml_available
,
response
=
login_view
(
request
,
authentication_form
=
authentication_form
,
'og_image'
:
(
settings
.
DJANGO_URL
.
rstrip
(
"/"
)
+
extra_context
=
extra_context
)
static
(
"dashboard/img/og.png"
))
set_language_cookie
(
request
,
response
)
})
return
response
return
ctx
def
form_valid
(
self
,
form
):
user
=
form
.
get_user
()
if
user
.
profile
.
two_factor_secret
:
self
.
request
.
session
[
'two-fa-user'
]
=
user
.
pk
self
.
request
.
session
[
'two-fa-redirect'
]
=
self
.
get_success_url
()
self
.
request
.
session
[
'login-type'
]
=
"password"
return
redirect
(
reverse
(
"two-factor-login"
))
else
:
response
=
super
(
CircleLoginView
,
self
)
.
form_valid
(
form
)
set_language_cookie
(
self
.
request
,
response
)
return
response
class
TokenLogin
(
View
):
class
TokenLogin
(
View
):
...
@@ -589,3 +601,134 @@ class DisableTwoFactorView(LoginRequiredMixin, UpdateView):
...
@@ -589,3 +601,134 @@ class DisableTwoFactorView(LoginRequiredMixin, UpdateView):
raise
PermissionDenied
raise
PermissionDenied
return
self
.
request
.
user
.
profile
return
self
.
request
.
user
.
profile
class
TwoFactorLoginView
(
FormView
):
form_class
=
TwoFactorAuthForm
template_name
=
"registration/two-factor-login.html"
def
dispatch
(
self
,
*
args
,
**
kwargs
):
if
not
self
.
request
.
session
.
get
(
'two-fa-user'
):
return
redirect
(
"/"
)
return
super
(
TwoFactorLoginView
,
self
)
.
dispatch
(
*
args
,
**
kwargs
)
def
get_form_kwargs
(
self
):
kwargs
=
super
(
TwoFactorLoginView
,
self
)
.
get_form_kwargs
()
user_pk
=
self
.
request
.
session
[
'two-fa-user'
]
kwargs
[
'user'
]
=
User
.
objects
.
get
(
pk
=
user_pk
)
return
kwargs
def
form_valid
(
self
,
form
):
user_pk
=
self
.
request
.
session
[
'two-fa-user'
]
user
=
User
.
objects
.
get
(
pk
=
user_pk
)
if
self
.
request
.
session
[
'login-type'
]
==
"saml2"
:
user
.
backend
=
'common.backends.Saml2Backend'
else
:
user
.
backend
=
'django.contrib.auth.backends.ModelBackend'
login
(
self
.
request
,
user
)
response
=
redirect
(
self
.
request
.
session
[
'two-fa-redirect'
])
set_language_cookie
(
self
.
request
,
response
)
return
response
if
hasattr
(
settings
,
'SAML_ORG_ID_ATTRIBUTE'
):
from
django.http
import
HttpResponseBadRequest
,
HttpResponseForbidden
from
django.views.decorators.csrf
import
csrf_exempt
from
djangosaml2.cache
import
IdentityCache
,
OutstandingQueriesCache
from
djangosaml2.conf
import
get_config
from
djangosaml2.signals
import
post_authenticated
from
djangosaml2.utils
import
get_custom_setting
from
saml2.client
import
Saml2Client
from
saml2.ident
import
code
from
saml2
import
BINDING_HTTP_POST
@require_POST
@csrf_exempt
def
circle_assertion_consumer_service
(
request
,
config_loader_path
=
None
,
attribute_mapping
=
None
,
create_unknown_user
=
None
):
"""SAML Authorization Response endpoint
The IdP will send its response to this view, which
will process it with pysaml2 help and log the user
in using the custom Authorization backend or redirect to 2fa
djangosaml2.backends.Saml2Backend that should be
enabled in the settings.py
"""
attribute_mapping
=
attribute_mapping
or
get_custom_setting
(
'SAML_ATTRIBUTE_MAPPING'
,
{
'uid'
:
(
'username'
,
)})
create_unknown_user
=
create_unknown_user
or
get_custom_setting
(
'SAML_CREATE_UNKNOWN_USER'
,
True
)
logger
.
debug
(
'Assertion Consumer Service started'
)
conf
=
get_config
(
config_loader_path
,
request
)
if
'SAMLResponse'
not
in
request
.
POST
:
return
HttpResponseBadRequest
(
'Couldn
\'
t find "SAMLResponse" in POST data.'
)
xmlstr
=
request
.
POST
[
'SAMLResponse'
]
client
=
Saml2Client
(
conf
,
identity_cache
=
IdentityCache
(
request
.
session
))
oq_cache
=
OutstandingQueriesCache
(
request
.
session
)
outstanding_queries
=
oq_cache
.
outstanding_queries
()
# process the authentication response
response
=
client
.
parse_authn_request_response
(
xmlstr
,
BINDING_HTTP_POST
,
outstanding_queries
)
if
response
is
None
:
logger
.
error
(
'SAML response is None'
)
return
HttpResponseBadRequest
(
"SAML response has errors. Please check the logs"
)
session_id
=
response
.
session_id
()
oq_cache
.
delete
(
session_id
)
# authenticate the remote user
session_info
=
response
.
session_info
()
if
callable
(
attribute_mapping
):
attribute_mapping
=
attribute_mapping
()
if
callable
(
create_unknown_user
):
create_unknown_user
=
create_unknown_user
()
logger
.
debug
(
'Trying to authenticate the user'
)
user
=
authenticate
(
session_info
=
session_info
,
attribute_mapping
=
attribute_mapping
,
create_unknown_user
=
create_unknown_user
)
if
user
is
None
:
logger
.
error
(
'The user is None'
)
return
HttpResponseForbidden
(
"Permission denied"
)
# redirect the user to the view where he came from
relay_state
=
request
.
POST
.
get
(
'RelayState'
,
'/'
)
if
not
relay_state
:
logger
.
warning
(
'The RelayState parameter exists but is empty'
)
relay_state
=
settings
.
LOGIN_REDIRECT_URL
logger
.
debug
(
'Redirecting to the RelayState: '
+
relay_state
)
if
user
.
profile
.
two_factor_secret
:
request
.
session
[
'two-fa-user'
]
=
user
.
pk
request
.
session
[
'two-fa-redirect'
]
=
relay_state
request
.
session
[
'login-type'
]
=
"saml2"
return
redirect
(
reverse
(
"two-factor-login"
))
else
:
login
(
request
,
user
)
def
_set_subject_id
(
session
,
subject_id
):
session
[
'_saml2_subject_id'
]
=
code
(
subject_id
)
_set_subject_id
(
request
.
session
,
session_info
[
'name_id'
])
logger
.
debug
(
'Sending the post_authenticated signal'
)
post_authenticated
.
send_robust
(
sender
=
user
,
session_info
=
session_info
)
# redirect the user to the view where he came from
return
redirect
(
relay_state
)
from
djangosaml2
import
views
as
saml2_views
saml2_views
.
assertion_consumer_service
=
circle_assertion_consumer_service
circle/dashboard/views/util.py
View file @
21207ef7
...
@@ -23,19 +23,28 @@ from collections import OrderedDict
...
@@ -23,19 +23,28 @@ from collections import OrderedDict
from
urlparse
import
urljoin
from
urlparse
import
urljoin
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib
import
messages
from
django.contrib.auth
import
REDIRECT_FIELD_NAME
,
login
as
auth_login
from
django.contrib.auth.forms
import
AuthenticationForm
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.views
import
redirect_to_login
from
django.contrib.sites.shortcuts
import
get_current_site
from
django.core
import
signing
from
django.core
import
signing
from
django.core.exceptions
import
PermissionDenied
,
SuspiciousOperation
from
django.core.exceptions
import
PermissionDenied
,
SuspiciousOperation
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.contrib
import
messages
from
django.contrib.auth.views
import
redirect_to_login
from
django.db.models
import
Q
from
django.db.models
import
Q
from
django.http
import
(
from
django.http
import
(
HttpResponse
,
Http404
,
HttpResponseRedirect
,
JsonResponse
HttpResponse
,
Http404
,
HttpResponseRedirect
,
JsonResponse
)
)
from
django.shortcuts
import
redirect
,
render
from
django.shortcuts
import
redirect
,
render
,
resolve_url
from
django.utils.http
import
is_safe_url
from
django.utils.translation
import
ugettext_lazy
as
_
,
ugettext_noop
from
django.utils.translation
import
ugettext_lazy
as
_
,
ugettext_noop
from
django.views.generic
import
DetailView
,
View
,
DeleteView
from
django.utils.decorators
import
method_decorator
from
django.views.decorators.cache
import
never_cache
from
django.views.decorators.csrf
import
csrf_protect
from
django.views.decorators.debug
import
sensitive_post_parameters
from
django.views.generic
import
DetailView
,
View
,
DeleteView
,
FormView
from
django.views.generic.detail
import
SingleObjectMixin
from
django.views.generic.detail
import
SingleObjectMixin
from
braces.views
import
LoginRequiredMixin
from
braces.views
import
LoginRequiredMixin
...
@@ -747,3 +756,61 @@ class DeleteViewBase(LoginRequiredMixin, DeleteView):
...
@@ -747,3 +756,61 @@ class DeleteViewBase(LoginRequiredMixin, DeleteView):
else
:
else
:
messages
.
success
(
request
,
self
.
success_message
)
messages
.
success
(
request
,
self
.
success_message
)
return
HttpResponseRedirect
(
self
.
get_success_url
())
return
HttpResponseRedirect
(
self
.
get_success_url
())
# only in Django 1.9
class
LoginView
(
FormView
):
"""
Displays the login form and handles the login action.
"""
form_class
=
AuthenticationForm
authentication_form
=
None
redirect_field_name
=
REDIRECT_FIELD_NAME
template_name
=
'registration/login.html'
redirect_authenticated_user
=
False
extra_context
=
None
@method_decorator
(
sensitive_post_parameters
())
@method_decorator
(
csrf_protect
)
@method_decorator
(
never_cache
)
def
dispatch
(
self
,
request
,
*
args
,
**
kwargs
):
if
(
self
.
redirect_authenticated_user
and
self
.
request
.
user
.
is_authenticated
):
redirect_to
=
self
.
get_success_url
()
if
redirect_to
==
self
.
request
.
path
:
raise
ValueError
(
"Redirection loop for authenticated user detected. Check "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return
HttpResponseRedirect
(
redirect_to
)
return
super
(
LoginView
,
self
)
.
dispatch
(
request
,
*
args
,
**
kwargs
)
def
get_success_url
(
self
):
"""Ensure the user-originating redirection URL is safe."""
redirect_to
=
self
.
request
.
POST
.
get
(
self
.
redirect_field_name
,
self
.
request
.
GET
.
get
(
self
.
redirect_field_name
,
''
)
)
if
not
is_safe_url
(
url
=
redirect_to
,
host
=
self
.
request
.
get_host
()):
return
resolve_url
(
settings
.
LOGIN_REDIRECT_URL
)
return
redirect_to
def
get_form_class
(
self
):
return
self
.
authentication_form
or
self
.
form_class
def
form_valid
(
self
,
form
):
"""Security check complete. Log the user in."""
auth_login
(
self
.
request
,
form
.
get_user
())
return
HttpResponseRedirect
(
self
.
get_success_url
())
def
get_context_data
(
self
,
**
kwargs
):
context
=
super
(
LoginView
,
self
)
.
get_context_data
(
**
kwargs
)
current_site
=
get_current_site
(
self
.
request
)
context
.
update
({
self
.
redirect_field_name
:
self
.
get_success_url
(),
'site'
:
current_site
,
'site_name'
:
current_site
.
name
,
})
if
self
.
extra_context
is
not
None
:
context
.
update
(
self
.
extra_context
)
return
context
circle/templates/registration/two-factor-login.html
0 → 100644
View file @
21207ef7
{% extends "registration/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title-page %}{% trans "Login" %}{% endblock %}
{% block content_box %}
<div
class=
"row"
>
<div
class=
"col-md-12"
>
<form
action=
""
method=
"POST"
>
{% csrf_token %}
{{ form.confirmation_code|as_crispy_field }}
<input
type=
"submit"
/>
</form>
</div>
</div>
<style>
.help-block
{
display
:
block
;
}
</style>
{% endblock %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment