Refactoring. Add views for client creation.
This commit is contained in:
parent
f10d307fa4
commit
02c2ae99f7
14 changed files with 300 additions and 21 deletions
|
@ -41,7 +41,7 @@ class AuthorizeEndpoint(object):
|
|||
self.params.client_id = self.query_dict.get('client_id', '')
|
||||
self.params.redirect_uri = self.query_dict.get('redirect_uri', '')
|
||||
self.params.response_type = self.query_dict.get('response_type', '')
|
||||
self.params.scope = self.query_dict.get('scope', '')
|
||||
self.params.scope = self.query_dict.get('scope', '').split()
|
||||
self.params.state = self.query_dict.get('state', '')
|
||||
|
||||
def _extract_implicit_params(self):
|
||||
|
@ -57,8 +57,11 @@ class AuthorizeEndpoint(object):
|
|||
if not self.params.redirect_uri:
|
||||
raise RedirectUriError()
|
||||
|
||||
if not ('openid' in self.params.scope.split()):
|
||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_scope', self.grant_type)
|
||||
if not ('openid' in self.params.scope):
|
||||
raise AuthorizeError(
|
||||
self.params.redirect_uri,
|
||||
'invalid_scope',
|
||||
self.grant_type)
|
||||
|
||||
try:
|
||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||
|
@ -66,8 +69,13 @@ class AuthorizeEndpoint(object):
|
|||
if not (self.params.redirect_uri in self.client.redirect_uris):
|
||||
raise RedirectUriError()
|
||||
|
||||
if not (self.grant_type) or not (self.params.response_type == self.client.response_type):
|
||||
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type', self.grant_type)
|
||||
if not (self.grant_type) or \
|
||||
not (self.params.response_type == self.client.response_type):
|
||||
|
||||
raise AuthorizeError(
|
||||
self.params.redirect_uri,
|
||||
'unsupported_response_type',
|
||||
self.grant_type)
|
||||
|
||||
except Client.DoesNotExist:
|
||||
raise ClientIdError()
|
||||
|
@ -75,7 +83,10 @@ class AuthorizeEndpoint(object):
|
|||
def create_response_uri(self, allow):
|
||||
|
||||
if not allow:
|
||||
raise AuthorizeError(self.params.redirect_uri, 'access_denied', self.grant_type)
|
||||
raise AuthorizeError(
|
||||
self.params.redirect_uri,
|
||||
'access_denied',
|
||||
self.grant_type)
|
||||
|
||||
try:
|
||||
self.validate_params()
|
||||
|
@ -110,7 +121,7 @@ class AuthorizeEndpoint(object):
|
|||
|
||||
id_token = encode_id_token(id_token_dic, self.client.client_secret)
|
||||
|
||||
# TODO: Check if response_type is 'id_token token' and
|
||||
# TODO: Check if response_type is 'id_token token' then
|
||||
# add access_token to the fragment.
|
||||
uri = self.params.redirect_uri + \
|
||||
'#token_type={0}&id_token={1}&expires_in={2}'.format(
|
||||
|
@ -118,9 +129,13 @@ class AuthorizeEndpoint(object):
|
|||
id_token,
|
||||
60*10)
|
||||
except:
|
||||
raise AuthorizeError(self.params.redirect_uri, 'server_error', self.grant_type)
|
||||
raise AuthorizeError(
|
||||
self.params.redirect_uri,
|
||||
'server_error',
|
||||
self.grant_type)
|
||||
|
||||
# Add state if present.
|
||||
uri = uri + ('&state={0}'.format(self.params.state) if self.params.state else '')
|
||||
uri = uri + ('&state={0}'.format(self.params.state)
|
||||
if self.params.state else '')
|
||||
|
||||
return uri
|
18
openid_provider/lib/utils/decorators.py
Normal file
18
openid_provider/lib/utils/decorators.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
|
||||
def staff_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
|
||||
"""
|
||||
Decorator for views that checks that the user is logged in and is staff,
|
||||
redirecting to the log-in page if necessary.
|
||||
"""
|
||||
actual_decorator = user_passes_test(
|
||||
lambda u: u.is_authenticated() and u.is_staff,
|
||||
login_url=login_url,
|
||||
redirect_field_name=redirect_field_name
|
||||
)
|
||||
if function:
|
||||
return actual_decorator(function)
|
||||
|
||||
return actual_decorator
|
|
@ -1,2 +1,4 @@
|
|||
|
||||
|
||||
class Params(object):
|
||||
pass
|
108
openid_provider/migrations/0001_initial.py
Normal file
108
openid_provider/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Client',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(default=b'', max_length=100)),
|
||||
('client_id', models.CharField(unique=True, max_length=255)),
|
||||
('client_secret', models.CharField(unique=True, max_length=255)),
|
||||
('client_type', models.CharField(max_length=20, choices=[(b'confidential', b'Confidential'), (b'public', b'Public')])),
|
||||
('response_type', models.CharField(max_length=30, choices=[(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'), (b'id_token token', b'id_token token (Implicit Flow)')])),
|
||||
('_scope', models.TextField(default=b'')),
|
||||
('_redirect_uris', models.TextField(default=b'')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Code',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('code', models.CharField(unique=True, max_length=255)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('_scope', models.TextField(default=b'')),
|
||||
('client', models.ForeignKey(to='openid_provider.Client')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Token',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('access_token', models.CharField(unique=True, max_length=255)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('_scope', models.TextField(default=b'')),
|
||||
('_id_token', models.TextField()),
|
||||
('client', models.ForeignKey(to='openid_provider.Client')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserInfo',
|
||||
fields=[
|
||||
('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('given_name', models.CharField(default=b'', max_length=255)),
|
||||
('family_name', models.CharField(default=b'', max_length=255)),
|
||||
('middle_name', models.CharField(default=b'', max_length=255)),
|
||||
('nickname', models.CharField(default=b'', max_length=255)),
|
||||
('preferred_username', models.CharField(default=b'', max_length=255)),
|
||||
('profile', models.URLField(default=b'')),
|
||||
('picture', models.URLField(default=b'')),
|
||||
('website', models.URLField(default=b'')),
|
||||
('email_verified', models.BooleanField(default=False)),
|
||||
('gender', models.CharField(default=b'', max_length=100)),
|
||||
('birthdate', models.DateField()),
|
||||
('zoneinfo', models.CharField(default=b'', max_length=100)),
|
||||
('locale', models.CharField(default=b'', max_length=100)),
|
||||
('phone_number', models.CharField(default=b'', max_length=255)),
|
||||
('phone_number_verified', models.BooleanField(default=False)),
|
||||
('address_formatted', models.CharField(default=b'', max_length=255)),
|
||||
('address_street_address', models.CharField(default=b'', max_length=255)),
|
||||
('address_locality', models.CharField(default=b'', max_length=255)),
|
||||
('address_region', models.CharField(default=b'', max_length=255)),
|
||||
('address_postal_code', models.CharField(default=b'', max_length=255)),
|
||||
('address_country', models.CharField(default=b'', max_length=255)),
|
||||
('updated_at', models.DateTimeField()),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='token',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='code',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='client',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
|
@ -54,7 +54,15 @@ class Code(models.Model):
|
|||
client = models.ForeignKey(Client)
|
||||
code = models.CharField(max_length=255, unique=True)
|
||||
expires_at = models.DateTimeField()
|
||||
scope = models.TextField() # TODO: add getter and setter for this.
|
||||
|
||||
_scope = models.TextField(default='')
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
|
||||
def has_expired(self):
|
||||
return timezone.now() >= self.expires_at
|
||||
|
@ -64,9 +72,16 @@ class Token(models.Model):
|
|||
user = models.ForeignKey(User)
|
||||
client = models.ForeignKey(Client)
|
||||
access_token = models.CharField(max_length=255, unique=True)
|
||||
refresh_token = models.CharField(max_length=255, unique=True)
|
||||
expires_at = models.DateTimeField()
|
||||
scope = models.TextField() # TODO: add getter and setter for this.
|
||||
|
||||
_scope = models.TextField(default='')
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
|
||||
_id_token = models.TextField()
|
||||
def id_token():
|
||||
|
|
|
@ -11,18 +11,18 @@
|
|||
<h3 class="panel-title">Request for Permission</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>Client {{ client.name }} would like to access this information of you ...</p>
|
||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||
<form method="post" action="{% url 'openid_provider:authorize' %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<input name="client_id" type="hidden" value="{{ params.client_id }}" />
|
||||
<input name="redirect_uri" type="hidden" value="{{ params.redirect_uri }}" />
|
||||
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
||||
<input name="scope" type="hidden" value="{{ params.scope }}" />
|
||||
<input name="scope" type="hidden" value="{{ params.scope_str }}" />
|
||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
||||
|
||||
<ul class="list-group">
|
||||
{% for scope in params.scope.split %}
|
||||
{% for scope in params.scope %}
|
||||
{% if scope != 'openid' %}
|
||||
<li class="list-group-item">{{ scope | capfirst }}</li>
|
||||
{% endif %}
|
||||
|
|
|
@ -9,9 +9,13 @@
|
|||
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.1/spacelab/bootstrap.min.css" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #fbfbfb;
|
||||
padding-top: 90px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
.btn {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
|
|
33
openid_provider/templates/openid_provider/clients.html
Normal file
33
openid_provider/templates/openid_provider/clients.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends "openid_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-md-offset-1">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<a href="#" class="btn btn-success btn-block">New Client</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Clients</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="list-group">
|
||||
{% for client in object_list %}
|
||||
<a href="#{{ client.client_id }}" class="list-group-item">{{ client.name }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item">No clients yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
39
openid_provider/templates/registration/login.html
Normal file
39
openid_provider/templates/registration/login.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends "openid_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h2>Please sign in</h2>
|
||||
<hr>
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">Username and password mismatch.</div>
|
||||
{% endif %}
|
||||
<form accept-charset="UTF-8" role="form" method="post" action="{% url 'openid_provider:login' %}">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Username" name="username" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" placeholder="Password" name="password" type="password" value="">
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input name="remember" type="checkbox" value="Remember Me"> Remember Me
|
||||
</label>
|
||||
</div>
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<input class="btn btn-success btn-block" type="submit" value="OK">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
19
openid_provider/templates/registration/logout.html
Normal file
19
openid_provider/templates/registration/logout.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "openid_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<h2>Logged out</h2>
|
||||
<hr>
|
||||
<p>See you next time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,12 +1,18 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from . import views
|
||||
from openid_provider.views import clients
|
||||
from openid_provider.views import endpoints
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
||||
url(r'^authorize/$', views.AuthorizeView.as_view(), name='authorize'),
|
||||
url(r'^token/$', csrf_exempt(views.TokenView.as_view()), name='token'),
|
||||
url(r'^userinfo/$', csrf_exempt(views.userinfo), name='userinfo'),
|
||||
url(r'^authorize/$', endpoints.AuthorizeView.as_view(), name='authorize'),
|
||||
url(r'^token/$', csrf_exempt(endpoints.TokenView.as_view()), name='token'),
|
||||
url(r'^userinfo/$', csrf_exempt(endpoints.userinfo), name='userinfo'),
|
||||
|
||||
url(r'^login/$', 'django.contrib.auth.views.login', name='login'),
|
||||
url(r'^logout/$', 'django.contrib.auth.views.logout', name='logout'),
|
||||
|
||||
url(r'^clients/$', clients.ClientListView.as_view(), name='clients'),
|
||||
|
||||
)
|
0
openid_provider/views/__init__.py
Normal file
0
openid_provider/views/__init__.py
Normal file
14
openid_provider/views/clients.py
Normal file
14
openid_provider/views/clients.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic.list import ListView
|
||||
from openid_provider.lib.utils.decorators import staff_required
|
||||
from openid_provider.models import Client
|
||||
|
||||
|
||||
class ClientListView(ListView):
|
||||
|
||||
model = Client
|
||||
template_name = "openid_provider/clients.html"
|
||||
|
||||
@method_decorator(staff_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(ClientListView, self).dispatch(*args, **kwargs)
|
|
@ -21,6 +21,10 @@ class AuthorizeView(View):
|
|||
authorize.validate_params()
|
||||
|
||||
if request.user.is_authenticated():
|
||||
|
||||
# This is for passing scopes into the form.
|
||||
authorize.params.scope_str = ' '.join(authorize.params.scope)
|
||||
|
||||
data = {
|
||||
'params': authorize.params,
|
||||
'client': authorize.client,
|
||||
|
@ -29,7 +33,7 @@ class AuthorizeView(View):
|
|||
return render(request, 'openid_provider/authorize.html', data)
|
||||
else:
|
||||
next = urllib.quote(request.get_full_path())
|
||||
login_url = settings.LOGIN_URL + '?next={0}'.format(next)
|
||||
login_url = settings.LOGIN_URL + '?next=' + next
|
||||
|
||||
return HttpResponseRedirect(login_url)
|
||||
|
||||
|
@ -42,7 +46,9 @@ class AuthorizeView(View):
|
|||
return render(request, 'openid_provider/error.html', data)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state)
|
||||
uri = error.create_uri(
|
||||
authorize.params.redirect_uri,
|
||||
authorize.params.state)
|
||||
|
||||
return HttpResponseRedirect(uri)
|
||||
|
Loading…
Reference in a new issue