Parents
Children
Initial revision bascially just configuration at this point
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Sat May 23 06:01:04 2020 -0500
@@ -0,0 +1,5 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rboidc/extension.py Sat May 23 06:01:04 2020 -0500
@@ -0,0 +1,29 @@
+from __future__ import unicode_literals +from django.utils.translation import ugettext_lazy as _ +from reviewboard.extensions.base import Extension +from reviewboard.extensions.hooks import AuthBackendHook, TemplateHook +from rboidc.oidc import OIDCAuthBackend +class RbOIDCExtension(Extension): + 'Name': _('OpenID Connect Authentication'), + 'Summary': _('Support for any OIDC compliant issuer'), + AuthBackendHook(self, OIDCAuthBackend) + from django.conf import settings + name='before-login-form', + template_name='oidc-links.html', --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rboidc/oidc.py Sat May 23 06:01:04 2020 -0500
@@ -0,0 +1,172 @@
+from __future__ import unicode_literals + from urllib.parse import urljoin + from urlparse import urljoin +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +from djblets.siteconfig.forms import SiteSettingsForm +from reviewboard.accounts.backends import AuthBackend +class OIDCSettingsForm(SiteSettingsForm): + """ OIDC Settings Form """ + auth_oidc_issuer_url = forms.CharField( + help_text=_('The URL of the OIDC provider.'), + widget=forms.TextInput(attrs={'size': '40'}), + auth_oidc_client_id = forms.CharField( + help_text=_('The client ID that the issuer provided you with.'), + widget=forms.TextInput(attrs={'size': '40'}), + auth_oidc_client_secret = forms.CharField( + label=_('Client Secret'), + help_text=_('The client secret that the issuer provided you with.'), + widget=forms.PasswordInput(render_value=True, attrs={'size': '40'}), + auth_oidc_scopes = forms.CharField( + help_text=_('A comma separated list scopes that you are requesting ' + widget=forms.TextInput(attrs={'size': '40'}), + auth_oidc_authorization_endpoint = forms.CharField( + label=_('Authorization Endpoint'), + help_text=_('The endpoint to authorize users.'), + widget=forms.TextInput(attrs={'size': '40'}), + auth_oidc_token_endpoint = forms.CharField( + label=_('Token Endpoint'), + help_text=_('The endpoint to request tokens.'), + widget=forms.TextInput(attrs={'size': '40'}), + auth_oidc_userinfo_endpoint = forms.CharField( + label=_('Userinfo Endpoint'), + help_text=_('The endpoint to get information about the user.'), + widget=forms.TextInput(attrs={'size': '40'}), + def clean_auth_oidc_issuer_url(self): + url = self.cleaned_data.get('auth_oidc_issuer_url') + # Remove a trailing slash to make it easier to check if we already have + # the .well-known path. + if url.endswith('/.well-known/openid-configuration'): + # Put a / back in case the provider as a path element to it. + if not url.endswith('/'): + return urljoin(url, '.well-known/openid-configuration') + def clean_auth_oidc_scopes(self): + scopes = self.cleaned_data.get('auth_oidc_scopes') + cleaned = [x.strip() for x in scopes.split(',') if x.strip() != ''] + return ','.join(cleaned) + """ Validate the settings the user provided. """ + self.cleaned_data = super(OIDCSettingsForm, self).clean() + # Try to discover our endpoints from the endpoints from the issuer url. + issuer_url = self.cleaned_data['auth_oidc_issuer_url'] + resp = requests.get(issuer_url) + # The OIDC Discovery spec says it *HAS* to respond with a 200 OK. + if resp.status_code != 200: + raise ValidationError(_('%s responsed with %d' % (issuer_url, + raise ValidationError(_('%s did not return a JSON document' % + # grab and validate the authorization_endpoint + endpoint = data.get('authorization_endpoint', None) + raise ValidationError(_('%s did not provide an ' + 'authorization_endpoint' % issuer_url)) + self.cleaned_data['auth_oidc_authorization_endpoint'] = endpoint + # grab and validate the token_endpoint + endpoint = data.get('token_endpoint', None) + raise ValidationError(_('%s did not provide a token_endpoint' % + self.cleaned_data['auth_oidc_token_endpoint'] = endpoint + # grab and validate the userinfo_endpoint + endpoint = data.get('userinfo_endpoint', None) + raise ValidationError(_('%s did not provide an userinfo_endpoint' % + self.cleaned_data['auth_oidc_userinfo_endpoint'] = endpoint + # Now make sure all the scopes we're looking for are supported. + scopes = self.cleaned_data.get('auth_oidc_scopes') + for scope in scopes.split(','): + if scope not in data['scopes_supported']: + raise ValidationError(_('%s does not support scope %s' % + return self.cleaned_data + title = _('OpenID Connect Settings') + 'fields': ('auth_oidc_issuer_url', 'auth_oidc_client_id', + 'auth_oidc_client_secret', 'auth_oidc_scopes'), + (_('Discovered Endpoints'), { + 'fields': ('auth_oidc_authorization_endpoint', + 'auth_oidc_token_endpoint', + 'auth_oidc_userinfo_endpoint'), +class OIDCAuthBackend(AuthBackend): + backend_id = 'grim-oidc' + name = _('OpenID Connect') + settings_form = OIDCSettingsForm + def authenticate(self, username, password): + def get_or_create_user(self, username, request): --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rboidc/templates/oidc-links.html Sat May 23 06:01:04 2020 -0500
@@ -0,0 +1,3 @@
+<a href="#">Login with OIDC</a> +<div style="margin-left: 25%; margin-right: 25%; "><hr/></div> --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py Sat May 23 06:01:04 2020 -0500
@@ -0,0 +1,32 @@
+from __future__ import unicode_literals +from reviewboard.extensions.packaging import setup +from setuptools import find_packages + author='Gary Kramlich', + author_email='grim@reaperworld.com', + url='https://keep.imfreedom.org/grim/rb-oidc', + description='OpenID Connect for ReviewBoard', + packages=find_packages(), + 'reviewboard.extensions': [ + 'rboidc = rboidc.extension:RbOIDCExtension', + 'Development Status :: 3 - Alpha', + 'Environment :: Web Framework', + 'Framework :: Review Board', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',