Skip to content

Commit b58ad7d

Browse files
committed
Use iss field instead of issuer in auth init request to comply with spec
Nonce session_id and created fields are added, nothing is stored in session object Set default nonce size to 28 bytes to meet specification requirements Allow both GET and POST reauests for login initiation Add DISABLE_OIDC_DISCOVER setting to disable OP auto discover Add LOGIN_COMPLETE hook Replace exception in views with HTTP responses
1 parent 5b166a3 commit b58ad7d

File tree

6 files changed

+82
-47
lines changed

6 files changed

+82
-47
lines changed

oidc_auth/errors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,5 @@ class ForbiddenAuthRequest(OpenIDConnectError):
4242
message = 'querystring state differs from state saved on session'
4343

4444

45-
class MissingRedirectURL(OpenIDConnectError):
46-
message = "Missing URL for oidc redirect (maybe DEFAULT_ENDPOINT's missing on settings?)"
45+
class InvalidIssuer(OpenIDConnectError):
46+
message = "Missing or invalid OIDC provider URL"

oidc_auth/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33

44
class OpenIDConnectForm(forms.Form):
5-
issuer = forms.CharField(max_length=200,
5+
iss = forms.CharField(max_length=200,
66
widget=forms.TextInput(attrs={'class': 'required openid'}))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
import datetime
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('oidc_auth', '0002_delete_openiduser'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='nonce',
17+
name='created',
18+
field=models.DateTimeField(default=datetime.datetime(2016, 2, 28, 5, 40, 41, 496670), auto_now_add=True),
19+
preserve_default=False,
20+
),
21+
migrations.AddField(
22+
model_name='nonce',
23+
name='session_id',
24+
field=models.CharField(default='', max_length=128),
25+
preserve_default=False,
26+
),
27+
]

oidc_auth/models.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,38 @@ class Nonce(models.Model):
1717
issuer_url = models.URLField()
1818
state = models.CharField(max_length=255, unique=True)
1919
redirect_url = models.CharField(max_length=100)
20+
session_id = models.CharField(max_length=128)
21+
created = models.DateTimeField(auto_now_add=True)
2022

2123
def __unicode__(self):
2224
return '%s' % self.state
2325

2426
def __init__(self, *args, **kwargs):
2527
super(Nonce, self).__init__(*args, **kwargs)
2628

27-
@classmethod
28-
def generate(cls, redirect_url, issuer_url, length=oidc_settings.NONCE_LENGTH):
29-
"""This method generates and returns a nonce, an unique generated
30-
string. If the maximum of retries is exceeded, it returns None.
31-
"""
29+
@staticmethod
30+
def nonce(length=oidc_settings.NONCE_LENGTH):
31+
"""Generate nonce string"""
3232
CHARS = string.letters + string.digits
33+
return ''.join(random.choice(CHARS) for n in range(length))
3334

34-
for i in range(5):
35-
_hash = ''.join(random.choice(CHARS) for n in range(length))
36-
37-
try:
38-
log.debug('Attempt %s to save nonce %s to issuer %s' % (i+1,
39-
_hash, issuer_url))
40-
nonce = cls.objects.create(issuer_url=issuer_url, state=_hash,
41-
redirect_url=redirect_url)
42-
return nonce.state
43-
except IntegrityError:
44-
pass
45-
46-
log.error('Maximum of retries to create a nonce to issuer %s '
47-
'exceeded! Max: 5' % issuer_url)
48-
return None
35+
@classmethod
36+
def generate(cls, request, session_id, redirect_url, issuer_url, nonce=None):
37+
"""Generate and return state string for specified session"""
38+
state = nonce or cls.nonce()
39+
try:
40+
cls.objects.create(issuer_url=issuer_url, state=state,
41+
session_id=session_id, redirect_url=redirect_url)
42+
return state
43+
except IntegrityError:
44+
return None
4945

5046
@classmethod
51-
def validate(cls, state):
52-
"""This method validates nonce and returns encoded data dictionary
53-
"""
47+
def validate(cls, request, session_id, state):
48+
"""This method validates nonce for the session and returns
49+
object with redirect_url and issuer or None"""
5450
try:
55-
return cls.objects.get(state=state)
51+
return cls.objects.get(state=state, session_id=session_id)
5652
except cls.DoesNotExist:
5753
return None
5854

@@ -96,6 +92,9 @@ def discover(cls, issuer='', credentials={}, save=True):
9692
except cls.DoesNotExist:
9793
pass
9894

95+
if oidc_settings.DISABLE_OIDC_DISCOVER:
96+
raise errors.InvalidIssuer()
97+
9998
log.debug('Provider %s not discovered yet, proceeding discovery' % issuer)
10099
discover_endpoint = urljoin(issuer, '.well-known/openid-configuration')
101100
response = requests.get(discover_endpoint, verify=oidc_settings.VERIFY_SSL)

oidc_auth/settings.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44

