Skip to content

Commit fcd9f53

Browse files
authored
#94 rewrite RefreshIDToken middleware to use prompt=none (#127)
Rewrite RefreshIDToken middleware to use authentication endpoint with prompt=None. In addition: * this moves the middleware and friends into the mozilla_django_oidc Python module * adds a OIDC_EXEMPT_URLS setting * makes the middleware more flexible for subclassing to augment the behavior * adds debug logging to make it easier to verify the middleware is set up correctly and working * adds some rough troubleshooting docs
1 parent 2207c27 commit fcd9f53

File tree

11 files changed

+281
-201
lines changed

11 files changed

+281
-201
lines changed

docs/installation.rst

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -185,39 +185,26 @@ However, even if that account was disabled, the user's account and session on
185185
your site will continue. In this way, a user can quit his/her job, lose access to
186186
his/her corporate account, but continue to use your website.
187187

188-
To handle this scenario, your website needs to know if the user's ID token with
188+
To handle this scenario, your website needs to know if the user's id token with
189189
the OIDC provider is still valid. You need to use the
190-
:py:class:`mozilla_django_oidc.contrib.auth0.middleware.RefreshIDToken` middleware.
190+
:py:class:`mozilla_django_oidc.middleware.RefreshIDToken` middleware.
191191

192192
To add it to your site, put it in the settings::
193193

194194
MIDDLEWARE_CLASSES = [
195195
# middleware involving session and autheentication must come first
196196
# ...
197-
'mozilla_django_oidc.contrib.auth0.middleware.RefreshIDToken',
197+
'mozilla_django_oidc.middleware.RefreshIDToken',
198198
# ...
199199
]
200200

201201

202-
The ``RefreshIDToken`` middleware will check that the id token is still valid
203-
with the OIDC provider every ``settings.OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS``
204-
which defaults to 15 minutes.
202+
The ``RefreshIDToken`` middleware will check to see if the user's id token has
203+
expired and if so, redirect to the OIDC provider's authentication endpoint
204+
for a silent re-auth. That will redirect back to the page the user was going to.
205205

206-
You also need to set ``OIDC_STORE_ACCESS_TOKEN``::
207-
208-
OIDC_STORE_ACCESS_TOKEN = True
209-
210-
211-
This stores the token that the middleware renews.
212-
213-
You also need to set the domain for your Auth0 SSO::
214-
215-
OIDC_OP_DOMAIN = "<domain for OP>"
216-
217-
218-
.. note::
219-
Currently, this is implemented using an Auth0-specific API endpoint. That
220-
will change soon.
206+
The length of time it takes for an id token to expire is set in
207+
``settings.OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS`` which defaults to 15 minutes.
221208

222209

223210
Connecting OIDC user identities to Django users
@@ -352,3 +339,25 @@ setting::
352339

