views.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. # Copyright (C) 2010-2013 Yaco Sistemas (http://www.yaco.es)
  2. # Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. try:
  17. from xml.etree import ElementTree
  18. except ImportError:
  19. from elementtree import ElementTree
  20. from django.conf import settings
  21. from django.contrib import auth
  22. from django.contrib.auth.decorators import login_required
  23. from django.contrib.auth.views import logout as django_logout
  24. from django.http import Http404, HttpResponse
  25. from django.http import HttpResponseRedirect # 30x
  26. from django.http import HttpResponseBadRequest, HttpResponseForbidden # 40x
  27. from django.http import HttpResponseServerError # 50x
  28. from django.views.decorators.http import require_POST
  29. from django.shortcuts import render_to_response
  30. from django.template import RequestContext
  31. try:
  32. from django.views.decorators.csrf import csrf_exempt
  33. except ImportError:
  34. # Django 1.0 compatibility
  35. def csrf_exempt(view_func):
  36. return view_func
  37. from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
  38. from saml2.client import Saml2Client
  39. from saml2.metadata import entity_descriptor
  40. from saml2.ident import code, decode
  41. from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
  42. from djangosaml2.cache import StateCache
  43. from djangosaml2.conf import get_config
  44. from djangosaml2.signals import post_authenticated
  45. from djangosaml2.utils import get_custom_setting, available_idps, get_location
  46. logger = logging.getLogger('djangosaml2')
  47. def _set_subject_id(session, subject_id):
  48. session['_saml2_subject_id'] = code(subject_id)
  49. def _get_subject_id(session):
  50. try:
  51. return decode(session['_saml2_subject_id'])
  52. except KeyError:
  53. return None
  54. def login(request,
  55. config_loader_path=None,
  56. wayf_template='djangosaml2/wayf.html',
  57. authorization_error_template='djangosaml2/auth_error.html'):
  58. """SAML Authorization Request initiator
  59. This view initiates the SAML2 Authorization handshake
  60. using the pysaml2 library to create the AuthnRequest.
  61. It uses the SAML 2.0 Http Redirect protocol binding.
  62. """
  63. logger.debug('Login process started')
  64. came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
  65. if not came_from:
  66. logger.warning('The next parameter exists but is empty')
  67. came_from = settings.LOGIN_REDIRECT_URL
  68. # if the user is already authenticated that maybe because of two reasons:
  69. # A) He has this URL in two browser windows and in the other one he
  70. # has already initiated the authenticated session.
  71. # B) He comes from a view that (incorrectly) send him here because
  72. # he does not have enough permissions. That view should have shown
  73. # an authorization error in the first place.
  74. # We can only make one thing here and that is configurable with the
  75. # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
  76. # is True (default value) we will redirect him to the came_from view.
  77. # Otherwise, we will show an (configurable) authorization error.
  78. if not request.user.is_anonymous():
  79. try:
  80. redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
  81. except AttributeError:
  82. redirect_authenticated_user = True
  83. if redirect_authenticated_user:
  84. return HttpResponseRedirect(came_from)
  85. else:
  86. logger.debug('User is already logged in')
  87. return render_to_response(authorization_error_template, {
  88. 'came_from': came_from,
  89. }, context_instance=RequestContext(request))
  90. selected_idp = request.GET.get('idp', None)
  91. conf = get_config(config_loader_path, request)
  92. # is a embedded wayf needed?
  93. idps = available_idps(conf)
  94. if selected_idp is None and len(idps) > 1:
  95. logger.debug('A discovery process is needed')
  96. return render_to_response(wayf_template, {
  97. 'available_idps': idps.items(),
  98. 'came_from': came_from,
  99. }, context_instance=RequestContext(request))
  100. client = Saml2Client(conf)
  101. try:
  102. (session_id, result) = client.prepare_for_authenticate(
  103. entityid=selected_idp, relay_state=came_from,
  104. binding=BINDING_HTTP_REDIRECT,
  105. )
  106. except TypeError, e:
  107. logger.error('Unable to know which IdP to use')
  108. return HttpResponse(unicode(e))
  109. logger.debug('Saving the session_id in the OutstandingQueries cache')
  110. oq_cache = OutstandingQueriesCache(request.session)
  111. oq_cache.set(session_id, came_from)
  112. logger.debug('Redirecting the user to the IdP')
  113. return HttpResponseRedirect(get_location(result))
  114. @require_POST
  115. @csrf_exempt
  116. def assertion_consumer_service(request,
  117. config_loader_path=None,
  118. attribute_mapping=None,
  119. create_unknown_user=None):
  120. """SAML Authorization Response endpoint
  121. The IdP will send its response to this view, which
  122. will process it with pysaml2 help and log the user
  123. in using the custom Authorization backend
  124. djangosaml2.backends.Saml2Backend that should be
  125. enabled in the settings.py
  126. """
  127. attribute_mapping = attribute_mapping or get_custom_setting(
  128. 'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
  129. create_unknown_user = create_unknown_user or get_custom_setting(
  130. 'SAML_CREATE_UNKNOWN_USER', True)
  131. logger.debug('Assertion Consumer Service started')
  132. conf = get_config(config_loader_path, request)
  133. if 'SAMLResponse' not in request.POST:
  134. return HttpResponseBadRequest(
  135. 'Couldn\'t find "SAMLResponse" in POST data.')
  136. xmlstr = request.POST['SAMLResponse']
  137. client = Saml2Client(conf, identity_cache=IdentityCache(request.session))
  138. oq_cache = OutstandingQueriesCache(request.session)
  139. outstanding_queries = oq_cache.outstanding_queries()
  140. # process the authentication response
  141. response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST,
  142. outstanding_queries)
  143. if response is None:
  144. logger.error('SAML response is None')
  145. return HttpResponseBadRequest(
  146. "SAML response has errors. Please check the logs")
  147. session_id = response.session_id()
  148. oq_cache.delete(session_id)
  149. # authenticate the remote user
  150. session_info = response.session_info()
  151. if callable(attribute_mapping):
  152. attribute_mapping = attribute_mapping()
  153. if callable(create_unknown_user):
  154. create_unknown_user = create_unknown_user()
  155. logger.debug('Trying to authenticate the user')
  156. user = auth.authenticate(session_info=session_info,
  157. attribute_mapping=attribute_mapping,
  158. create_unknown_user=create_unknown_user)
  159. if user is None:
  160. logger.error('The user is None')
  161. return HttpResponseForbidden("Permission denied")
  162. auth.login(request, user)
  163. _set_subject_id(request.session, session_info['name_id'])
  164. logger.debug('Sending the post_authenticated signal')
  165. post_authenticated.send_robust(sender=user, session_info=session_info)
  166. # redirect the user to the view where he came from
  167. relay_state = request.POST.get('RelayState', '/')
  168. if not relay_state:
  169. logger.warning('The RelayState parameter exists but is empty')
  170. relay_state = settings.LOGIN_REDIRECT_URL
  171. logger.debug('Redirecting to the RelayState: ' + relay_state)
  172. return HttpResponseRedirect(relay_state)
  173. @login_required
  174. def echo_attributes(request,
  175. config_loader_path=None,
  176. template='djangosaml2/echo_attributes.html'):
  177. """Example view that echo the SAML attributes of an user"""
  178. state = StateCache(request.session)
  179. conf = get_config(config_loader_path, request)
  180. client = Saml2Client(conf, state_cache=state,
  181. identity_cache=IdentityCache(request.session))
  182. subject_id = _get_subject_id(request.session)
  183. identity = client.users.get_identity(subject_id,
  184. check_not_on_or_after=False)
  185. return render_to_response(template, {'attributes': identity[0]},
  186. context_instance=RequestContext(request))
  187. @login_required
  188. def logout(request, config_loader_path=None):
  189. """SAML Logout Request initiator
  190. This view initiates the SAML2 Logout request
  191. using the pysaml2 library to create the LogoutRequest.
  192. """
  193. logger.debug('Logout process started')
  194. state = StateCache(request.session)
  195. conf = get_config(config_loader_path, request)
  196. client = Saml2Client(conf, state_cache=state,
  197. identity_cache=IdentityCache(request.session))
  198. subject_id = _get_subject_id(request.session)
  199. if subject_id is None:
  200. logger.warning(
  201. 'The session does not contains the subject id for user %s'
  202. % request.user)
  203. result = client.global_logout(subject_id)
  204. state.sync()
  205. if not result:
  206. logger.error("Looks like the user %s is not logged in any IdP/AA" % subject_id)
  207. return HttpResponseBadRequest("You are not logged in any IdP/AA")
  208. if len(result) > 1:
  209. logger.error('Sorry, I do not know how to logout from several sources. I will logout just from the first one')
  210. for entityid, logout_info in result.items():
  211. if isinstance(logout_info, tuple):
  212. binding, http_info = logout_info
  213. if binding == BINDING_HTTP_POST:
  214. logger.debug('Returning form to the IdP to continue the logout process')
  215. body = ''.join(http_info['data'])
  216. return HttpResponse(body)
  217. elif binding == BINDING_HTTP_REDIRECT:
  218. logger.debug('Redirecting to the IdP to continue the logout process')
  219. return HttpResponseRedirect(get_location(http_info))
  220. else:
  221. logger.error('Unknown binding: %s', binding)
  222. return HttpResponseServerError('Failed to log out')
  223. else:
  224. # We must have had a soap logout
  225. return finish_logout(request, logout_info)
  226. logger.error('Could not logout because there only the HTTP_REDIRECT is supported')
  227. return HttpResponseServerError('Logout Binding not supported')
  228. def logout_service(request, *args, **kwargs):
  229. return do_logout_service(request, request.GET, BINDING_HTTP_REDIRECT, *args, **kwargs)
  230. @csrf_exempt
  231. def logout_service_post(request, *args, **kwargs):
  232. return do_logout_service(request, request.POST, BINDING_HTTP_POST, *args, **kwargs)
  233. def do_logout_service(request, data, binding, config_loader_path=None, next_page=None,
  234. logout_error_template='djangosaml2/logout_error.html'):
  235. """SAML Logout Response endpoint
  236. The IdP will send the logout response to this view,
  237. which will process it with pysaml2 help and log the user
  238. out.
  239. Note that the IdP can request a logout even when
  240. we didn't initiate the process as a single logout
  241. request started by another SP.
  242. """
  243. logger.debug('Logout service started')
  244. conf = get_config(config_loader_path, request)
  245. state = StateCache(request.session)
  246. client = Saml2Client(conf, state_cache=state,
  247. identity_cache=IdentityCache(request.session))
  248. if 'SAMLResponse' in data: # we started the logout
  249. logger.debug('Receiving a logout response from the IdP')
  250. response = client.parse_logout_request_response(data['SAMLResponse'], binding)
  251. state.sync()
  252. return finish_logout(request, response, next_page=next_page)
  253. elif 'SAMLRequest' in data: # logout started by the IdP
  254. logger.debug('Receiving a logout request from the IdP')
  255. subject_id = _get_subject_id(request.session)
  256. if subject_id is None:
  257. logger.warning(
  258. 'The session does not contain the subject id for user %s. Performing local logout'
  259. % request.user)
  260. auth.logout(request)
  261. return render_to_response(logout_error_template, {},
  262. context_instance=RequestContext(request))
  263. else:
  264. http_info = client.handle_logout_request(
  265. data['SAMLRequest'],
  266. subject_id,
  267. binding)
  268. state.sync()
  269. auth.logout(request)
  270. return HttpResponseRedirect(get_location(http_info))
  271. else:
  272. logger.error('No SAMLResponse or SAMLRequest parameter found')
  273. raise Http404('No SAMLResponse or SAMLRequest parameter found')
  274. def finish_logout(request, response, next_page=None):
  275. if response and response.status_ok():
  276. if next_page is None and hasattr(settings, 'LOGOUT_REDIRECT_URL'):
  277. next_page = settings.LOGOUT_REDIRECT_URL
  278. logger.debug('Performing django_logout with a next_page of %s'
  279. % next_page)
  280. return django_logout(request, next_page=next_page)
  281. else:
  282. logger.error('Unknown error during the logout')
  283. return HttpResponse('Error during logout')
  284. def metadata(request, config_loader_path=None, valid_for=None):
  285. """Returns an XML with the SAML 2.0 metadata for this
  286. SP as configured in the settings.py file.
  287. """
  288. conf = get_config(config_loader_path, request)
  289. metadata = entity_descriptor(conf)
  290. return HttpResponse(content=str(metadata),
  291. content_type="text/xml; charset=utf8")
  292. def register_namespace_prefixes():
  293. from saml2 import md, saml, samlp
  294. import xmlenc
  295. import xmldsig
  296. prefixes = (('saml', saml.NAMESPACE),
  297. ('samlp', samlp.NAMESPACE),
  298. ('md', md.NAMESPACE),
  299. ('ds', xmldsig.NAMESPACE),
  300. ('xenc', xmlenc.NAMESPACE))
  301. if hasattr(ElementTree, 'register_namespace'):
  302. for prefix, namespace in prefixes:
  303. ElementTree.register_namespace(prefix, namespace)
  304. else:
  305. for prefix, namespace in prefixes:
  306. ElementTree._namespace_map[namespace] = prefix
  307. register_namespace_prefixes()