wrapper.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. # -*- test-case-name: twisted.web.test.test_httpauth -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. A guard implementation which supports HTTP header-based authentication
  6. schemes.
  7. If no I{Authorization} header is supplied, an anonymous login will be
  8. attempted by using a L{Anonymous} credentials object. If such a header is
  9. supplied and does not contain allowed credentials, or if anonymous login is
  10. denied, a 401 will be sent in the response along with I{WWW-Authenticate}
  11. headers for each of the allowed authentication schemes.
  12. """
  13. from zope.interface import implementer
  14. from twisted.cred import error
  15. from twisted.cred.credentials import Anonymous
  16. from twisted.logger import Logger
  17. from twisted.python.components import proxyForInterface
  18. from twisted.web import util
  19. from twisted.web.resource import IResource, _UnsafeErrorPage
  20. @implementer(IResource)
  21. class UnauthorizedResource:
  22. """
  23. Simple IResource to escape Resource dispatch
  24. """
  25. isLeaf = True
  26. def __init__(self, factories):
  27. self._credentialFactories = factories
  28. def render(self, request):
  29. """
  30. Send www-authenticate headers to the client
  31. """
  32. def ensureBytes(s):
  33. return s.encode("ascii") if isinstance(s, str) else s
  34. def generateWWWAuthenticate(scheme, challenge):
  35. lst = []
  36. for k, v in challenge.items():
  37. k = ensureBytes(k)
  38. v = ensureBytes(v)
  39. lst.append(k + b"=" + quoteString(v))
  40. return b" ".join([scheme, b", ".join(lst)])
  41. def quoteString(s):
  42. return b'"' + s.replace(b"\\", rb"\\").replace(b'"', rb"\"") + b'"'
  43. request.setResponseCode(401)
  44. for fact in self._credentialFactories:
  45. challenge = fact.getChallenge(request)
  46. request.responseHeaders.addRawHeader(
  47. b"www-authenticate", generateWWWAuthenticate(fact.scheme, challenge)
  48. )
  49. if request.method == b"HEAD":
  50. return b""
  51. return b"Unauthorized"
  52. def getChildWithDefault(self, path, request):
  53. """
  54. Disable resource dispatch
  55. """
  56. return self
  57. def putChild(self, path, child):
  58. # IResource.putChild
  59. raise NotImplementedError()
  60. @implementer(IResource)
  61. class HTTPAuthSessionWrapper:
  62. """
  63. Wrap a portal, enforcing supported header-based authentication schemes.
  64. @ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
  65. avatars.
  66. @ivar _credentialFactories: A list of L{ICredentialFactory} providers which
  67. will be used to decode I{Authorization} headers into L{ICredentials}
  68. providers.
  69. """
  70. isLeaf = False
  71. _log = Logger()
  72. def __init__(self, portal, credentialFactories):
  73. """
  74. Initialize a session wrapper
  75. @type portal: C{Portal}
  76. @param portal: The portal that will authenticate the remote client
  77. @type credentialFactories: C{Iterable}
  78. @param credentialFactories: The portal that will authenticate the
  79. remote client based on one submitted C{ICredentialFactory}
  80. """
  81. self._portal = portal
  82. self._credentialFactories = credentialFactories
  83. def _authorizedResource(self, request):
  84. """
  85. Get the L{IResource} which the given request is authorized to receive.
  86. If the proper authorization headers are present, the resource will be
  87. requested from the portal. If not, an anonymous login attempt will be
  88. made.
  89. """
  90. authheader = request.getHeader(b"authorization")
  91. if not authheader:
  92. return util.DeferredResource(self._login(Anonymous()))
  93. factory, respString = self._selectParseHeader(authheader)
  94. if factory is None:
  95. return UnauthorizedResource(self._credentialFactories)
  96. try:
  97. credentials = factory.decode(respString, request)
  98. except error.LoginFailed:
  99. return UnauthorizedResource(self._credentialFactories)
  100. except BaseException:
  101. self._log.failure("Unexpected failure from credentials factory")
  102. return _UnsafeErrorPage(500, "Internal Error", "")
  103. else:
  104. return util.DeferredResource(self._login(credentials))
  105. def render(self, request):
  106. """
  107. Find the L{IResource} avatar suitable for the given request, if
  108. possible, and render it. Otherwise, perhaps render an error page
  109. requiring authorization or describing an internal server failure.
  110. """
  111. return self._authorizedResource(request).render(request)
  112. def getChildWithDefault(self, path, request):
  113. """
  114. Inspect the Authorization HTTP header, and return a deferred which,
  115. when fired after successful authentication, will return an authorized
  116. C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
  117. be returned, essentially halting further dispatch on the wrapped
  118. resource and all children
  119. """
  120. # Don't consume any segments of the request - this class should be
  121. # transparent!
  122. request.postpath.insert(0, request.prepath.pop())
  123. return self._authorizedResource(request)
  124. def _login(self, credentials):
  125. """
  126. Get the L{IResource} avatar for the given credentials.
  127. @return: A L{Deferred} which will be called back with an L{IResource}
  128. avatar or which will errback if authentication fails.
  129. """
  130. d = self._portal.login(credentials, None, IResource)
  131. d.addCallbacks(self._loginSucceeded, self._loginFailed)
  132. return d
  133. def _loginSucceeded(self, args):
  134. """
  135. Handle login success by wrapping the resulting L{IResource} avatar
  136. so that the C{logout} callback will be invoked when rendering is
  137. complete.
  138. """
  139. interface, avatar, logout = args
  140. class ResourceWrapper(proxyForInterface(IResource, "resource")):
  141. """
  142. Wrap an L{IResource} so that whenever it or a child of it
  143. completes rendering, the cred logout hook will be invoked.
  144. An assumption is made here that exactly one L{IResource} from
  145. among C{avatar} and all of its children will be rendered. If
  146. more than one is rendered, C{logout} will be invoked multiple
  147. times and probably earlier than desired.
  148. """
  149. def getChildWithDefault(self, name, request):
  150. """
  151. Pass through the lookup to the wrapped resource, wrapping
  152. the result in L{ResourceWrapper} to ensure C{logout} is
  153. called when rendering of the child is complete.
  154. """
  155. return ResourceWrapper(self.resource.getChildWithDefault(name, request))
  156. def render(self, request):
  157. """
  158. Hook into response generation so that when rendering has
  159. finished completely (with or without error), C{logout} is
  160. called.
  161. """
  162. request.notifyFinish().addBoth(lambda ign: logout())
  163. return super().render(request)
  164. return ResourceWrapper(avatar)
  165. def _loginFailed(self, result):
  166. """
  167. Handle login failure by presenting either another challenge (for
  168. expected authentication/authorization-related failures) or a server
  169. error page (for anything else).
  170. """
  171. if result.check(error.Unauthorized, error.LoginFailed):
  172. return UnauthorizedResource(self._credentialFactories)
  173. else:
  174. self._log.failure(
  175. "HTTPAuthSessionWrapper.getChildWithDefault encountered "
  176. "unexpected error",
  177. failure=result,
  178. )
  179. return _UnsafeErrorPage(500, "Internal Error", "")
  180. def _selectParseHeader(self, header):
  181. """
  182. Choose an C{ICredentialFactory} from C{_credentialFactories}
  183. suitable to use to decode the given I{Authenticate} header.
  184. @return: A two-tuple of a factory and the remaining portion of the
  185. header value to be decoded or a two-tuple of L{None} if no
  186. factory can decode the header value.
  187. """
  188. elements = header.split(b" ")
  189. scheme = elements[0].lower()
  190. for fact in self._credentialFactories:
  191. if fact.scheme == scheme:
  192. return (fact, b" ".join(elements[1:]))
  193. return (None, None)
  194. def putChild(self, path, child):
  195. # IResource.putChild
  196. raise NotImplementedError()