Compare commits

...

7 Commits
master ... sso

Author SHA1 Message Date
Cayo Puigdefabregas 5a3ca10825 fix scope of authorization 2023-12-21 13:19:32 +01:00
Cayo Puigdefabregas addc4fe0f7 add oidc allow_code to views 2023-09-27 14:00:25 +02:00
Cayo Puigdefabregas df9c0c2361 add api call for validate with token 2023-09-27 13:59:30 +02:00
Cayo Puigdefabregas 8d3033c0ca add endpoint to urls 2023-09-27 13:58:58 +02:00
Cayo Puigdefabregas 010c30cc0c modify login template 2023-09-27 13:57:36 +02:00
Cayo Puigdefabregas 85843c42b8 add authlib to requirements 2023-09-27 13:57:09 +02:00
Cayo Puigdefabregas ba53e461e8 add variables to settings 2023-09-27 13:56:48 +02:00
7 changed files with 140 additions and 4 deletions

View File

@ -1,4 +1,9 @@
SECRET_KEY='$omeR@nd0mSecr3tKeyWith4V3ryL0ng$tring!?' SECRET_KEY='$omeR@nd0mSecr3tKeyWith4V3ryL0ng$tring!?'
DEBUG=True DEBUG=True
ALLOWED_HOSTS=.localhost,127.0.0.1 ALLOWED_HOSTS=.localhost,127.0.0.1
API_BASE_URL = 'https://api.examplea.org/' API_BASE_URL = 'http://localhost:9080/api/'
STATIC_ROOT = 'musician/static/'
CLIENT_ID = "ZjYHcERGfUKo26y41VLI4KHz"
CLIENT_SECRET = "jjUfJq9vOomJaj8Zm5W6OweMG61wQ5G3VyKhBzxLqp5k5HVW"
OIDC_PROVIDER = "http://localhost:9000"
DOMAIN = "http://localhost:8000"

View File

