Ajout type contrat
This commit is contained in:
@@ -0,0 +1 @@
|
||||
default_app_config = 'simple_sso.sso_server.apps.SimpleSSOServer'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SimpleSSOServer(AppConfig):
|
||||
name = 'simple_sso.sso_server'
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.db import migrations, models
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
import simple_sso.sso_server.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Consumer',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(unique=True, max_length=100)),
|
||||
('private_key', models.CharField(default=simple_sso.sso_server.models.ConsumerSecretKeyGenerator('private_key'), unique=True, max_length=64)),
|
||||
('public_key', models.CharField(default=simple_sso.sso_server.models.ConsumerSecretKeyGenerator('public_key'), unique=True, max_length=64)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Token',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('request_token', models.CharField(default=simple_sso.sso_server.models.TokenSecretKeyGenerator('request_token'), unique=True, max_length=64)),
|
||||
('access_token', models.CharField(default=simple_sso.sso_server.models.TokenSecretKeyGenerator('access_token'), unique=True, max_length=64)),
|
||||
('timestamp', models.DateTimeField(default=timezone.now)),
|
||||
('redirect_to', models.CharField(max_length=255)),
|
||||
('consumer', models.ForeignKey(related_name='tokens', to='sso_server.Consumer', on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sso_server', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='consumer',
|
||||
name='name',
|
||||
field=models.CharField(unique=True, max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sso_server', '0002_consumer_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='token',
|
||||
name='redirect_to',
|
||||
field=models.CharField(max_length=1023),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.deconstruct import deconstructible
|
||||
|
||||
from ..utils import gen_secret_key
|
||||
|
||||
|
||||
@deconstructible
|
||||
class SecretKeyGenerator:
|
||||
"""
|
||||
Helper to give default values to Client.secret and Client.key
|
||||
"""
|
||||
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
|
||||
def __call__(self):
|
||||
key = gen_secret_key(64)
|
||||
while self.get_model().objects.filter(**{self.field: key}).exists():
|
||||
key = gen_secret_key(64)
|
||||
return key
|
||||
|
||||
|
||||
class ConsumerSecretKeyGenerator(SecretKeyGenerator):
|
||||
def get_model(self):
|
||||
return Consumer
|
||||
|
||||
|
||||
class TokenSecretKeyGenerator(SecretKeyGenerator):
|
||||
def get_model(self):
|
||||
return Token
|
||||
|
||||
|
||||
class Consumer(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
private_key = models.CharField(
|
||||
max_length=64, unique=True,
|
||||
default=ConsumerSecretKeyGenerator('private_key')
|
||||
)
|
||||
public_key = models.CharField(
|
||||
max_length=64, unique=True,
|
||||
default=ConsumerSecretKeyGenerator('public_key')
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def rotate_keys(self):
|
||||
self.secret = ConsumerSecretKeyGenerator('private_key')()
|
||||
self.key = ConsumerSecretKeyGenerator('public_key')()
|
||||
self.save()
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
consumer = models.ForeignKey(
|
||||
Consumer,
|
||||
related_name='tokens',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
request_token = models.CharField(
|
||||
unique=True, max_length=64,
|
||||
default=TokenSecretKeyGenerator('request_token')
|
||||
)
|
||||
access_token = models.CharField(
|
||||
unique=True, max_length=64,
|
||||
default=TokenSecretKeyGenerator('access_token')
|
||||
)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
redirect_to = models.CharField(max_length=1023)
|
||||
user = models.ForeignKey(
|
||||
getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def refresh(self):
|
||||
self.timestamp = timezone.now()
|
||||
self.save()
|
||||
@@ -0,0 +1,175 @@
|
||||
import datetime
|
||||
from urllib.parse import urlparse, urlencode, urlunparse
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.options import ModelAdmin
|
||||
from django.http import (HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, QueryDict)
|
||||
from django.urls import re_path
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.generic.base import View
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
from simple_sso.sso_server.models import Token, Consumer
|
||||
from simple_sso.utils import BaseProvider, provider_wrapper
|
||||
|
||||
|
||||
class Provider(BaseProvider):
|
||||
max_age = 5
|
||||
|
||||
def __init__(self, server):
|
||||
self.server = server
|
||||
|
||||
def get_private_key(self, public_key):
|
||||
try:
|
||||
self.consumer = Consumer.objects.get(public_key=public_key)
|
||||
except Consumer.DoesNotExist:
|
||||
return None
|
||||
return self.consumer.private_key
|
||||
|
||||
|
||||
class RequestTokenProvider(Provider):
|
||||
def provide(self, data):
|
||||
redirect_to = data['redirect_to']
|
||||
token = Token.objects.create(consumer=self.consumer, redirect_to=redirect_to)
|
||||
return {'request_token': token.request_token}
|
||||
|
||||
|
||||
class AuthorizeView(View):
|
||||
"""
|
||||
The client get's redirected to this view with the `request_token` obtained
|
||||
by the Request Token Request by the client application beforehand.
|
||||
|
||||
This view checks if the user is logged in on the server application and if
|
||||
that user has the necessary rights.
|
||||
|
||||
If the user is not logged in, the user is prompted to log in.
|
||||
"""
|
||||
server = None
|
||||
|
||||
def get(self, request):
|
||||
request_token = request.GET.get('token', None)
|
||||
if not request_token:
|
||||
return self.missing_token_argument()
|
||||
try:
|
||||
self.token = Token.objects.select_related('consumer').get(request_token=request_token)
|
||||
except Token.DoesNotExist:
|
||||
return self.token_not_found()
|
||||
if not self.check_token_timeout():
|
||||
return self.token_timeout()
|
||||
self.token.refresh()
|
||||
if request.user.is_authenticated:
|
||||
return self.handle_authenticated_user()
|
||||
else:
|
||||
return self.handle_unauthenticated_user()
|
||||
|
||||
def missing_token_argument(self):
|
||||
return HttpResponseBadRequest('Token missing')
|
||||
|
||||
def token_not_found(self):
|
||||
return HttpResponseForbidden('Token not found')
|
||||
|
||||
def token_timeout(self):
|
||||
return HttpResponseForbidden('Token timed out')
|
||||
|
||||
def check_token_timeout(self):
|
||||
delta = timezone.now() - self.token.timestamp
|
||||
if delta > self.server.token_timeout:
|
||||
self.token.delete()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def handle_authenticated_user(self):
|
||||
if self.server.has_access(self.request.user, self.token.consumer):
|
||||
return self.success()
|
||||
else:
|
||||
return self.access_denied()
|
||||
|
||||
def handle_unauthenticated_user(self):
|
||||
next_ = '%s?%s' % (self.request.path, urlencode([('token', self.token.request_token)]))
|
||||
url = '%s?%s' % (reverse(self.server.auth_view_name), urlencode([('next', next_)]))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
def access_denied(self):
|
||||
return HttpResponseForbidden("Access denied")
|
||||
|
||||
def success(self):
|
||||
self.token.user = self.request.user
|
||||
self.token.save()
|
||||
serializer = URLSafeTimedSerializer(self.token.consumer.private_key)
|
||||
parse_result = urlparse(self.token.redirect_to)
|
||||
query_dict = QueryDict(parse_result.query, mutable=True)
|
||||
query_dict['access_token'] = serializer.dumps(self.token.access_token)
|
||||
url = urlunparse((parse_result.scheme, parse_result.netloc, parse_result.path, '', query_dict.urlencode(), ''))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class VerificationProvider(Provider, AuthorizeView):
|
||||
def provide(self, data):
|
||||
token = data['access_token']
|
||||
try:
|
||||
self.token = Token.objects.select_related('user').get(access_token=token, consumer=self.consumer)
|
||||
except Token.DoesNotExist:
|
||||
return self.token_not_found()
|
||||
if not self.check_token_timeout():
|
||||
return self.token_timeout()
|
||||
if not self.token.user:
|
||||
return self.token_not_bound()
|
||||
extra_data = data.get('extra_data', None)
|
||||
return self.server.get_user_data(
|
||||
self.token.user, self.consumer, extra_data=extra_data)
|
||||
|
||||
def token_not_bound(self):
|
||||
return HttpResponseForbidden("Invalid token")
|
||||
|
||||
|
||||
class ConsumerAdmin(ModelAdmin):
|
||||
readonly_fields = ['public_key', 'private_key']
|
||||
|
||||
|
||||
class Server:
|
||||
request_token_provider = RequestTokenProvider
|
||||
authorize_view = AuthorizeView
|
||||
verification_provider = VerificationProvider
|
||||
token_timeout = datetime.timedelta(minutes=5)
|
||||
client_admin = ConsumerAdmin
|
||||
auth_view_name = 'login'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
self.register_admin()
|
||||
|
||||
def register_admin(self):
|
||||
admin.site.register(Consumer, self.client_admin)
|
||||
|
||||
def has_access(self, user, consumer):
|
||||
return True
|
||||
|
||||
def get_user_extra_data(self, user, consumer, extra_data):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_user_data(self, user, consumer, extra_data=None):
|
||||
user_data = {
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'first_name': user.first_name,
|
||||
'last_name': user.last_name,
|
||||
'is_staff': False,
|
||||
'is_superuser': False,
|
||||
'is_active': user.is_active,
|
||||
}
|
||||
if extra_data:
|
||||
user_data['extra_data'] = self.get_user_extra_data(
|
||||
user, consumer, extra_data)
|
||||
return user_data
|
||||
|
||||
def get_urls(self):
|
||||
return [
|
||||
re_path(r'^request-token/$', provider_wrapper(self.request_token_provider(server=self)),
|
||||
name='simple-sso-request-token'),
|
||||
re_path(r'^authorize/$', self.authorize_view.as_view(server=self), name='simple-sso-authorize'),
|
||||
re_path(r'^verify/$', provider_wrapper(
|
||||
self.verification_provider(server=self)), name='simple-sso-verify'),
|
||||
]
|
||||
Reference in New Issue
Block a user