def test_login_timeout(mocker, requests_mock): store = {} handler = indieauth.IndieAuth('http://client/', tokens.DictStore(store), 10) mock_time = mocker.patch('time.time') mock_time.return_value = 238742 requests_mock.get( 'http://example.user/', text='hello', headers={'Link': '<http://endpoint/>; rel="authorization_endpoint"'}) assert len(store) == 0 response = handler.initiate_auth('http://example.user', 'http://client/cb', '/dest') assert isinstance(response, disposition.Redirect) assert len(store) == 1 mock_time.return_value += 100 data = {'state': parse_args(response.url)['state'], 'code': 'bogus'} response = handler.check_callback('http://client/cb', data, {}) assert isinstance(response, disposition.Error) assert 'timed out' in response.message assert len(store) == 0
def test_security_requirements(): with pytest.raises(Exception): indieauth.IndieAuth('foobarbaz', tokens.Serializer('qwerpoiu'))
def test_handler_failures(requests_mock): store = {} handler = indieauth.IndieAuth('http://client/', tokens.DictStore(store), 10) # Attempt to auth against page with no endpoint requests_mock.get('http://no-endpoint/', text='hello') response = handler.initiate_auth('http://no-endpoint/', 'http://cb/', 'bogus') assert isinstance(response, disposition.Error) assert 'endpoint' in response.message assert len(store) == 0 # Attempt to inject a transaction-less callback response response = handler.check_callback('http://no-transaction', {}, {}) assert isinstance(response, disposition.Error) assert 'No transaction' in response.message assert len(store) == 0 # Attempt to inject a forged callback response response = handler.check_callback('http://bogus-transaction', {'state': 'bogus'}, {}) assert isinstance(response, disposition.Error) assert 'Invalid token' in response.message assert len(store) == 0 # Get a valid state token requests_mock.get( 'http://example.user/', text='hello', headers={'Link': '<http://endpoint/>; rel="authorization_endpoint"'}) response = handler.initiate_auth('http://example.user', 'http://client/cb', '/dest') assert isinstance(response, disposition.Redirect) data = {'state': parse_args(response.url)['state']} assert len(store) == 1 # no code assigned assert "Missing 'code'" in handler.check_callback('http://client/cb', data, {}).message assert len(store) == 0 def check_failure(message): assert len(store) == 0 response = handler.initiate_auth('http://example.user', 'http://client/cb', '/dest') assert isinstance(response, disposition.Redirect) assert len(store) == 1 data = {'state': parse_args(response.url)['state'], 'code': 'bogus'} response = handler.check_callback('http://client/cb', data, {}) assert isinstance(response, disposition.Error) assert message in response.message assert len(store) == 0 # callback returns error requests_mock.post('http://endpoint/', status_code=400) check_failure('returned 400') # callback returns broken JSON requests_mock.post('http://endpoint/', text='invalid json') check_failure('invalid response JSON') # callback returns a page with no endpoint requests_mock.post('http://endpoint/', json={'me': 'http://empty.user'}) requests_mock.get('http://empty.user', text='hello') check_failure('missing IndieAuth endpoint') # callback returns a page with a different endpoint requests_mock.post('http://endpoint/', json={'me': 'http://different.user'}) requests_mock.get( 'http://different.user', headers={ 'Link': '<http://otherendpoint/>; rel="authorization_endpoint"' }) check_failure('Authorization endpoint mismatch')
def test_handler_success(requests_mock): store = {} handler = indieauth.IndieAuth('http://client/', tokens.DictStore(store)) assert handler.service_name == 'IndieAuth' assert handler.url_schemes assert 'IndieAuth' in handler.description assert handler.cb_id assert handler.logo_html[0][1] == 'IndieAuth' # profile page at http://example.user/ which redirects to https://example.user/bob endpoint = { 'Link': '<https://auth.example/endpoint>; rel="authorization_endpoint' } requests_mock.get('http://example.user/', headers=endpoint) requests_mock.get('https://example.user/bob', headers=endpoint) injected = requests.get('http://example.user/') # it should not handle the URL on its own assert not handler.handles_url('http://example.user/') assert handler.handles_page('http://example.user/', injected.headers, BeautifulSoup(injected.text, 'html.parser'), injected.links) # and now the URL should be cached assert handler.handles_url('http://example.user/') disp = handler.initiate_auth('http://example.user/', 'http://client/cb', '/dest') assert isinstance(disp, disposition.Redirect) assert disp.url.startswith('https://auth.example/endpoint') # fake the user dialog on the IndieAuth endpoint user_get = parse_args(disp.url) assert user_get['redirect_uri'].startswith('http://client/cb') assert 'client_id' in user_get assert 'state' in user_get assert user_get['state'] in store assert user_get['response_type'] == 'code' assert 'me' in user_get challenge = user_get['code_challenge'] assert user_get['code_challenge_method'] == 'S256' # fake the verification response def verify_callback(request, _): import urllib.parse args = urllib.parse.parse_qs(request.text) assert args['code'] == ['asdf'] assert args['client_id'] == ['http://client/'] assert 'redirect_uri' in args verifier = args['code_verifier'][0] assert utils.pkce_challenge(verifier) == challenge return json.dumps({ 'me': 'https://example.user/bob', 'profile': { 'email': '*****@*****.**', 'url': 'https://bob.example.user/' } }) requests_mock.post('https://auth.example/endpoint', text=verify_callback) LOGGER.debug("state=%s", user_get['state']) response = handler.check_callback(user_get['redirect_uri'], { 'state': user_get['state'], 'code': 'asdf', }, {}) LOGGER.debug("verification response: %s", response) assert isinstance(response, disposition.Verified) assert response.identity == 'https://example.user/bob' assert response.redir == '/dest' for key, val in { # provided by profile scope 'homepage': 'https://bob.example.user/', 'email': '*****@*****.**', }.items(): assert response.profile[key] == val # trying to replay the same transaction should fail response = handler.check_callback(user_get['redirect_uri'], { 'state': user_get['state'], 'code': 'asdf', }, {}) assert isinstance(response, disposition.Error)