diff --git a/example_project/myapp/settings.py b/example_project/myapp/settings.py index bcde181..e97ad1d 100644 --- a/example_project/myapp/settings.py +++ b/example_project/myapp/settings.py @@ -32,6 +32,7 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'oidc_provider.middleware.SessionManagementMiddleware', ] TEMPLATES = [ @@ -87,3 +88,4 @@ LOGIN_REDIRECT_URL = '/' # OIDC Provider settings SITE_URL = 'http://localhost:8000' +OIDC_SESSION_MANAGEMENT_ENABLE = True diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 58be181..731d299 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -1,10 +1,15 @@ from datetime import timedelta +from hashlib import ( + md5, + sha256, +) import logging try: from urllib import urlencode from urlparse import urlsplit, parse_qs, urlunsplit except ImportError: from urllib.parse import urlsplit, parse_qs, urlunsplit, urlencode +from uuid import uuid4 from django.utils import timezone @@ -173,6 +178,29 @@ class AuthorizeEndpoint(object): query_fragment['state'] = self.params['state'] if self.params['state'] else '' + if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): + # Generate client origin URI from the redirect_uri param. + redirect_uri_parsed = urlsplit(self.params['redirect_uri']) + client_origin = '{0}://{1}'.format(redirect_uri_parsed.scheme, redirect_uri_parsed.netloc) + + # Create random salt. + salt = md5(uuid4().hex.encode()).hexdigest() + + # The generation of suitable Session State values is based + # on a salted cryptographic hash of Client ID, origin URL, + # and OP browser state. + session_state = '{client_id} {origin} {browser_state} {salt}'.format( + client_id=self.client.client_id, + origin=client_origin, + browser_state=self.request.COOKIES['op_browser_state'], + salt=salt) + session_state = sha256(session_state).hexdigest() + session_state += '.' + salt + if self.grant_type == 'authorization_code': + query_params['session_state'] = session_state + elif self.grant_type in ['implicit', 'hybrid']: + query_fragment['session_state'] = session_state + except Exception as error: logger.debug('[Authorize] Error when trying to create response uri: %s', error) raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type) diff --git a/oidc_provider/middleware.py b/oidc_provider/middleware.py new file mode 100644 index 0000000..511851e --- /dev/null +++ b/oidc_provider/middleware.py @@ -0,0 +1,17 @@ +from hashlib import sha224 + +from django.conf import settings as django_settings +from django.utils.deprecation import MiddlewareMixin + + +class SessionManagementMiddleware(MiddlewareMixin): + """ + Maintain a `op_browser_state` cookie along with the `sessionid` cookie that + represents the End-User's login state at the OP. If the user is not logged + in then use `SECRET_KEY` value. + """ + + def process_response(self, request, response): + session_state = sha224(request.session.session_key or django_settings.SECRET_KEY).hexdigest() + response.set_cookie('op_browser_state', session_state) + return response diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 8e16aab..610534c 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -61,6 +61,13 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_sub_generator' + @property + def OIDC_SESSION_MANAGEMENT_ENABLE(self): + """ + OPTIONAL. If enabled, the Server will support Session Management 1.0 specification. + """ + return False + @property def OIDC_SKIP_CONSENT_ALWAYS(self): """ diff --git a/oidc_provider/static/oidc_provider/js/sha256.min.js b/oidc_provider/static/oidc_provider/js/sha256.min.js new file mode 100644 index 0000000..c7b78b5 --- /dev/null +++ b/oidc_provider/static/oidc_provider/js/sha256.min.js @@ -0,0 +1,16 @@ +/** + * [js-sha256]{@link https://github.com/emn178/js-sha256} + * + * @version 0.3.2 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2016 + * @license MIT + */ +(function(I){"object"==typeof process&&process.versions&&process.versions.node&&(I=global);var a="0123456789abcdef".split(""),Q=[-2147483648,8388608,32768,128],C=[24,16,8,0],L=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711, +113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],c=[],J=function(a){return A(a,!0)},A=function(D,A){var K="string"!=typeof D;K&&D.constructor==I.ArrayBuffer&&(D=new Uint8Array(D));var m, +n,p,q,r,t,u,v,e,J=!0,O=!1,b,B=0,M=0,P=0,N=D.length,h,d,E,F,G,H;A?(m=3238371032,n=914150663,p=812702999,q=4144912697,r=4290775857,t=1750603025,u=1694076839,v=3204075428):(m=1779033703,n=3144134277,p=1013904242,q=2773480762,r=1359893119,t=2600822924,u=528734635,v=1541459225);e=0;do{c[0]=e;c[16]=c[1]=c[2]=c[3]=c[4]=c[5]=c[6]=c[7]=c[8]=c[9]=c[10]=c[11]=c[12]=c[13]=c[14]=c[15]=0;if(K)for(b=M;Bb;++B)c[b>>2]|=D[B]<b;++B)e=D.charCodeAt(B),128>e?c[b>>2]|=e< +e?c[b>>2]|=(192|e>>6)<e||57344<=e?c[b>>2]|=(224|e>>12)<>2]|=(240|e>>18)<>2]|=(128|e>>12&63)<>2]|=(128|e>>6&63)<>2]|=(128|e&63)<>2]|=Q[b&3],++B);e=c[16];B>N&&56>b&&(c[15]=P<<3,O=!0);var w=m,k=n,l=p,f=q,x=r,y=t,z=u,g=v;for(b=16;64>b;++b)d=c[b-15],h=(d>>>7|d<<25)^(d>>>18|d<<14)^d>>>3,d=c[b-2],d=(d>>>17|d<<15)^(d>>>19|d<<13)^d>>>10,c[b]=c[b-16]+ +h+c[b-7]+d<<0;H=k&l;for(b=0;64>b;b+=4)J?(A?(G=300032,d=c[0]-1413257819,g=d-150054599<<0,f=d+24177077<<0):(G=704751109,d=c[0]-210244248,g=d-1521486534<<0,f=d+143694565<<0),J=!1):(h=(w>>>2|w<<30)^(w>>>13|w<<19)^(w>>>22|w<<10),d=(x>>>6|x<<26)^(x>>>11|x<<21)^(x>>>25|x<<7),G=w&k,E=G^w&l^H,F=x&y^~x&z,d=g+d+F+L[b]+c[b],h+=E,g=f+d<<0,f=d+h<<0),h=(f>>>2|f<<30)^(f>>>13|f<<19)^(f>>>22|f<<10),d=(g>>>6|g<<26)^(g>>>11|g<<21)^(g>>>25|g<<7),H=f&w,E=H^f&k^G,F=g&x^~g&y,d=z+d+F+L[b+1]+c[b+1],h+=E,z=l+d<<0,l=d+h<<0, +h=(l>>>2|l<<30)^(l>>>13|l<<19)^(l>>>22|l<<10),d=(z>>>6|z<<26)^(z>>>11|z<<21)^(z>>>25|z<<7),G=l&f,E=G^l&w^H,F=z&g^~z&x,d=y+d+F+L[b+2]+c[b+2],h+=E,y=k+d<<0,k=d+h<<0,h=(k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10),d=(y>>>6|y<<26)^(y>>>11|y<<21)^(y>>>25|y<<7),H=k&l,E=H^k&f^G,F=y&z^~y&g,d=x+d+F+L[b+3]+c[b+3],h+=E,x=w+d<<0,w=d+h<<0;m=m+w<<0;n=n+k<<0;p=p+l<<0;q=q+f<<0;r=r+x<<0;t=t+y<<0;u=u+z<<0;v=v+g<<0}while(!O);K=a[m>>28&15]+a[m>>24&15]+a[m>>20&15]+a[m>>16&15]+a[m>>12&15]+a[m>>8&15]+a[m>>4&15]+a[m&15]+a[n>> +28&15]+a[n>>24&15]+a[n>>20&15]+a[n>>16&15]+a[n>>12&15]+a[n>>8&15]+a[n>>4&15]+a[n&15]+a[p>>28&15]+a[p>>24&15]+a[p>>20&15]+a[p>>16&15]+a[p>>12&15]+a[p>>8&15]+a[p>>4&15]+a[p&15]+a[q>>28&15]+a[q>>24&15]+a[q>>20&15]+a[q>>16&15]+a[q>>12&15]+a[q>>8&15]+a[q>>4&15]+a[q&15]+a[r>>28&15]+a[r>>24&15]+a[r>>20&15]+a[r>>16&15]+a[r>>12&15]+a[r>>8&15]+a[r>>4&15]+a[r&15]+a[t>>28&15]+a[t>>24&15]+a[t>>20&15]+a[t>>16&15]+a[t>>12&15]+a[t>>8&15]+a[t>>4&15]+a[t&15]+a[u>>28&15]+a[u>>24&15]+a[u>>20&15]+a[u>>16&15]+a[u>>12& +15]+a[u>>8&15]+a[u>>4&15]+a[u&15];A||(K+=a[v>>28&15]+a[v>>24&15]+a[v>>20&15]+a[v>>16&15]+a[v>>12&15]+a[v>>8&15]+a[v>>4&15]+a[v&15]);return K};!I.JS_SHA256_TEST&&"object"==typeof module&&module.exports?(A.sha256=A,A.sha224=J,module.exports=A):I&&(I.sha256=A,I.sha224=J)})(this); diff --git a/oidc_provider/templates/oidc_provider/check_session_iframe.html b/oidc_provider/templates/oidc_provider/check_session_iframe.html new file mode 100644 index 0000000..445fda2 --- /dev/null +++ b/oidc_provider/templates/oidc_provider/check_session_iframe.html @@ -0,0 +1,49 @@ +{% load staticfiles %} + + + + + OP Iframe + + + + + OpenID Connect Session Management OP Iframe. + + diff --git a/oidc_provider/urls.py b/oidc_provider/urls.py index bd9e911..1907782 100644 --- a/oidc_provider/urls.py +++ b/oidc_provider/urls.py @@ -1,6 +1,10 @@ from django.conf.urls import url from django.views.decorators.csrf import csrf_exempt -from oidc_provider import views + +from oidc_provider import ( + settings, + views, +) urlpatterns = [ @@ -12,3 +16,8 @@ urlpatterns = [ url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(), name='provider_info'), url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'), ] + +if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): + urlpatterns += [ + url(r'^check-session-iframe/?$', views.CheckSessionIframeView.as_view(), name='check-session-iframe'), + ] diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 91b96a4..3220ded 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -6,6 +6,8 @@ from django.core.urlresolvers import reverse from django.http import JsonResponse from django.shortcuts import render from django.template.loader import render_to_string +from django.utils.decorators import method_decorator +from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.http import require_http_methods from django.views.generic import View from jwkest import long_to_base64 @@ -203,6 +205,9 @@ class ProviderInfoView(View): dic['token_endpoint_auth_methods_supported'] = ['client_secret_post', 'client_secret_basic'] + if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): + dic['check_session_iframe'] = site_url + reverse('oidc_provider:check-session-iframe') + response = JsonResponse(dic) response['Access-Control-Allow-Origin'] = '*' @@ -236,3 +241,13 @@ class LogoutView(View): def get(self, request, *args, **kwargs): # We should actually verify if the requested redirect URI is safe return logout(request, next_page=request.GET.get('post_logout_redirect_uri')) + + +class CheckSessionIframeView(View): + + @method_decorator(xframe_options_exempt) + def dispatch(self, request, *args, **kwargs): + return super(CheckSessionIframeView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + return render(request, 'oidc_provider/check_session_iframe.html', kwargs)