1 # -*- test-case-name: twisted.web.test.test_httpauth -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 A guard implementation which supports HTTP header-based authentication
9 If no I{Authorization} header is supplied, an anonymous login will be
10 attempted by using a L{Anonymous} credentials object. If such a header is
11 supplied and does not contain allowed credentials, or if anonymous login is
12 denied, a 401 will be sent in the response along with I{WWW-Authenticate}
13 headers for each of the allowed authentication schemes.
16 from zope.interface import implements
18 from twisted.python import log
19 from twisted.python.components import proxyForInterface
20 from twisted.web.resource import IResource, ErrorPage
21 from twisted.web import util
22 from twisted.cred import error
23 from twisted.cred.credentials import Anonymous
26 class UnauthorizedResource(object):
28 Simple IResource to escape Resource dispatch
34 def __init__(self, factories):
35 self._credentialFactories = factories
38 def render(self, request):
40 Send www-authenticate headers to the client
42 def generateWWWAuthenticate(scheme, challenge):
44 for k,v in challenge.iteritems():
45 l.append("%s=%s" % (k, quoteString(v)))
46 return "%s %s" % (scheme, ", ".join(l))
49 return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
51 request.setResponseCode(401)
52 for fact in self._credentialFactories:
53 challenge = fact.getChallenge(request)
54 request.responseHeaders.addRawHeader(
56 generateWWWAuthenticate(fact.scheme, challenge))
57 if request.method == 'HEAD':
62 def getChildWithDefault(self, path, request):
64 Disable resource dispatch
70 class HTTPAuthSessionWrapper(object):
72 Wrap a portal, enforcing supported header-based authentication schemes.
74 @ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
77 @ivar _credentialFactories: A list of L{ICredentialFactory} providers which
78 will be used to decode I{Authorization} headers into L{ICredentials}
84 def __init__(self, portal, credentialFactories):
86 Initialize a session wrapper
88 @type portal: C{Portal}
89 @param portal: The portal that will authenticate the remote client
91 @type credentialFactories: C{Iterable}
92 @param credentialFactories: The portal that will authenticate the
93 remote client based on one submitted C{ICredentialFactory}
96 self._credentialFactories = credentialFactories
99 def _authorizedResource(self, request):
101 Get the L{IResource} which the given request is authorized to receive.
102 If the proper authorization headers are present, the resource will be
103 requested from the portal. If not, an anonymous login attempt will be
106 authheader = request.getHeader('authorization')
108 return util.DeferredResource(self._login(Anonymous()))
110 factory, respString = self._selectParseHeader(authheader)
112 return UnauthorizedResource(self._credentialFactories)
114 credentials = factory.decode(respString, request)
115 except error.LoginFailed:
116 return UnauthorizedResource(self._credentialFactories)
118 log.err(None, "Unexpected failure from credentials factory")
119 return ErrorPage(500, None, None)
121 return util.DeferredResource(self._login(credentials))
124 def render(self, request):
126 Find the L{IResource} avatar suitable for the given request, if
127 possible, and render it. Otherwise, perhaps render an error page
128 requiring authorization or describing an internal server failure.
130 return self._authorizedResource(request).render(request)
133 def getChildWithDefault(self, path, request):
135 Inspect the Authorization HTTP header, and return a deferred which,
136 when fired after successful authentication, will return an authorized
137 C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
138 be returned, essentially halting further dispatch on the wrapped
139 resource and all children
141 # Don't consume any segments of the request - this class should be
143 request.postpath.insert(0, request.prepath.pop())
144 return self._authorizedResource(request)
147 def _login(self, credentials):
149 Get the L{IResource} avatar for the given credentials.
151 @return: A L{Deferred} which will be called back with an L{IResource}
152 avatar or which will errback if authentication fails.
154 d = self._portal.login(credentials, None, IResource)
155 d.addCallbacks(self._loginSucceeded, self._loginFailed)
159 def _loginSucceeded(self, (interface, avatar, logout)):
161 Handle login success by wrapping the resulting L{IResource} avatar
162 so that the C{logout} callback will be invoked when rendering is
165 class ResourceWrapper(proxyForInterface(IResource, 'resource')):
167 Wrap an L{IResource} so that whenever it or a child of it
168 completes rendering, the cred logout hook will be invoked.
170 An assumption is made here that exactly one L{IResource} from
171 among C{avatar} and all of its children will be rendered. If
172 more than one is rendered, C{logout} will be invoked multiple
173 times and probably earlier than desired.
175 def getChildWithDefault(self, name, request):
177 Pass through the lookup to the wrapped resource, wrapping
178 the result in L{ResourceWrapper} to ensure C{logout} is
179 called when rendering of the child is complete.
181 return ResourceWrapper(self.resource.getChildWithDefault(name, request))
183 def render(self, request):
185 Hook into response generation so that when rendering has
186 finished completely (with or without error), C{logout} is
189 request.notifyFinish().addBoth(lambda ign: logout())
190 return super(ResourceWrapper, self).render(request)
192 return ResourceWrapper(avatar)
195 def _loginFailed(self, result):
197 Handle login failure by presenting either another challenge (for
198 expected authentication/authorization-related failures) or a server
199 error page (for anything else).
201 if result.check(error.Unauthorized, error.LoginFailed):
202 return UnauthorizedResource(self._credentialFactories)
206 "HTTPAuthSessionWrapper.getChildWithDefault encountered "
208 return ErrorPage(500, None, None)
211 def _selectParseHeader(self, header):
213 Choose an C{ICredentialFactory} from C{_credentialFactories}
214 suitable to use to decode the given I{Authenticate} header.
216 @return: A two-tuple of a factory and the remaining portion of the
217 header value to be decoded or a two-tuple of C{None} if no
218 factory can decode the header value.
220 elements = header.split(' ')
221 scheme = elements[0].lower()
222 for fact in self._credentialFactories:
223 if fact.scheme == scheme:
224 return (fact, ' '.join(elements[1:]))