Merge pull request #25 from django-py/master

Adding test settings, coverage file, templates, CI integration and loggers
This commit is contained in:
Nicolas Mendoza 2015-06-08 16:44:44 -03:00
commit 4dcd69f8d7
16 changed files with 404 additions and 6 deletions

8
.coveragerc Normal file
View file

@ -0,0 +1,8 @@
[run]
omit =
tests/*
example_project/*
.tox/*
setup.py
*.egg/*
*/__main__.py

11
.travis.yml Normal file
View file

@ -0,0 +1,11 @@
language: python
python:
- "2.7"
env:
- DJANGO=1.7.8
- DJANGO=1.8.2
install:
- pip install -q Django==$DJANGO --use-mirrors
- pip install pyjwt==1.1.0 --use-mirrors
script:
- PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test oidc_provider --settings=oidc_provider.tests.test_settings

View file

@ -1,6 +1,10 @@
Django OIDC Provider Django OIDC Provider
#################### ####################
.. image:: https://api.travis-ci.org/django-py/django-openid-provider.png?branch=master
:alt: Build Status
:target: http://travis-ci.org/django-py/django-openid-provider
Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects.

View file

@ -1,13 +1,11 @@
from datetime import timedelta import logging
import uuid
from django.utils import timezone
from oidc_provider.lib.errors import * from oidc_provider.lib.errors import *
from oidc_provider.lib.utils.params import * from oidc_provider.lib.utils.params import *
from oidc_provider.lib.utils.token import * from oidc_provider.lib.utils.token import *
from oidc_provider.models import * from oidc_provider.models import *
from oidc_provider import settings
logger = logging.getLogger(__name__)
class AuthorizeEndpoint(object): class AuthorizeEndpoint(object):
@ -134,6 +132,10 @@ class AuthorizeEndpoint(object):
if self.params.response_type == 'id_token token': if self.params.response_type == 'id_token token':
uri += '&access_token={0}'.format(token.access_token) uri += '&access_token={0}'.format(token.access_token)
except: except:
logger.error('Authorization server error, grant_type: %s' %self.grant_type, extra={
'redirect_uri': self.redirect_uri,
'state': self.params.state
})
raise AuthorizeError( raise AuthorizeError(
self.params.redirect_uri, self.params.redirect_uri,
'server_error', 'server_error',

View file

@ -1,3 +1,4 @@
import logging
import urllib import urllib
from django.http import JsonResponse from django.http import JsonResponse
@ -8,6 +9,7 @@ from oidc_provider.lib.utils.token import *
from oidc_provider.models import * from oidc_provider.models import *
from oidc_provider import settings from oidc_provider import settings
logger = logging.getLogger(__name__)
class TokenEndpoint(object): class TokenEndpoint(object):
@ -16,6 +18,11 @@ class TokenEndpoint(object):
self.params = Params() self.params = Params()
self._extract_params() self._extract_params()
logger.debug('Request %s', self.request)
logger.debug('TokenEndPoint request.POST --> : %s', self.request.POST)
logger.debug('TokenEndpoint request.GET --> : %s', self.request.GET)
logger.debug('TokenEndPoint extract_params --> : %s', self.params.__dict__)
def _extract_params(self): def _extract_params(self):
query_dict = self.request.POST query_dict = self.request.POST
@ -29,21 +36,25 @@ class TokenEndpoint(object):
def validate_params(self): def validate_params(self):
if not (self.params.grant_type == 'authorization_code'): if not (self.params.grant_type == 'authorization_code'):
logger.error('Unsupported grant type: --> : %s', self.params.grant_type)
raise TokenError('unsupported_grant_type') raise TokenError('unsupported_grant_type')
try: try:
self.client = Client.objects.get(client_id=self.params.client_id) self.client = Client.objects.get(client_id=self.params.client_id)
if not (self.client.client_secret == self.params.client_secret): if not (self.client.client_secret == self.params.client_secret):
logger.error('Invalid client, client secret -->: %s', self.params.client_secret)
raise TokenError('invalid_client') raise TokenError('invalid_client')
if not (self.params.redirect_uri in self.client.redirect_uris): if not (self.params.redirect_uri in self.client.redirect_uris):
logger.error('Invalid client, redirect_uri --> : %s', self.params.redirect_uri)
raise TokenError('invalid_client') raise TokenError('invalid_client')
self.code = Code.objects.get(code=self.params.code) self.code = Code.objects.get(code=self.params.code)
if not (self.code.client == self.client) \ if not (self.code.client == self.client) \
or self.code.has_expired(): or self.code.has_expired():
logger.error('Invalid grant, code client --> %s', self.code.client)
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
except Client.DoesNotExist: except Client.DoesNotExist:
@ -77,7 +88,7 @@ class TokenEndpoint(object):
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
'id_token': id_token, 'id_token': id_token,
} }
logger.debug('Response dictionary --> : %s', dic)
return dic return dic
@classmethod @classmethod
@ -89,4 +100,6 @@ class TokenEndpoint(object):
response['Cache-Control'] = 'no-store' response['Cache-Control'] = 'no-store'
response['Pragma'] = 'no-cache' response['Pragma'] = 'no-cache'
logger.debug('JSON Response --> : %s', response.__dict__)
return response return response