@ -14,6 +14,7 @@ TOKEN_PATH = '/api-token-auth/'
API_PATHS = { API_PATHS = {
# auth # auth
'token-auth': '/api-token-auth/', 'token-auth': '/api-token-auth/',
'token-auth-v2': '/api-token-auth-v2/',
'my-account': 'accounts/', 'my-account': 'accounts/',
# services # services
@ -37,14 +38,17 @@ API_PATHS = {
class Orchestra(object): class Orchestra(object):
def __init__(self, *args, username=None, password=None, **kwargs): def __init__(self, *args, username=None, password=None, token=None, **kwargs):
self.base_url = kwargs.pop('base_url', settings.API_BASE_URL) self.base_url = kwargs.pop('base_url', settings.API_BASE_URL)
self.username = username self.username = username
self.session = requests.Session() self.session = requests.Session()
self.auth_token = kwargs.pop("auth_token", None) self.auth_token = kwargs.pop("auth_token", None)
if self.auth_token is None: if self.auth_token is None:
self.auth_token = self.authenticate(self.username, password) if token:
self.auth_token = self.authenticate_token(token)
else:
self.auth_token = self.authenticate(self.username, password)
def build_absolute_uri(self, path_name): def build_absolute_uri(self, path_name):
path = API_PATHS.get(path_name, None) path = API_PATHS.get(path_name, None)
@ -63,6 +67,15 @@ class Orchestra(object):
return response.json().get("token", None) return response.json().get("token", None)
def authenticate_token(self, token):
url = self.build_absolute_uri('token-auth-v2')
response = self.session.post(
url,
data={"token": token},
)
return response.json().get("token", None)
def request(self, verb, resource=None, url=None, data=None, render_as="json", querystring=None, raise_exception=True): def request(self, verb, resource=None, url=None, data=None, render_as="json", querystring=None, raise_exception=True):
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"] assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
if resource is not None: if resource is not None:

View File

@ -64,6 +64,9 @@
</div> </div>
<!--/#login-content--> <!--/#login-content-->
<div id="login-footer"> <div id="login-footer">
{% if oidc_provider %}
<a href="{{ oidc_provider }}">{% trans "Identification with SSO" %}</a><br />
{% endif %}
<a href="#password_reset" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a> <a href="#password_reset" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
</div> </div>
</div><!-- /#login-wrapper --> </div><!-- /#login-wrapper -->

View File

@ -14,6 +14,7 @@ app_name = 'musician'
urlpatterns = [ urlpatterns = [
path('auth/login/', views.LoginView.as_view(), name='login'), path('auth/login/', views.LoginView.as_view(), name='login'),
path('auth/logout/', views.LogoutView.as_view(), name='logout'), path('auth/logout/', views.LogoutView.as_view(), name='logout'),
path('allow_code', views.AllowCodeView.as_view(), name='allow-code'),
path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'), path('domains/<int:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
path('billing/', views.BillingView.as_view(), name='billing'), path('billing/', views.BillingView.as_view(), name='billing'),

View File

@ -1,6 +1,8 @@
import logging import logging
import smtplib import smtplib
import datetime import datetime
import requests
import json
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@ -20,6 +22,7 @@ from django.views.generic.list import ListView
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from . import get_version from . import get_version
from . import api
from .auth import login as auth_login from .auth import login as auth_login
from .auth import logout as auth_logout from .auth import logout as auth_logout
from .forms import LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxUpdateForm, MailForm from .forms import LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxUpdateForm, MailForm
@ -33,6 +36,7 @@ from .utils import get_bootstraped_percent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView): class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
template_name = "musician/dashboard.html" template_name = "musician/dashboard.html"
extra_context = { extra_context = {
@ -535,6 +539,96 @@ class DomainDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
return domain return domain
class AllowCodeView(RedirectView):
"""
Log in the user with OAuth2.
"""
permanent = False
success_url = reverse_lazy('musician:dashboard')
userinfo = None
def get_token(self):
oidc_provider = settings.OIDC_PROVIDER.strip("/")
domain = settings.DOMAIN.strip("/")
url = f"{oidc_provider}/application/o/token/"
client_id = settings.CLIENT_ID
client_secret = settings.CLIENT_SECRET
self.code = self.request.GET.get('code')
data = {
'grant_type': 'authorization_code',
'code': self.code,
'redirect_uri': f"{domain}/allow_code",
}
auth = (client_id, client_secret)
msg = requests.post(url, data=data, auth=auth)
self.token = msg.text
def get_user_info(self):
# DELETE THIS METHOD IS ONLY A TEST
if self.userinfo:
return self.username
if 'error' in self.token:
return
if 'access_token' not in self.token:
return
if not isinstance(self.token, str):
return
self.token = json.loads(self.token)
oidc_provider = settings.OIDC_PROVIDER.strip("/")
url = f"{oidc_provider}/application/o/userinfo/"
access_token = self.token.get('access_token')
token_type = self.token.get('token_type', 'Bearer')
if not access_token or not token_type:
return
headers = {"Authorization": f"{token_type} {access_token}"}
msg = requests.get(url, headers=headers)
self.userinfo = json.loads(msg.text)
self.username = self.userinfo.get("username")
# import pdb; pdb.set_trace()
return self.username
def get(self, request, *args, **kwargs):
"""
Logs in the user.
"""
self.get_token()
# Delete this line
# self.get_user_info()
orchestra = api.Orchestra(token=self.token)
self.orchestra_token = orchestra.auth_token
self.user = orchestra.retrieve_profile()
username = self.user.username
auth_login(self.request, username, self.orchestra_token)
# set user language as active language
user_language = self.user.language
translation.activate(user_language)
response = HttpResponseRedirect(self.get_success_url())
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
return response
# return super().get(*args, **kwargs)
def get_success_url(self):
url = self.get_redirect_url()
return url or self.success_url
def get_redirect_url(self):
"""Return the user-originating redirect URL if it's safe."""
redirect_to = self.success_url
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts={self.request.get_host()},
require_https=self.request.is_secure(),
)
return redirect_to if url_is_safe else ''
class LoginView(FormView): class LoginView(FormView):
template_name = 'auth/login.html' template_name = 'auth/login.html'
form_class = LoginForm form_class = LoginForm
@ -551,6 +645,18 @@ class LoginView(FormView):
kwargs['request'] = self.request kwargs['request'] = self.request
return kwargs return kwargs
def get_oidc_url(self):
client_id = settings.CLIENT_ID
oidc_provider = settings.OIDC_PROVIDER.strip("/")
domain = settings.DOMAIN.strip("/")
if not client_id or not oidc_provider:
return
url = f'{oidc_provider}/application/o/authorize/?client_id={client_id}'
url += f'&scope=openid+musician&response_type=code&nonce=abc'
url += f'&redirect_uri={domain}/allow_code&response_type=code&nonce=abc'
return url
def form_valid(self, form): def form_valid(self, form):
"""Security check complete. Log the user in.""" """Security check complete. Log the user in."""
auth_login(self.request, form.username, form.token) auth_login(self.request, form.username, form.token)
@ -585,6 +691,7 @@ class LoginView(FormView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
self.redirect_field_name: self.get_redirect_url(), self.redirect_field_name: self.get_redirect_url(),
'oidc_provider': self.get_oidc_url(),
**(self.extra_context or {}) **(self.extra_context or {})
}) })
return context return context

View File

@ -1,4 +1,5 @@
django==2.2.27 # django==2.2.27
django==3.2
python-decouple==3.1 python-decouple==3.1
django-bootstrap4 django-bootstrap4
django-extensions django-extensions

View File

@ -175,6 +175,12 @@ URL_SAAS_OWNCLOUD = config('URL_SAAS_OWNCLOUD', None)
URL_SAAS_WORDPRESS = config('URL_SAAS_WORDPRESS', None) URL_SAAS_WORDPRESS = config('URL_SAAS_WORDPRESS', None)
CLIENT_ID = config('CLIENT_ID')
CLIENT_SECRET = config('CLIENT_SECRET')
OIDC_PROVIDER = config('OIDC_PROVIDER')
DOMAIN = config('DOMAIN')
# Managers: who should get notifications about services changes that # Managers: who should get notifications about services changes that
# may require human actions (e.g. deleted mailboxes) # may require human actions (e.g. deleted mailboxes)