55
DEFAULTS = {
66
'DISABLE_OIDC': False,
7+
'DISABLE_OIDC_DISCOVER': False,
78
'DEFAULT_PROVIDER': {},
89
'SCOPES': ('openid', 'given_name', 'family_name', 'preferred_username', 'email'),
910
'CLIENT_ID': None,
1011
'CLIENT_SECRET': None,
11-
'NONCE_LENGTH': 8,
12+
'NONCE_LENGTH': 32,
1213
'VERIFY_SSL': True,
1314
'COMPLETE_URL': None,
1415
'USER_MANAGER': None,
15-
'STATE_KEEPER': '.models.Nonce'
16+
'STATE_KEEPER': '.models.Nonce',
17+
'LOGIN_COMPLETE': None,
1618
}
1719

1820
USER_SETTINGS = getattr(settings, 'OIDC_AUTH', {})

oidc_auth/views.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from urllib import urlencode
22
from django.conf import settings
3-
from django.http import HttpResponseBadRequest, HttpResponse
3+
from django.http import HttpResponseBadRequest, HttpResponse, HttpResponseNotAllowed
44
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login as django_login
55
from django.core.urlresolvers import reverse
66
from django.shortcuts import render, redirect
@@ -30,18 +30,24 @@ def _redirect(request, login_complete_view, form_class, redirect_field_name):
3030
provider = get_default_provider()
3131

3232
if not provider:
33-
form = form_class(request.POST)
34-
35-
if not form.is_valid():
36-
raise errors.MissingRedirectURL()
37-
38-
provider = OpenIDProvider.discover(issuer=form.cleaned_data['issuer'])
33+
if request.method == 'POST':
34+
form = form_class(request.POST)
35+
if not form.is_valid():
36+
return HttpResponseBadRequest('Invalid issuer')
37+
provider = OpenIDProvider.discover(issuer=form.cleaned_data['iss'])
38+
elif request.method == 'GET':
39+
try:
40+
iss = request.GET['iss']
41+
except KeyError:
42+
return HttpResponseBadRequest('Invalid issuer')
43+
provider = OpenIDProvider.discover(issuer=iss)
44+
else:
45+
return HttpResponseNotAllowed(['POST', 'GET'])
3946

4047
redirect_url = request.GET.get(redirect_field_name, settings.LOGIN_REDIRECT_URL)
4148

4249
Nonce = import_from_str(oidc_settings.STATE_KEEPER)
43-
state = Nonce.generate(redirect_url, provider.issuer)
44-
request.session['oidc_state'] = state
50+
state = Nonce.generate(request, request.session.session_key, redirect_url, provider.issuer)
4551

4652
redirect_url = oidc_settings.COMPLETE_URL
4753
if redirect_url is None:
@@ -68,19 +74,15 @@ def login_complete(request, login_complete_view='oidc-complete',
6874
'error': request.GET['error']
6975
})
7076

71-
if 'oidc_state' not in request.session:
72-
return redirect(settings.LOGIN_URL)
73-
7477
if 'code' not in request.GET and 'state' not in request.GET:
7578
return HttpResponseBadRequest('Invalid request')
7679

77-
if request.GET['state'] != request.session['oidc_state']:
78-
raise errors.ForbiddenAuthRequest()
80+
state = request.GET['state']
7981

8082
Nonce = import_from_str(oidc_settings.STATE_KEEPER)
81-
nonce = Nonce.validate(request.GET['state'])
83+
nonce = Nonce.validate(request, request.session.session_key, state)
8284
if nonce is None:
83-
raise errors.ForbiddenAuthRequest()
85+
return HttpResponseBadRequest('Invalid state')
8486

8587
provider = OpenIDProvider.objects.get(issuer=nonce.issuer_url)
8688
log.debug('Login started from provider %s' % provider)
@@ -100,14 +102,18 @@ def login_complete(request, login_complete_view='oidc-complete',
100102
data=params, verify=oidc_settings.VERIFY_SSL)
101103

102104
if response.status_code != 200:
103-
raise errors.RequestError(provider.token_endpoint, response.status_code)
105+
return HttpResponseForbiddent('Invalid token')
104106

105107
log.debug('Token exchange done, proceeding authentication')
106108
credentials = response.json()
107109
credentials['provider'] = provider
108110
user = authenticate(credentials=credentials)
109111
django_login(request, user)
110112

113+
if oidc_settings.LOGIN_COMPLETE:
114+
hook = import_from_str(oidc_settings.LOGIN_COMPLETE)
115+
return hook(request, state, nonce.redirect_url)
116+
111117
return redirect(nonce.redirect_url)
112118

113119

@@ -119,4 +125,5 @@ def _redirect_to_provider(request):
119125
has_default_provider = oidc_settings.DEFAULT_PROVIDER
120126

121127
return (not oidc_settings.DISABLE_OIDC
122-
and (has_default_provider or request.method == 'POST'))
128+
and (has_default_provider
129+
or request.method == 'POST' or request.GET.get('iss')))

0 commit comments

Comments
 (0)