diff --git a/passbook/core/templates/base/skeleton_login.html b/passbook/core/templates/base/skeleton_login.html new file mode 100644 index 000000000..a747775c9 --- /dev/null +++ b/passbook/core/templates/base/skeleton_login.html @@ -0,0 +1,54 @@ +{% load static %} +{% load i18n %} +{% load utils %} + + + + + + + + + {% block title %} + {% title %} + {% endblock %} + + + + + + + + {% block head %} + {% endblock %} + + + + {% if 'impersonate_id' in request.session %} +
+ + {% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %} + {% trans 'Stop impersonation' %} + +
+ {% endif %} + {% block body %} + {% endblock %} + + + + + {% block scripts %} + {% endblock %} +
+ {% include 'partials/about_modal.html' %} +
+ + + diff --git a/passbook/core/templates/login/base.html b/passbook/core/templates/login/base.html index fa9f79215..7450204ff 100644 --- a/passbook/core/templates/login/base.html +++ b/passbook/core/templates/login/base.html @@ -1,4 +1,4 @@ -{% extends 'base/skeleton.html' %} +{% extends 'base/skeleton_login.html' %} {% load static %} {% load i18n %} diff --git a/passbook/oauth_provider/forms.py b/passbook/oauth_provider/forms.py index 1da6ee820..895291ff6 100644 --- a/passbook/oauth_provider/forms.py +++ b/passbook/oauth_provider/forms.py @@ -1,4 +1,4 @@ -"""passbook OAuth2 IDP Forms""" +"""passbook OAuth2 Provider Forms""" from django import forms diff --git a/passbook/oauth_provider/models.py b/passbook/oauth_provider/models.py index a318eb88e..9dff16202 100644 --- a/passbook/oauth_provider/models.py +++ b/passbook/oauth_provider/models.py @@ -25,8 +25,6 @@ class OAuth2Provider(Provider, AbstractApplication): reverse('passbook_oauth_provider:token')), 'userinfo_url': request.build_absolute_uri( reverse('passbook_api:openid')), - 'openid_url': request.build_absolute_uri( - reverse('passbook_oauth_provider:openid-discovery')) } class Meta: diff --git a/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html b/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html index b17f9ed7f..902107626 100644 --- a/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html +++ b/passbook/oauth_provider/templates/oauth2_provider/setup_url_modal.html @@ -31,19 +31,10 @@ -
-
-
- -
- -
-
-
- \ No newline at end of file + diff --git a/passbook/oauth_provider/urls.py b/passbook/oauth_provider/urls.py index 5badf8aa3..1fd14cc2d 100644 --- a/passbook/oauth_provider/urls.py +++ b/passbook/oauth_provider/urls.py @@ -3,7 +3,7 @@ from django.urls import path from oauth2_provider import views -from passbook.oauth_provider.views import oauth2, openid +from passbook.oauth_provider.views import oauth2 urlpatterns = [ # Custom OAuth 2 Authorize View @@ -17,9 +17,4 @@ urlpatterns = [ path("token/", views.TokenView.as_view(), name="token"), path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"), path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"), - # OpenID-Connect Discovery - path('.well-known/openid-configuration', openid.OpenIDConfigurationView.as_view(), - name='openid-discovery'), - path('.well-known/jwks.json', openid.JSONWebKeyView.as_view(), - name='openid-jwks'), ] diff --git a/passbook/oauth_provider/views/oauth2.py b/passbook/oauth_provider/views/oauth2.py index a582f57a5..eae14a028 100644 --- a/passbook/oauth_provider/views/oauth2.py +++ b/passbook/oauth_provider/views/oauth2.py @@ -57,10 +57,10 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView): provider.save() self._application = application # Check permissions - passing, policy_meaages = self.user_has_access(self._application, request.user) + passing, policy_messages = self.user_has_access(self._application, request.user) if not passing: - for policy_meaage in policy_meaages: - messages.error(request, policy_meaage) + for policy_message in policy_messages: + messages.error(request, policy_message) return redirect('passbook_oauth_provider:oauth2-permission-denied') # Some clients don't pass response_type, so we default to code if 'response_type' not in request.GET: diff --git a/passbook/oauth_provider/views/openid.py b/passbook/oauth_provider/views/openid.py deleted file mode 100644 index 5b72b40a0..000000000 --- a/passbook/oauth_provider/views/openid.py +++ /dev/null @@ -1,35 +0,0 @@ -"""passbook oauth provider OpenID Views""" - -from django.http import HttpRequest, JsonResponse -from django.shortcuts import reverse -from django.views.generic import View - - -class OpenIDConfigurationView(View): - """Return OpenID Configuration""" - - def get_issuer_url(self, request): - """Get correct issuer URL""" - full_url = request.build_absolute_uri(reverse('passbook_oauth_provider:openid-discovery')) - return full_url.replace(".well-known/openid-configuration", "") - - def get(self, request: HttpRequest): - """Get Response conform to https://openid.net/specs/openid-connect-discovery-1_0.html""" - return JsonResponse({ - 'issuer': self.get_issuer_url(request), - 'authorization_endpoint': request.build_absolute_uri( - reverse('passbook_oauth_provider:oauth2-authorize')), - 'token_endpoint': request.build_absolute_uri(reverse('passbook_oauth_provider:token')), - "jwks_uri": request.build_absolute_uri(reverse('passbook_oauth_provider:openid-jwks')), - "scopes_supported": [ - "openid", - ], - }) - - -class JSONWebKeyView(View): - """JSON Web Key View""" - - def get(self, request: HttpRequest): - """JSON Webkeys are not implemented yet, hence return an empty object""" - return JsonResponse({}) diff --git a/passbook/oidc_provider/__init__.py b/passbook/oidc_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/oidc_provider/apps.py b/passbook/oidc_provider/apps.py new file mode 100644 index 000000000..c6066356b --- /dev/null +++ b/passbook/oidc_provider/apps.py @@ -0,0 +1,27 @@ +"""passbook auth oidc provider app config""" +from logging import getLogger + +from django.apps import AppConfig +from django.urls import include, path + +LOGGER = getLogger(__name__) + +class PassbookOIDCProviderConfig(AppConfig): + """passbook auth oidc provider app config""" + + name = 'passbook.oidc_provider' + label = 'passbook_oidc_provider' + verbose_name = 'passbook OIDC Provider' + + def ready(self): + from Cryptodome.PublicKey import RSA + from oidc_provider.models import RSAKey + if not RSAKey.objects.exists(): + key = RSA.generate(2048) + rsakey = RSAKey(key=key.exportKey('PEM').decode('utf8')) + rsakey.save() + LOGGER.info("Created key") + from passbook.root import urls + urls.urlpatterns.append( + path('application/oidc/', include('oidc_provider.urls', namespace='oidc_provider')), + ) diff --git a/passbook/oidc_provider/forms.py b/passbook/oidc_provider/forms.py new file mode 100644 index 000000000..4b2f84ca2 --- /dev/null +++ b/passbook/oidc_provider/forms.py @@ -0,0 +1,38 @@ +"""passbook OIDC IDP Forms""" + +from django import forms +from oauth2_provider.generators import (generate_client_id, + generate_client_secret) +from oidc_provider.models import Client + +from passbook.oidc_provider.models import OpenIDProvider + + +class OIDCProviderForm(forms.ModelForm): + """OpenID Client form""" + + def __init__(self, *args, **kwargs): + # Correctly load data from 1:1 rel + if 'instance' in kwargs: + kwargs['instance'] = kwargs['instance'].oidc_client + super().__init__(*args, **kwargs) + self.fields['client_id'].initial = generate_client_id() + self.fields['client_secret'].initial = generate_client_secret() + + def save(self, *args, **kwargs): + response = super().save(*args, **kwargs) + # Check if openidprovider class instance exists + if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists(): + OpenIDProvider.objects.create(oidc_client=self.instance) + return response + + class Meta: + model = Client + fields = [ + 'name', 'client_type', 'client_id', 'client_secret', 'response_types', + 'jwt_alg', 'reuse_consent', 'require_consent', '_redirect_uris', '_scope' + ] + # exclude = ['owner', 'website_url', 'terms_url', 'contact_email', 'logo', ] + labels = { + 'client_secret': "Client Secret" + } diff --git a/passbook/oidc_provider/lib.py b/passbook/oidc_provider/lib.py new file mode 100644 index 000000000..07c17edcd --- /dev/null +++ b/passbook/oidc_provider/lib.py @@ -0,0 +1,30 @@ +"""OIDC Permission checking""" +from logging import getLogger + +from django.contrib import messages +from django.shortcuts import redirect + +from passbook.core.models import Application +from passbook.core.policies import PolicyEngine + +LOGGER = getLogger(__name__) + +def check_permissions(request, user, client): + """Check permissions, used for + https://django-oidc-provider.readthedocs.io/en/latest/ + sections/settings.html#oidc-after-userlogin-hook""" + try: + application = client.openidprovider.application + except Application.DoesNotExist: + return redirect('passbook_oauth_provider:oauth2-permission-denied') + LOGGER.debug("Checking permissions of %s on application %s...", user, application) + policy_engine = PolicyEngine(application.policies.all()) + policy_engine.for_user(user).with_request(request).build() + + # Check permissions + passing, policy_messages = policy_engine.result + if not passing: + for policy_message in policy_messages: + messages.error(request, policy_message) + return redirect('passbook_oauth_provider:oauth2-permission-denied') + return None diff --git a/passbook/oidc_provider/migrations/0001_initial.py b/passbook/oidc_provider/migrations/0001_initial.py new file mode 100644 index 000000000..0e810eed7 --- /dev/null +++ b/passbook/oidc_provider/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.3 on 2019-07-05 12:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('oidc_provider', '0026_client_multiple_response_types'), + ('passbook_core', '0024_ssologinpolicy'), + ] + + operations = [ + migrations.CreateModel( + name='OpenIDProvider', + fields=[ + ('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')), + ('oidc_client', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client')), + ], + bases=('passbook_core.provider',), + ), + ] diff --git a/passbook/oidc_provider/migrations/__init__.py b/passbook/oidc_provider/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/oidc_provider/models.py b/passbook/oidc_provider/models.py new file mode 100644 index 000000000..091b1ef8b --- /dev/null +++ b/passbook/oidc_provider/models.py @@ -0,0 +1,45 @@ +"""oidc models""" +from django.db import models +from django.shortcuts import reverse +from django.utils.translation import gettext as _ +from oidc_provider.models import Client + +from passbook.core.models import Provider + + +class OpenIDProvider(Provider): + """Proxy model for OIDC Client""" + # Since oidc_provider doesn't currently support swappable models + # (https://github.com/juanifioren/django-oidc-provider/pull/305) + # we have a 1:1 relationship, and update oidc_client when the form is saved. + + oidc_client = models.OneToOneField(Client, on_delete=models.CASCADE) + + form = 'passbook.oidc_provider.forms.OIDCProviderForm' + + @property + def name(self): + """Name property for UI""" + return self.oidc_client.name + + def __str__(self): + return "OpenID Connect Provider %s" % self.oidc_client.__str__() + + def html_setup_urls(self, request): + """return template and context modal with URLs for authorize, token, openid-config, etc""" + return "oidc_provider/setup_url_modal.html", { + 'provider': self, + 'authorize': request.build_absolute_uri( + reverse('oidc_provider:authorize')), + 'token': request.build_absolute_uri( + reverse('oidc_provider:token')), + 'userinfo': request.build_absolute_uri( + reverse('oidc_provider:userinfo')), + 'provider_info': request.build_absolute_uri( + reverse('oidc_provider:provider-info')), + } + + class Meta: + + verbose_name = _('OpenID Provider') + verbose_name_plural = _('OpenID Providers') diff --git a/passbook/oidc_provider/requirements.txt b/passbook/oidc_provider/requirements.txt new file mode 100644 index 000000000..615ae1804 --- /dev/null +++ b/passbook/oidc_provider/requirements.txt @@ -0,0 +1 @@ +django-oidc-provider diff --git a/passbook/oidc_provider/settings.py b/passbook/oidc_provider/settings.py new file mode 100644 index 000000000..dfde0f058 --- /dev/null +++ b/passbook/oidc_provider/settings.py @@ -0,0 +1,7 @@ +"""passbook OIDC Provider""" + +INSTALLED_APPS = [ + 'oidc_provider', +] + +OIDC_AFTER_USERLOGIN_HOOK = "passbook.oidc_provider.lib.check_permissions" diff --git a/passbook/oidc_provider/templates/oidc_provider/authorize.html b/passbook/oidc_provider/templates/oidc_provider/authorize.html new file mode 100644 index 000000000..d6392bf1e --- /dev/null +++ b/passbook/oidc_provider/templates/oidc_provider/authorize.html @@ -0,0 +1,70 @@ +{% extends "login/base.html" %} + +{% load utils %} +{% load i18n %} + +{% block title %} +{% title 'Authorize Application' %} +{% endblock %} + +{% block card %} +
+

{% trans 'Authorize Application' %}

+
+
+ {% csrf_token %} + {% if not error %} + {% csrf_token %} + {% for field in form %} + {% if field.is_hidden %} + {{ field }} + {% endif %} + {% endfor %} +
+

+ {% blocktrans with remote=client.name %} + You're about to sign into {{ remote }} + {% endblocktrans %} +

+

{% trans "Application requires following permissions" %}

+ + {{ hidden_inputs }} + {{ form.errors }} + {{ form.non_field_errors }} +

+ {% blocktrans with user=user %} + You are logged in as {{ user }}. Not you? + {% endblocktrans %} + {% trans 'Logout' %} +

+ + +
+ {% else %} +
+

+ {% blocktrans with err=error.error %}Error: {{ err }}{% endblocktrans %} +

+

{{ error.description }}

+
+ {% endif %} +
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/passbook/oidc_provider/templates/oidc_provider/setup_url_modal.html b/passbook/oidc_provider/templates/oidc_provider/setup_url_modal.html new file mode 100644 index 000000000..46ba06b3e --- /dev/null +++ b/passbook/oidc_provider/templates/oidc_provider/setup_url_modal.html @@ -0,0 +1,49 @@ +{% load i18n %} + + + diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 9115b31e5..0f87ecba1 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -82,6 +82,7 @@ INSTALLED_APPS = [ 'passbook.ldap.apps.PassbookLdapConfig', 'passbook.oauth_client.apps.PassbookOAuthClientConfig', 'passbook.oauth_provider.apps.PassbookOAuthProviderConfig', + 'passbook.oidc_provider.apps.PassbookOIDCProviderConfig', 'passbook.saml_idp.apps.PassbookSAMLIDPConfig', 'passbook.otp.apps.PassbookOTPConfig', 'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', diff --git a/requirements.txt b/requirements.txt index c92c9854f..8f2ab6fd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ -r passbook/admin/requirements.txt -r passbook/api/requirements.txt -r passbook/app_gw/requirements.txt +-r passbook/oidc_provider/requirements.txt