def test_missingOptionalParameterJSON(self): # type: () -> None """ If a required Field is missing from the JSON body, its default value is used. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/notrequired", json=dict(name="one"), headers={b"X-Test-Session": session.identifier}, )) response2 = self.successResultOf( stub.post( "https://localhost/notrequired", json=dict(name="two", value=2), headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertEqual(response2.code, 200) self.assertEqual(self.successResultOf(content(response)), b"okay") self.assertEqual(self.successResultOf(content(response2)), b"okay") self.assertEqual(to.calls, [("one", 7.0), ("two", 2.0)])
def test_numberConstraints(self): # type: () -> None """ Number parameters have minimum and maximum validations and the object will not be called when the values exceed them. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) tooLow = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='1'), headers={b'X-Test-Session': session.identifier})) tooHigh = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='20'), headers={b'X-Test-Session': session.identifier})) justRight = self.successResultOf( stub.post('https://localhost/constrained', data=dict(goldilocks='7'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(tooHigh.code, 400) self.assertEqual(tooLow.code, 400) self.assertEqual(justRight.code, 200) self.assertEqual(self.successResultOf(content(justRight)), b'got it') self.assertEqual(to.calls, [(u'constrained', 7)])
def test_cookieWithToken(self): # type: () -> None """ A cookie-authenticated, CRSF-protected form will call the form as expected. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Cookie)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", data=dict( name="hello", value="1234", ignoreme="extraneous", __csrf_protection__=session.identifier, ), cookies={ "Klein-Secure-Session": nativeString(session.identifier) }, )) self.assertEqual(to.calls, [("hello", 1234)]) self.assertEqual(response.code, 200) self.assertIn(b"yay", self.successResultOf(content(response)))
def test_customValidationHandling(self): # type: () -> None """ L{Form.onValidationFailureFor} handles form validation failures by handing its thing a renderable form. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) testobj = TestObject(mem) stub = StubTreq(testobj.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle-validation", headers={b"X-Test-Session": session.identifier}, json={"value": 300}, )) self.assertEqual(response.code, 200) self.assertIn( response.headers.getRawHeaders(b"content-type")[0], b"text/html") responseText = self.successResultOf(content(response)) self.assertEqual(responseText, b"~special~") self.assertEqual( [(k.pythonArgumentName, v) for k, v in testobj.calls[-1][1].prevalidationValues.items()], [("value", 300)], )
def test_missingRequiredParameter(self): # type: () -> None """ If required fields are missing, a default error form is presented and the form's handler is not called. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", data=dict(), headers={b"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 400) self.assertIn( b"a value was required but none was supplied", self.successResultOf(content(response)), ) self.assertEqual(to.calls, [])
def test_noSessionPOST(self): # type: () -> None """ An unauthenticated, CSRF-protected form will return a 403 Forbidden status code. """ mem = MemorySessionStore() to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/handle', data=dict(name='hello', value='1234'))) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) self.assertIn(b'CSRF', self.successResultOf(content(response)))
def test_noName(self): # type: () -> None """ A handler for a Form with a Field that doesn't have a name will return an error explaining the problem. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/dangling-param', data=dict(), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 500) errors = self.flushLoggedErrors(ValueError) self.assertEqual(len(errors), 1) self.assertIn(str(errors[0].value), "Cannot extract unnamed form field.")
def test_cookieNoToken(self): # type: () -> None """ A cookie-authenticated, CSRF-protected form will return a 403 Forbidden status code when a CSRF protection token is not supplied. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Cookie) ) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf(stub.post( 'https://localhost/handle', data=dict(name='hello', value='1234', ignoreme='extraneous'), cookies={"Klein-Secure-Session": nativeString(session.identifier)} )) self.assertEqual(to.calls, []) self.assertEqual(response.code, 403) self.assertIn(b'CSRF', self.successResultOf(content(response)))
def test_handlingPassword(self): # type: () -> None """ From the perspective of form handling, passwords are handled like strings. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/password-field', data=dict(pw='asdfjkl;'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b'password received') self.assertEqual(to.calls, [(u'password', u'asdfjkl;')])
def test_handlingJSON(self) -> None: """ A handler for a form with Fields receives those fields as input, as passed by an HTTP client that submits a JSON POST body. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post( "https://localhost/handle", json=dict(name="hello", value="1234", ignoreme="extraneous"), headers={"X-Test-Session": session.identifier}, )) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b"yay") self.assertEqual(to.calls, [("hello", 1234)])
def test_handling(self): # type: () -> None """ A handler for a Form with Fields receives those fields as input, as passed by an HTTP client. """ mem = MemorySessionStore() session = self.successResultOf( mem.newSession(True, SessionMechanism.Header)) to = TestObject(mem) stub = StubTreq(to.router.resource()) response = self.successResultOf( stub.post('https://localhost/handle', data=dict(name='hello', value='1234', ignoreme='extraneous'), headers={b'X-Test-Session': session.identifier})) self.assertEqual(response.code, 200) self.assertEqual(self.successResultOf(content(response)), b'yay') self.assertEqual(to.calls, [(u'hello', 1234)])
class CsauthTests(unittest.TestCase): """ Tests for L{txwebutils.csauth}. """ WEB_NAME = u"web.localhost" AUTH_NAME = u"auth.localhost" SITE_A_NAME = u"Site Alpha" SITE_A_TOKEN = u"site a token" SITE_A_SECRET = u"secret for a" SITE_B_NAME = u"Site Beta" SITE_B_TOKEN = u"site B token" SITE_B_SECRET = u"secret for _b_" USER_A_NAME = u"UserA" USER_A_PSWD = u"PswdA" USER_B_NAME = u"IAmUserB" USER_B_PSWD = u"P-S-W-D_B" def setUp(self): # prepare the authentication systems self.userdb = cred.InMemoryUnicodeUsernamePasswordDatabase() self.sitedb = cred.InMemoryUnicodeUsernamePasswordDatabase() self.userrealm = TestUserRealm() self.siterealm = TestSiteRealm() self.userportal = portal.Portal(self.userrealm) self.siteportal = portal.Portal(self.siterealm) self.userportal.registerChecker(self.userdb) self.siteportal.registerChecker(self.sitedb) # register tokens self.sitedb.addUser( self.SITE_A_TOKEN, self.SITE_A_SECRET, ) self.siterealm.register_csauth_perm( self.SITE_A_TOKEN, TestWebAuthPermission( self.SITE_A_NAME, "http://{}/login".format(self.WEB_NAME), full_username=True, ), ) self.sitedb.addUser( self.SITE_B_TOKEN, self.SITE_B_SECRET, ) self.siterealm.register_csauth_perm( self.SITE_B_TOKEN, TestWebAuthPermission( self.SITE_B_NAME, "http://{}/bpage".format(self.WEB_NAME), full_username=False, ), ) # register Users self.userdb.addUser( self.USER_A_NAME, self.USER_A_PSWD, ) self.userdb.addUser( self.USER_B_NAME, self.USER_B_PSWD, ) # prepare the auth site lr = BasicLoginResource() authpage = AuthResource( self.siteportal, self.userportal, lr, url=u"http://{}/auth".format(self.AUTH_NAME), ) arr = DebugResource("auth") arr.putChild(b"auth", authpage) arr.putChild(b"expire", SessionExpiringResource()) # prepare the web site lrr = DebugResource("web") # add the login page loginpage = TestLoginResource( u"http://{}/auth".format(self.AUTH_NAME), self.SITE_A_TOKEN, self.SITE_A_SECRET, ) lrr.putChild(b"login", loginpage) # add a login page with a different permission bpage = TestLoginResource( u"http://{}/auth".format(self.AUTH_NAME), self.SITE_B_TOKEN, self.SITE_B_SECRET, ) lrr.putChild(b"bpage", bpage) # add a resource with an invalid token/secret combination invalidepage = TestLoginResource( u"http://{}/auth".format(self.AUTH_NAME), self.SITE_A_TOKEN, self.SITE_B_SECRET, # <- site B secret ) lrr.putChild(b"invalid", invalidepage) # create and install treq stubs supersite = NameVirtualHost() supersite.addHost(self.AUTH_NAME.encode("ascii"), arr) supersite.addHost(self.WEB_NAME.encode("ascii"), lrr) supersite.default = DebugResource("default") self.treq = StubTreq(supersite) loginpage._set_request_dispatcher(self.treq) bpage._set_request_dispatcher(self.treq) invalidepage._set_request_dispatcher(self.treq) @defer.inlineCallbacks def test_login_correct(self): """ Test a correct login. """ # first, GET loginpage r = yield self.treq.get(u"http://{}/login".format(self.WEB_NAME), ) cookies = r.cookies() self.assertEqual(r.code, 200) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were redirected to the auth server self.assertNotIn(six.text_type(self.WEB_NAME), pa_url) self.assertIn(six.text_type(self.AUTH_NAME), pa_url) # ensure paramters were set correctly self.assertIn(u"action=login", pa_url) self.assertIn(u"ctoken=", pa_url) # POST correct login userdata r = yield self.treq.post( pa_url, params={ u"username": self.USER_A_NAME, u"password": self.USER_A_PSWD, }, cookies=cookies, browser_like_redirects=True, ) self.assertEqual(r.code, 200) url = r.request.absoluteURI.decode("ascii") # ensure we were redirected back to webserver self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(six.text_type(self.WEB_NAME), url) # ensure content was set correctly text = yield r.text() self.assertEqual(text, self.USER_A_NAME) # ensure cookies will keep us logged in cr = yield self.treq.get( u"http://{}/login".format(self.WEB_NAME), cookies=cookies, ) url = cr.request.absoluteURI.decode("ascii") self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(six.text_type(self.WEB_NAME), url) # expire session er = yield self.treq.get( u"http://{}/expire".format(self.AUTH_NAME), cookies=cookies, ) self.assertEqual(er.code, 200) # ensure we will no longer be logged in automatically cr = yield self.treq.get( u"http://{}/login".format(self.WEB_NAME), cookies=cookies, ) url = cr.request.absoluteURI.decode("ascii") self.assertNotIn(six.text_type(self.WEB_NAME), url) self.assertIn(six.text_type(self.AUTH_NAME), url) @defer.inlineCallbacks def test_login_different_perm_correct(self): """ Test a correct login with site b, which has different permissions. """ # first, GET loginpage r = yield self.treq.get(u"http://{}/bpage".format(self.WEB_NAME), ) cookies = r.cookies() self.assertEqual(r.code, 200) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were redirected to the auth server self.assertNotIn(six.text_type(self.WEB_NAME), pa_url) self.assertIn(six.text_type(self.AUTH_NAME), pa_url) # ensure paramters were set correctly self.assertIn(u"action=login", pa_url) self.assertIn(u"ctoken=", pa_url) # POST correct login userdata # we use user b this time, so we also check whether different # users work r = yield self.treq.post( pa_url, params={ u"username": self.USER_B_NAME, u"password": self.USER_B_PSWD, }, cookies=cookies, browser_like_redirects=True, ) self.assertEqual(r.code, 200) url = r.request.absoluteURI.decode("ascii") # ensure we were redirected back to webserver self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(six.text_type(self.WEB_NAME), url) # ensure we are at the bpade self.assertIn(u"bpage", url) # ensure content was set correctly text = yield r.text() self.assertEqual(text, six.text_type(len(self.USER_B_NAME))) # ensure cookies will keep us logged in cr = yield self.treq.get( u"http://{}/bpage".format(self.WEB_NAME), cookies=cookies, ) url = cr.request.absoluteURI.decode("ascii") self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(six.text_type(self.WEB_NAME), url) self.assertIn(u"bpage", url) @defer.inlineCallbacks def test_crossite_cookies(self): """ Test that cookies will kept you logged in across sites. """ # first, GET loginpage r = yield self.treq.get(u"http://{}/login".format(self.WEB_NAME), ) cookies = r.cookies() self.assertEqual(r.code, 200) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were redirected to the auth server self.assertNotIn(six.text_type(self.WEB_NAME), pa_url) self.assertIn(six.text_type(self.AUTH_NAME), pa_url) self.assertIn(u"login", pa_url) # ensure paramters were set correctly self.assertIn(u"action=login", pa_url) self.assertIn(u"ctoken=", pa_url) # POST correct login userdata r = yield self.treq.post( pa_url, params={ u"username": self.USER_A_NAME, u"password": self.USER_A_PSWD, }, cookies=cookies, browser_like_redirects=True, ) self.assertEqual(r.code, 200) url = r.request.absoluteURI.decode("ascii") # ensure we were redirected back to webserver self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(six.text_type(self.WEB_NAME), url) # ensure content was set correctly text = yield r.text() self.assertEqual(text, self.USER_A_NAME) # GET bpage r = yield self.treq.get( u"http://{}/bpage".format(self.WEB_NAME), cookies=cookies, ) self.assertEqual(r.code, 200) url = r.request.absoluteURI.decode("ascii") # ensure we were not redirected to the auth server self.assertIn(six.text_type(self.WEB_NAME), url) self.assertNotIn(six.text_type(self.AUTH_NAME), url) self.assertIn(u"bpage", url) # ensure content is still correct # this is important, since the different sites have different # access to the userdata text = yield r.text() self.assertEqual(text, six.text_type(len(self.USER_A_NAME))) @defer.inlineCallbacks def test_login_invalid(self): """ Test an invalid login. """ # first, GET loginpage r = yield self.treq.get(u"http://{}/login".format(self.WEB_NAME), ) cookies = r.cookies() self.assertEqual(r.code, 200) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were redirected to the auth server self.assertNotIn(six.text_type(self.WEB_NAME), pa_url) self.assertIn(six.text_type(self.AUTH_NAME), pa_url) # ensure paramters were set correctly self.assertIn(u"action=login", pa_url) self.assertIn(u"ctoken=", pa_url) # POST incorrect login userdata r = yield self.treq.post( pa_url, params={ u"username": self.USER_A_NAME, u"password": self.USER_B_PSWD, # <- user b password }, cookies=cookies, browser_like_redirects=True, ) self.assertEqual(r.code, 401) url = r.request.absoluteURI.decode("ascii") # ensure we were not redirected back to webserver self.assertIn(six.text_type(self.AUTH_NAME), url) self.assertNotIn(six.text_type(self.WEB_NAME), url) # ensure a malicious request back to the webserver will fail qs = urlparse(url).query ctoken = parse_qs(qs)[u"ctoken"] r = yield self.treq.get( u"http://{}/login?action=callback&ctoken={}".format( self.WEB_NAME, ctoken, ), cookies=cookies, ) cookies = r.cookies() self.assertEqual(r.code, 200) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were redirected to the auth server self.assertNotIn(six.text_type(self.WEB_NAME), pa_url) self.assertIn(six.text_type(self.AUTH_NAME), pa_url) @defer.inlineCallbacks def test_token_secret_invalid(self): """ Test an invalid token/secret. """ # first, GET loginpage r = yield self.treq.get(u"http://{}/invalid".format(self.WEB_NAME), ) cookies = r.cookies() self.assertEqual(r.code, 500) pa_url = r.request.absoluteURI.decode("ascii") # ensure we were not redirected to the auth server self.assertIn(six.text_type(self.WEB_NAME), pa_url) self.assertNotIn(six.text_type(self.AUTH_NAME), pa_url)