diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..faded83 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Intelie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/oidc_auth/apps.py b/oidc_auth/apps.py new file mode 100644 index 0000000..b67feb8 --- /dev/null +++ b/oidc_auth/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class OidcAuthConfig(AppConfig): + name = 'oidc_auth' diff --git a/oidc_auth/migrations/0001_initial.py b/oidc_auth/migrations/0001_initial.py index 9868394..2812d4b 100644 --- a/oidc_auth/migrations/0001_initial.py +++ b/oidc_auth/migrations/0001_initial.py @@ -1,124 +1,52 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Nonce' - db.create_table(u'oidc_auth_nonce', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('issuer_url', self.gf('django.db.models.fields.URLField')(max_length=200)), - ('state', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), - ('redirect_url', self.gf('django.db.models.fields.CharField')(max_length=100)), - )) - db.send_create_signal(u'oidc_auth', ['Nonce']) - - # Adding model 'OpenIDProvider' - db.create_table(u'oidc_auth_openidprovider', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('issuer', self.gf('django.db.models.fields.URLField')(unique=True, max_length=200)), - ('authorization_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), - ('token_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), - ('userinfo_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), - ('jwks_uri', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)), - ('signing_alg', self.gf('django.db.models.fields.CharField')(default='RS256', max_length=5)), - ('client_id', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('client_secret', self.gf('django.db.models.fields.CharField')(max_length=255)), - )) - db.send_create_signal(u'oidc_auth', ['OpenIDProvider']) - - # Adding model 'OpenIDUser' - db.create_table(u'oidc_auth_openiduser', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('sub', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), - ('issuer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oidc_auth.OpenIDProvider'])), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='oidc_account', unique=True, to=orm['auth.User'])), - ('access_token', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('refresh_token', self.gf('django.db.models.fields.CharField')(max_length=255)), - )) - db.send_create_signal(u'oidc_auth', ['OpenIDUser']) - - - def backwards(self, orm): - # Deleting model 'Nonce' - db.delete_table(u'oidc_auth_nonce') - - # Deleting model 'OpenIDProvider' - db.delete_table(u'oidc_auth_openidprovider') - - # Deleting model 'OpenIDUser' - db.delete_table(u'oidc_auth_openiduser') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'oidc_auth.nonce': { - 'Meta': {'object_name': 'Nonce'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'issuer_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), - 'redirect_url': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'state': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) - }, - u'oidc_auth.openidprovider': { - 'Meta': {'object_name': 'OpenIDProvider'}, - 'authorization_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}), - 'client_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'client_secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'issuer': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}), - 'jwks_uri': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), - 'signing_alg': ('django.db.models.fields.CharField', [], {'default': "'RS256'", 'max_length': '5'}), - 'token_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}), - 'userinfo_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}) - }, - u'oidc_auth.openiduser': { - 'Meta': {'object_name': 'OpenIDUser'}, - 'access_token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['oidc_auth.OpenIDProvider']"}), - 'refresh_token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'sub': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oidc_account'", 'unique': 'True', 'to': u"orm['auth.User']"}) - } - } - - complete_apps = ['oidc_auth'] \ No newline at end of file +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Nonce', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('issuer_url', models.URLField()), + ('state', models.CharField(unique=True, max_length=255)), + ('redirect_url', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='OpenIDProvider', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('issuer', models.URLField(unique=True)), + ('authorization_endpoint', models.URLField()), + ('token_endpoint', models.URLField()), + ('userinfo_endpoint', models.URLField()), + ('jwks_uri', models.URLField(null=True, blank=True)), + ('signing_alg', models.CharField(default=b'HS256', max_length=5, choices=[(b'RS256', b'RS256'), (b'HS256', b'HS256')])), + ('client_id', models.CharField(max_length=255)), + ('client_secret', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='OpenIDUser', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('sub', models.CharField(unique=True, max_length=255)), + ('access_token', models.CharField(max_length=255)), + ('refresh_token', models.CharField(max_length=255)), + ('issuer', models.ForeignKey(to='oidc_auth.OpenIDProvider', + on_delete=models.PROTECT)), + ('user', models.OneToOneField(related_name='oidc_account', + on_delete=models.PROTECT, + to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/oidc_auth/models.py b/oidc_auth/models.py index c65ffc9..97d0270 100644 --- a/oidc_auth/models.py +++ b/oidc_auth/models.py @@ -1,7 +1,7 @@ import string import random import json -from urlparse import urljoin +from urllib.parse import urljoin import requests from django.db import models, IntegrityError from django.conf import settings @@ -196,9 +196,15 @@ def get_default_provider(): class OpenIDUser(models.Model): sub = models.CharField(max_length=255, unique=True) - issuer = models.ForeignKey(OpenIDProvider) - user = models.OneToOneField(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), - related_name='oidc_account') + issuer = models.ForeignKey( + OpenIDProvider, + on_delete=models.PROTECT + ) + user = models.OneToOneField( + getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), + related_name='oidc_account', + on_delete=models.PROTECT + ) access_token = models.CharField(max_length=255) refresh_token = models.CharField(max_length=255) @@ -234,18 +240,21 @@ def get_or_create(cls, id_token, access_token, refresh_token, provider): log.debug('User with username %s not found locally, ' 'so it will be created' % id_token['sub']) - claims = cls._get_userinfo(provider, id_token['sub'], - access_token, refresh_token) - user = UserModel() - user.username = claims['preferred_username'] - user.email = claims['email'] - user.first_name = claims['given_name'] - user.last_name = claims['family_name'] - user.set_unusable_password() + # Always update user's local data + claims = cls._get_userinfo(provider, id_token['sub'], + access_token, refresh_token) + + user.username = claims['preferred_username'] + user.email = claims['email'] + user.first_name = claims['given_name'] + user.last_name = claims['family_name'] + user.is_superuser = claims.get('is_superuser', False) + user.is_staff = claims.get('is_staff', False) - user.save() + user.set_unusable_password() + user.save() # Avoid duplicate user key try: @@ -265,7 +274,7 @@ def get_or_create(cls, id_token, access_token, refresh_token, provider): user=user, access_token=access_token, refresh_token=refresh_token) @classmethod - def _get_userinfo(self, provider, sub, access_token, refresh_token): + def _get_userinfo(cls, provider, sub, access_token, refresh_token): # TODO encapsulate this? log.debug('Requesting userinfo in %s. sub: %s, access_token: %s' % ( provider.userinfo_endpoint, sub, access_token)) @@ -282,7 +291,7 @@ def _get_userinfo(self, provider, sub, access_token, refresh_token): raise errors.InvalidUserInfo() name = '%s %s' % (claims['given_name'], claims['family_name']) - log.debug('userinfo of sub: %s -> name: %s, preferred_username: %s, email: %s' % (sub, + log.debug('t userinfo of sub: %s -> name: %s, preferred_username: %s, email: %s' % (sub, name, claims['preferred_username'], claims['email'])) return claims diff --git a/oidc_auth/south_migrations/0001_initial.py b/oidc_auth/south_migrations/0001_initial.py new file mode 100644 index 0000000..00e02c7 --- /dev/null +++ b/oidc_auth/south_migrations/0001_initial.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Nonce' + db.create_table(u'oidc_auth_nonce', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('issuer_url', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('state', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('redirect_url', self.gf('django.db.models.fields.CharField')(max_length=100)), + )) + db.send_create_signal(u'oidc_auth', ['Nonce']) + + # Adding model 'OpenIDProvider' + db.create_table(u'oidc_auth_openidprovider', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('issuer', self.gf('django.db.models.fields.URLField')(unique=True, max_length=200)), + ('authorization_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('token_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('userinfo_endpoint', self.gf('django.db.models.fields.URLField')(max_length=200)), + ('jwks_uri', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)), + ('signing_alg', self.gf('django.db.models.fields.CharField')(default='HS256', max_length=5)), + ('client_id', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('client_secret', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal(u'oidc_auth', ['OpenIDProvider']) + + # Adding model 'OpenIDUser' + db.create_table(u'oidc_auth_openiduser', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('sub', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), + ('issuer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oidc_auth.OpenIDProvider'])), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='oidc_account', unique=True, to=orm['auth.User'])), + ('access_token', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('refresh_token', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal(u'oidc_auth', ['OpenIDUser']) + + + def backwards(self, orm): + # Deleting model 'Nonce' + db.delete_table(u'oidc_auth_nonce') + + # Deleting model 'OpenIDProvider' + db.delete_table(u'oidc_auth_openidprovider') + + # Deleting model 'OpenIDUser' + db.delete_table(u'oidc_auth_openiduser') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'oidc_auth.nonce': { + 'Meta': {'object_name': 'Nonce'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'redirect_url': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'state': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'oidc_auth.openidprovider': { + 'Meta': {'object_name': 'OpenIDProvider'}, + 'authorization_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'client_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'client_secret': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.URLField', [], {'unique': 'True', 'max_length': '200'}), + 'jwks_uri': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'signing_alg': ('django.db.models.fields.CharField', [], {'default': "'HS256'", 'max_length': '5'}), + 'token_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'userinfo_endpoint': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + u'oidc_auth.openiduser': { + 'Meta': {'object_name': 'OpenIDUser'}, + 'access_token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issuer': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['oidc_auth.OpenIDProvider']"}), + 'refresh_token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'sub': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oidc_account'", 'unique': 'True', 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['oidc_auth'] \ No newline at end of file diff --git a/oidc_auth/south_migrations/__init__.py b/oidc_auth/south_migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc_auth/tests/test_models.py b/oidc_auth/tests/test_models.py index c53bdcc..86322a4 100644 --- a/oidc_auth/tests/test_models.py +++ b/oidc_auth/tests/test_models.py @@ -3,7 +3,8 @@ from nose import tools from .utils import OIDCTestCase -from oidc_auth.models import OpenIDProvider, get_default_provider +from django.contrib.auth.models import User +from oidc_auth.models import OpenIDProvider, get_default_provider, OpenIDUser from oidc_auth.settings import oidc_settings @@ -87,3 +88,80 @@ class Foo(object): pass foo.__dict__.update(update_args) return foo + + +class TestOpenIDUser(OIDCTestCase): + @mock.patch('requests.get') + def test_create_new_superuser(self, get_mock): + get_mock.return_value = self.response_mock + provider = OpenIDProvider.discover(issuer=self.issuer) + + with mock.patch.object(OpenIDUser, '_get_userinfo') as get_userinfo: + get_userinfo.return_value = { + 'preferred_username': 'admin', + 'email': 'admin@admin.com', + 'given_name': 'foo', + 'family_name': 'bar', + 'is_superuser': True, + 'is_staff': True + } + + oidc_user = OpenIDUser.get_or_create( + id_token={'sub': 'admin'}, + access_token='foo', + refresh_token='bar', + provider=provider + ) + + user = User.objects.get(username='admin') + self.assertTrue(user.is_superuser) + self.assertTrue(user.is_staff) + + @mock.patch('requests.get') + def test_override_data_existing_user(self, get_mock): + get_mock.return_value = self.response_mock + provider = OpenIDProvider.discover(issuer=self.issuer) + user = User.objects.create_superuser('admin', 'admin@admin.com', 'admin password') + + with mock.patch.object(OpenIDUser, '_get_userinfo') as get_userinfo: + get_userinfo.return_value = { + 'preferred_username': 'admin', + 'email': 'new_admin@admin.com', + 'given_name': 'foo', + 'family_name': 'bar' + } + + oidc_user = OpenIDUser.get_or_create( + id_token={'sub': 'admin'}, + access_token='foo', + refresh_token='bar', + provider=provider + ) + + user = User.objects.get(username='admin') + self.assertEqual(user.email, 'new_admin@admin.com') + + @mock.patch('requests.get') + def test_revoke_permissions_existing_user(self, get_mock): + get_mock.return_value = self.response_mock + provider = OpenIDProvider.discover(issuer=self.issuer) + user = User.objects.create_superuser('admin', 'admin@admin.com', 'admin password') + + with mock.patch.object(OpenIDUser, '_get_userinfo') as get_userinfo: + get_userinfo.return_value = { + 'preferred_username': 'admin', + 'email': 'admin@admin.com', + 'given_name': 'foo', + 'family_name': 'bar' + } + + oidc_user = OpenIDUser.get_or_create( + id_token={'sub': 'admin'}, + access_token='foo', + refresh_token='bar', + provider=provider + ) + + user = User.objects.get(username='admin') + self.assertFalse(user.is_superuser) + self.assertFalse(user.is_staff) diff --git a/oidc_auth/tests/test_views.py b/oidc_auth/tests/test_views.py index 13dec03..fdc461d 100644 --- a/oidc_auth/tests/test_views.py +++ b/oidc_auth/tests/test_views.py @@ -1,13 +1,13 @@ from urlparse import urlparse, parse_qs from django.conf import settings from django.contrib.auth import get_user_model -from django.utils.importlib import import_module +from importlib import import_module from django.test import Client from nose import tools import mock from .utils import OIDCTestCase -from oidc_auth.models import OpenIDProvider, Nonce +from oidc_auth.models import OpenIDProvider, Nonce, OpenIDUser from oidc_auth.settings import oidc_settings UserModel = get_user_model() @@ -26,6 +26,7 @@ def test_get_login(self): response = self.client.get('/oidc/login/') tools.assert_equal(response.status_code, 200) + print('Templates:', response.templates) tools.assert_true(any(t.name == 'oidc/login.html' for t in response.templates)) @mock.patch('requests.get') @@ -95,7 +96,8 @@ def test_post_token_endpoint(self, post_mock): 'refresh_token': '12345', 'expires_in': 3600, 'token_type': 'Bearer', - 'id_token': '12345' + 'id_token': '12345', + 'sub': 'foobar' } post_mock.return_value = response @@ -110,6 +112,7 @@ def test_post_token_endpoint(self, post_mock): jwks_uri='http://a.b/jwks') user = UserModel.objects.create(username='foobar') + OpenIDUser.objects.create(sub='foobar', issuer=provider, user=user) session = self.client.session session['oidc_state'] = state @@ -168,6 +171,7 @@ def test_post_token_endpoint_with_invalid_ssl(self, post_mock): session.save() user = UserModel.objects.create(username='foobar') + OpenIDUser.objects.create(sub='foobar', issuer=provider, user=user) with mock.patch.object(OpenIDProvider, 'verify_id_token') as mock_verify_id_token: mock_verify_id_token.return_value = { 'sub': 'foobar' } diff --git a/oidc_auth/urls.py b/oidc_auth/urls.py index 31741ae..3bd2755 100644 --- a/oidc_auth/urls.py +++ b/oidc_auth/urls.py @@ -1,7 +1,8 @@ -from django.conf.urls import patterns, url +from django.urls import path +from . import views -urlpatterns = patterns('oidc_auth.views', - url(r'^login/$', 'login_begin', name='oidc-login'), - url(r'^complete/$', 'login_complete', name='oidc-complete'), -) +urlpatterns = [ + path(r'^login/$', views.login_begin, name='oidc-login'), + path(r'^complete/$', views.login_complete, name='oidc-complete'), +] diff --git a/oidc_auth/views.py b/oidc_auth/views.py index 85a036e..0013a75 100644 --- a/oidc_auth/views.py +++ b/oidc_auth/views.py @@ -1,8 +1,8 @@ -from urllib import urlencode +from urllib.parse import urlencode from django.conf import settings from django.http import HttpResponseBadRequest from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login as django_login -from django.core.urlresolvers import reverse +from django.urls import reverse from django.shortcuts import render, redirect import requests diff --git a/requirements.txt b/requirements.txt index dcd7184..2b64734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ # Production requirements -Django==1.6.2 -South==0.8.4 -pyjwkest==0.3.0 +Django>1.9 +pyjwkest==0.6.2 requests==2.2.1 +South==1.0.2 # Test requirements -nose==1.3.1 -django-nose==1.2 +django-nose==1.4.5 mock==1.0.1 +tox diff --git a/settings.py b/settings.py index 8683b39..6968f3c 100644 --- a/settings.py +++ b/settings.py @@ -1,6 +1,10 @@ import django import logging + +import os + django_version = django.get_version() +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -70,6 +74,23 @@ # Don't forget to use absolute paths, not relative paths. ) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, +}, +] + + INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', @@ -77,7 +98,6 @@ 'django.contrib.staticfiles', 'django.contrib.messages', 'django.contrib.admin', - 'south', 'django_nose', 'oidc_auth', ) diff --git a/setup.py b/setup.py index bfec403..acf0add 100644 --- a/setup.py +++ b/setup.py @@ -4,18 +4,17 @@ setup( name='django-oidc-auth', - version='0.0.8', + version='2.0.0', description='OpenID Connect client for Django applications', long_description='WIP', - author='Lucas S. Magalhães', - author_email='lucas.sampaio@intelie.com.br', + author='Lucas S. Magalhães, Daniel Pimentel', + author_email='lucas.sampaio@intelie.com.br, danielpimentel@lccv.ufal.br', packages=find_packages(exclude=['*.tests']), include_package_data=True, install_requires=[ - 'Django', - 'South', - 'pyjwkest', - 'requests', + 'Django>=4.2.8', + 'pyjwkest==1.4.2', + 'requests==2.31.0', ], zip_safe=True ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b5f7b5b --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = {py27}-django{15,16,17,18,19,111} + +[testenv] +basepython = + py27: python2.7 +deps = + pyjwkest==0.6.2 + requests==2.2.1 + South==1.0.2 + django-nose==1.4.5 + mock==1.0.1 + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + django17: Django>=1.7,<1.8 + django18: Django>=1.8,<1.9 + django19: Django>=1.9 + django111: Django>=1.11 +commands= + python manage.py test diff --git a/urls.py b/urls.py index 0c0ac45..f5c072d 100644 --- a/urls.py +++ b/urls.py @@ -1,11 +1,16 @@ -from django.conf.urls import patterns, url, include +from django.urls import path, include from django.contrib import admin +import oidc_auth +from . import views +from .oidc_auth import urls + admin.autodiscover() -urlpatterns = patterns('views', - url(r'^$', 'index'), - url(r'^oidc/', include('oidc_auth.urls')), - url(r'^admin/', include(admin.site.urls)), -) +urlpatterns = [ + path(r'^$', views.index), + path(r'^oidc/', include(oidc_auth.urls)), + path(r'^admin/', include(admin.site.urls)), +] +