View file

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<div class="panel panel-default" style="width:400px;margin:0 auto 25px auto;">
<div class="panel-body">
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
{% if form.errors %}
<div class="alert alert-danger" role="alert">Username and password are incorrect.</div>
{% endif %}
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="Username">
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="Password">
</div>
<input type="submit" class="btn btn-success btn-lg btn-block" value="Enter">
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<div class="panel panel-default" style="width:400px;margin:0 auto 25px auto;">
<div class="panel-body">
<h1>Bye!</h1>
<p>Thanks for spending some quality time with the web site today.</p>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenID Provider</title>
<!-- Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cerulean/bootstrap.min.css" rel="stylesheet">
<style type="text/css">body{padding-top:20px;padding-bottom:20px}.footer,.header,.marketing{padding-right:15px;padding-left:15px}.header{padding-bottom:20px;border-bottom:1px solid #e5e5e5}.header h3{margin-top:0;margin-bottom:0;line-height:40px}.footer{padding-top:19px;color:#777;border-top:1px solid #e5e5e5}@media (min-width:768px){.container{max-width:730px}}.container-narrow>hr{margin:30px 0}.jumbotron{border-bottom:1px solid #e5e5e5}.jumbotron .btn{padding:14px 24px;font-size:21px}.marketing{margin:40px 0}.marketing p+h4{margin-top:28px}@media screen and (min-width:768px){.footer,.header,.marketing{padding-right:0;padding-left:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}</style>
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="{% url 'home' %}">Home</a></li>
{% if user.is_authenticated %}
<li role="presentation"><a href="#">{{ user.email }}</a></li>
<li role="presentation"><a href="{% url 'logout' %}">Logout</a></li>
{% else %}
<li role="presentation"><a href="{% url 'login' %}">Login</a></li>
{% endif %}
</ul>
</nav>
<h3 class="text-muted">django-oidc-provider</h3>
</div>
{% block content %}{% endblock %}
<footer class="footer">
<p>Developed by <a href="http://github.com/juanifioren" target="_BLANK">Juan Ignacio Fiorentino</a>.</p>
</footer>
</div> <!-- /container -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block content %}
<div class="jumbotron">
<h1>Welcome!</h1>
<p class="lead">Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects.</p>
<p><a class="btn btn-lg btn-success" href="https://github.com/juanifioren/django-oidc-provider" role="button">View on Github</a></p>
</div>
{% endblock %}