353340
You might want to do this if you want to control user creation because your
354341
system requires additional process to allow people to use it.
342+
343+
344+
Troubleshooting
345+
---------------
346+
347+
mozilla-django-oidc logs using the ``mozilla_django_oidc`` logger. Enable that
348+
logger in settings to see logging messages to help you debug:
349+
350+
.. code-block:: python
351+
352+
LOGGING = {
353+
...
354+
'loggers': {
355+
'mozilla_django_oidc': {
356+
'handlers': ['console'],
357+
'level': 'DEBUG'
358+
},
359+
...
360+
}
361+
362+
363+
Make sure to use the appropriate handler for your app.

docs/settings.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ of ``mozilla-django-oidc``.
7272

7373
Controls whether the OpenID Connect client verifies the SSL certificate of the OP responses
7474

75+
.. py:attribute:: OIDC_EXEMPT_URLS
76+
77+
:default: ``[]``
78+
79+
This is a list of url paths or Django view names. This plus the
80+
mozilla-django-oidc urls are exempted from the id token renewal by the
81+
``RenewIDToken`` middleware.
82+
7583
.. py:attribute:: OIDC_CREATE_USER
7684
7785
:default: ``True``

mozilla_django_oidc/contrib/__init__.py

Whitespace-only changes.

mozilla_django_oidc/contrib/auth0/__init__.py

Whitespace-only changes.

mozilla_django_oidc/contrib/auth0/middleware.py

Lines changed: 0 additions & 49 deletions
This file was deleted.

mozilla_django_oidc/contrib/auth0/utils.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

mozilla_django_oidc/middleware.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import logging
2+
import time
3+
try:
4+
from urllib import urlencode
5+
except ImportError:
6+
from urllib.parse import urlencode
7+
8+
import django
9+
from django.core.urlresolvers import reverse
10+
from django.http import HttpResponseRedirect
11+
from django.utils.crypto import get_random_string
12+
13+
from mozilla_django_oidc.utils import absolutify, import_from_settings, is_authenticated
14+
15+
16+
LOGGER = logging.getLogger(__name__)
17+
18+
19+
# Django 1.10 makes changes to how middleware work. In Django 1.10+, we want to
20+
# use the mixin so that our middleware works as is.
21+
if django.VERSION >= (1, 10):
22+
from django.utils.deprecation import MiddlewareMixin
23+
else:
24+
class MiddlewareMixin(object):
25+
pass
26+
27+
28+
# FIXME(willkg): This doesn't appear to be used anywhere.
29+
def logout_url():
30+
"""Log out the user from Auth0."""
31+
url = 'https://' + import_from_settings('OIDC_OP_DOMAIN') + '/v2/logout'
32+
url += '?' + urlencode({
33+
'returnTo': import_from_settings('LOGOUT_REDIRECT_URL', '/'),
34+
'client_id': import_from_settings('OIDC_RP_CLIENT_ID')
35+
})
36+
return url
37+
38+
39+
class RefreshIDToken(MiddlewareMixin):
40+
"""Renews id_tokens after expiry seconds
41+
42+
For users authenticated with an id_token, we need to check that it's still
43+
valid after a specific amount of time and if not, force them to
44+
re-authenticate silently.
45+
46+
"""
47+
def get_exempt_urls(self):
48+
"""Generate and return a set of url paths to exempt from RefreshIDToken
49+
50+
This takes the value of ``settings.OIDC_EXEMPT_URLS`` and appends three
51+
urls that mozilla-django-oidc uses. These values can be view names or
52+
absolute url paths.
53+
54+
:returns: list of url paths (for example "/oidc/callback/")
55+
56+
"""
57+
exempt_urls = list(import_from_settings('OIDC_EXEMPT_URLS', []))
58+
exempt_urls.extend([
59+
'oidc_authentication_init',
60+
'oidc_authentication_callback',
61+
'oidc_logout',
62+
])
63+
64+
return [
65+
url if url.startswith('/') else reverse(url)
66+
for url in exempt_urls
67+
]
68+
69+
def is_refreshable_url(self, request):
70+
"""Takes a request and returns whether it triggers a refresh examination
71+
72+
:arg HttpRequest request:
73+
74+
:returns: boolean
75+
76+
"""
77+
exempt_urls = self.get_exempt_urls()
78+
79+
return (
80+
request.method == 'GET' and
81+
is_authenticated(request.user) and
82+
request.path not in exempt_urls and
83+
not request.is_ajax()
84+
)
85+
86+
def process_request(self, request):
87+
if not self.is_refreshable_url(request):
88+
LOGGER.debug('request is not refreshable')
89+
return
90+
91+
expiration = request.session.get('oidc_id_token_expiration', 0)
92+
now = time.time()
93+
if expiration > now:
94+
# The id_token is still valid, so we don't have to do anything.
95+
LOGGER.debug('id token is still valid (%s > %s)', expiration, now)
96+
return
97+
98+
LOGGER.debug('id token has expired')
99+
# The id_token has expired, so we have to re-authenticate silently.
100+
auth_url = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT')
101+
client_id = import_from_settings('OIDC_RP_CLIENT_ID')
102+
state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
103+
104+
# Build the parameters as if we were doing a real auth handoff, except
105+
# we also include prompt=none.
106+
params = {
107+
'response_type': 'code',
108+
'client_id': client_id,
109+
'redirect_uri': absolutify(reverse('oidc_authentication_callback')),
110+
'state': state,
111+
'scope': 'openid',
112+
'prompt': 'none',
113+
}
114+
115+
if import_from_settings('OIDC_USE_NONCE', True):
116+
nonce = get_random_string(import_from_settings('OIDC_NONCE_SIZE', 32))
117+
params.update({
118+
'nonce': nonce
119+
})
120+
request.session['oidc_nonce'] = nonce
121+
122+
request.session['oidc_state'] = state
123+
request.session['oidc_login_next'] = request.get_full_path()
124+
125+
query = urlencode(params)
126+
redirect_url = '{url}?{query}'.format(url=auth_url, query=query)
127+
return HttpResponseRedirect(redirect_url)

mozilla_django_oidc/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
try:
23
from urllib import urlencode
34
except ImportError:
@@ -37,6 +38,12 @@ def login_failure(self):
3738

3839
def login_success(self):
3940
auth.login(self.request, self.user)
41+
42+
# Figure out when this id_token will expire. This is ignored unless you're
43+
# using the RenewIDToken middleware.
44+
expiration_interval = import_from_settings('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 60 * 15)
45+
self.request.session['oidc_id_token_expiration'] = time.time() + expiration_interval
46+
4047
return HttpResponseRedirect(self.success_url)
4148

4249
def get(self, request):

tests/auth0_tests/__init__.py

Whitespace-only changes.

tests/auth0_tests/test_utils.py

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)