1
# -*- test-case-name: twisted.web.test.test_httpauth -*-
2
# Copyright (c) 2008-2009 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))
60
def getChildWithDefault(self, path, request):
62
Disable resource dispatch
68
class HTTPAuthSessionWrapper(object):
70
Wrap a portal, enforcing supported header-based authentication schemes.
72
@ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
75
@ivar _credentialFactories: A list of L{ICredentialFactory} providers which
76
will be used to decode I{Authorization} headers into L{ICredentials}
82
def __init__(self, portal, credentialFactories):
84
Initialize a session wrapper
86
@type portal: C{Portal}
87
@param portal: The portal that will authenticate the remote client
89
@type credentialFactories: C{Iterable}
90
@param credentialFactories: The portal that will authenticate the
91
remote client based on one submitted C{ICredentialFactory}
94
self._credentialFactories = credentialFactories
97
def _authorizedResource(self, request):
99
Get the L{IResource} which the given request is authorized to receive.
100
If the proper authorization headers are present, the resource will be
101
requested from the portal. If not, an anonymous login attempt will be
104
authheader = request.getHeader('authorization')
106
return util.DeferredResource(self._login(Anonymous()))
108
factory, respString = self._selectParseHeader(authheader)
110
return UnauthorizedResource(self._credentialFactories)
112
credentials = factory.decode(respString, request)
113
except error.LoginFailed:
114
return UnauthorizedResource(self._credentialFactories)
116
log.err(None, "Unexpected failure from credentials factory")
117
return ErrorPage(500, None, None)
119
return util.DeferredResource(self._login(credentials))
122
def render(self, request):
124
Find the L{IResource} avatar suitable for the given request, if
125
possible, and render it. Otherwise, perhaps render an error page
126
requiring authorization or describing an internal server failure.
128
return self._authorizedResource(request).render(request)
131
def getChildWithDefault(self, path, request):
133
Inspect the Authorization HTTP header, and return a deferred which,
134
when fired after successful authentication, will return an authorized
135
C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
136
be returned, essentially halting further dispatch on the wrapped
137
resource and all children
139
# Don't consume any segments of the request - this class should be
141
request.postpath.insert(0, request.prepath.pop())
142
return self._authorizedResource(request)
145
def _login(self, credentials):
147
Get the L{IResource} avatar for the given credentials.
149
@return: A L{Deferred} which will be called back with an L{IResource}
150
avatar or which will errback if authentication fails.
152
d = self._portal.login(credentials, None, IResource)
153
d.addCallbacks(self._loginSucceeded, self._loginFailed)
157
def _loginSucceeded(self, (interface, avatar, logout)):
159
Handle login success by wrapping the resulting L{IResource} avatar
160
so that the C{logout} callback will be invoked when rendering is
163
class ResourceWrapper(proxyForInterface(IResource, 'resource')):
165
Wrap an L{IResource} so that whenever it or a child of it
166
completes rendering, the cred logout hook will be invoked.
168
An assumption is made here that exactly one L{IResource} from
169
among C{avatar} and all of its children will be rendered. If
170
more than one is rendered, C{logout} will be invoked multiple
171
times and probably earlier than desired.
173
def getChildWithDefault(self, name, request):
175
Pass through the lookup to the wrapped resource, wrapping
176
the result in L{ResourceWrapper} to ensure C{logout} is
177
called when rendering of the child is complete.
179
return ResourceWrapper(self.resource.getChildWithDefault(name, request))
181
def render(self, request):
183
Hook into response generation so that when rendering has
184
finished completely, C{logout} is called.
186
request.notifyFinish().addCallback(lambda ign: logout())
187
return super(ResourceWrapper, self).render(request)
189
return ResourceWrapper(avatar)
192
def _loginFailed(self, result):
194
Handle login failure by presenting either another challenge (for
195
expected authentication/authorization-related failures) or a server
196
error page (for anything else).
198
if result.check(error.Unauthorized, error.LoginFailed):
199
return UnauthorizedResource(self._credentialFactories)
203
"HTTPAuthSessionWrapper.getChildWithDefault encountered "
205
return ErrorPage(500, None, None)
208
def _selectParseHeader(self, header):
210
Choose an C{ICredentialFactory} from C{_credentialFactories}
211
suitable to use to decode the given I{Authenticate} header.
213
@return: A two-tuple of a factory and the remaining portion of the
214
header value to be decoded or a two-tuple of C{None} if no
215
factory can decode the header value.
217
elements = header.split(' ')
218
scheme = elements[0].lower()
219
for fact in self._credentialFactories:
220
if fact.scheme == scheme:
221
return (fact, ' '.join(elements[1:]))