View file

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Request for Permission</h3>
</div>
<div class="panel-body">
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
<form method="post" action="{% url 'oidc_provider:authorize' %}">
{% csrf_token %}
{{ hidden_inputs }}
<ul>
{% for scope in params.scope %}
<li>{{ scope | capfirst }}</li>
{% endfor %}
</ul>
<input name="allow" class="btn btn-primary btn-lg btn-block" type="submit" value="Authorize" />
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">{{ error }}</h3>
</div>
<div class="panel-body">
{{ description }}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,90 @@
import os
from datetime import timedelta
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
SITE_ID = 1
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(asctime)s %(process)d [%(levelname)s] %(name)s Line: %(lineno)s id: %(process)d : %(message)s'
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'filters': ['require_debug_false'],
'formatter': 'simple',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler',
'formatter': 'simple',
},
"debug_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "simple",
'filename': 'debug.log',
'formatter': 'simple',
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
'loggers': {
'oidc_provider': {
'handlers': ['console', 'debug_file_handler'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},
},
}
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'oidc_provider',
)
SECRET_KEY = 'secret-for-test-secret-top-secret'
ROOT_URLCONF = 'oidc_provider.tests.test_urls'
TEMPLATE_DIRS = (
"oidc_provider/tests/templates",
)
# OIDC Provider settings.
SITE_URL = 'http://localhost:8000'

View file

@ -0,0 +1,15 @@
from django.contrib.auth import views as auth_views
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import TemplateView
urlpatterns = patterns('',
url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'),
url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'),
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
url(r'^admin/', include(admin.site.urls)),
)

View file

@ -0,0 +1,27 @@
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponse
# CLass JsonResponse see: https://github.com/django/django/blob/master/django/http/response.py#L456
class JsonResponse(HttpResponse):
"""
An HTTP response class that consumes data to be serialized to JSON.
:param data: Data to be dumped into json. By default only ``dict`` objects
are allowed to be passed due to a security flaw before EcmaScript 5. See
the ``safe`` parameter for more information.
:param encoder: Should be an json encoder class. Defaults to
``django.core.serializers.json.DjangoJSONEncoder``.
:param safe: Controls if only ``dict`` objects may be serialized. Defaults
to ``True``.
"""
def __init__(self, data, encoder=DjangoJSONEncoder, safe=True, **kwargs):
if safe and not isinstance(data, dict):
raise TypeError('In order to allow non-dict objects to be serialized set the safe parameter to False')
kwargs.setdefault('content_type', 'application/json')
data = json.dumps(data, cls=encoder)
super(JsonResponse, self).__init__(content=data, **kwargs)

View file

@ -0,0 +1,83 @@
from django.conf import settings
class DefaultSettings(object):
@property
def LOGIN_URL(self):
"""
REQUIRED.
"""
return None
@property
def SITE_URL(self):
"""
REQUIRED.
"""
return None
@property
def OIDC_AFTER_USERLOGIN_HOOK(self):
"""
OPTIONAL.
"""
def default_hook_func(request, user, client):
return None
return default_hook_func
@property
def OIDC_CODE_EXPIRE(self):
"""
OPTIONAL.
"""
return 60*10
@property
def OIDC_EXTRA_SCOPE_CLAIMS(self):
"""
OPTIONAL.
"""
from oidc_provider.lib.claims import AbstractScopeClaims
return AbstractScopeClaims
@property
def OIDC_IDTOKEN_EXPIRE(self):
"""
OPTIONAL.
"""
return 60*10
@property
def OIDC_IDTOKEN_SUB_GENERATOR(self):
"""
OPTIONAL.
"""
def default_sub_generator(user):
return user.id
return default_sub_generator
@property
def OIDC_TOKEN_EXPIRE(self):
"""
OPTIONAL.
"""
return 60*60
default_settings = DefaultSettings()
def get(name):
'''
Helper function to use inside the package.
'''
try:
value = getattr(default_settings, name)
value = getattr(settings, name)
except AttributeError:
if value == None:
raise Exception('You must set ' + name + ' in your settings.')
return value

View file

@ -33,6 +33,10 @@ setup(
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
], ],
tests_require=[
'pyjwt==1.1.0'
],
install_requires=[ install_requires=[
'pyjwt==1.1.0', 'pyjwt==1.1.0',
], ],