Exemplo n.º 1
0
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
Exemplo n.º 2
0
def test_handles_url(requests_mock):
    handler = fediverse.Fediverse('test',
                                  tokens.DictStore(),
                                  homepage='http://foo.example/')

    requests_mock.get('https://mastodon.example/api/v1/instance',
                      text=json.dumps({
                          'uri': 'foo',
                          'version': '2.5.1',
                          'urls': 'foo.bar'
                      }))

    requests_mock.get('https://not-mastodon.example/api/v1/instance',
                      text=json.dumps({'moo': 'cow'}))

    requests_mock.get('https://also-not.example/api/v1/instance',
                      status_code=404)

    assert handler.handles_url('https://mastodon.example/@fluffy')
    assert handler.handles_url('https://mastodon.example/')
    assert handler.handles_url('mastodon.example')
    assert not handler.handles_url('https://not-mastodon.example/@fluffy')
    assert not handler.handles_url('https://not-mastodon.example/')
    assert not handler.handles_url('https://blah.example/')
    assert not handler.handles_url('https://also-not.example/')
Exemplo n.º 3
0
def test_success():
    store = {}

    def do_callback(message):
        assert message['To'] == '*****@*****.**'

        url = message.get_payload().strip()
        args = parse_args(url)

        assert url.startswith('http://example/cb/')

        result = handler.check_callback(url, parse_args(url), {})
        LOGGER.info('check_callback(%s,%s): %s', url, args, result)

        assert isinstance(result, disposition.Verified)
        store['result'] = result

        store['is_done'] = result.identity

    handler = email_addr.EmailAddress(do_callback,
                                      'some data',
                                      tokens.DictStore(store),
                                      email_template_text='{url}')

    result = handler.initiate_auth('mailto:[email protected]',
                                   'http://example/cb/', '/redir')
    LOGGER.info('initiate_auth: %s', result)
    assert isinstance(result, disposition.Notify)
    assert result.cdata == 'some data'

    assert store['result'].identity == 'mailto:[email protected]'
    assert store['result'].redir == '/redir'
Exemplo n.º 4
0
def test_from_config(mocker):
    store = {}
    mock_open = mocker.patch('builtins.open',
                             mocker.mock_open(read_data='template'))
    mock_smtp = mocker.patch('smtplib.SMTP')
    conn = mocker.MagicMock()
    mock_smtp.return_value = conn

    handler = email_addr.from_config(
        {
            'EMAIL_FROM': '*****@*****.**',
            'EMAIL_SUBJECT': 'test subject',
            'EMAIL_CHECK_MESSAGE': 'check yr email',
            'EMAIL_TEMPLATE_FILE': 'template.txt',
            'EMAIL_EXPIRE_TIME': 37,
            'SMTP_HOST': 'smtp.example.com',
            'SMTP_PORT': 587,
            'SMTP_USE_SSL': False,
        }, tokens.DictStore(store))

    mock_open.assert_called_with('template.txt', encoding='utf-8')
    res = handler.initiate_auth('mailto:[email protected]', 'http://cb/',
                                '/redir')
    assert res.cdata['message'] == 'check yr email'

    assert len(store) == 1
    mock_smtp.assert_called_with('smtp.example.com', 587)
Exemplo n.º 5
0
def test_basics():
    handler = fediverse.from_config(
        {
            'FEDIVERSE_NAME': 'test',
            'MASTODON_HOMEPAGE': 'http://foo.bar/',
        }, tokens.DictStore())
    assert handler.service_name
    assert handler.url_schemes
    assert handler.description
    assert handler.cb_id
    assert handler.logo_html
Exemplo n.º 6
0
def test_auth_success(mocker, requests_mock):
    store = tokens.DictStore()
    handler = fediverse.Fediverse('test',
                                  store,
                                  homepage='http://foo.example/')
    mock_mastodon = mocker.patch('mastodon.Mastodon')
    mock_mastodon.create_app.return_value = ('the id', 'the secret')
    mock_mastodon().auth_request_url.return_value = 'https://cb?code=12345'
    mock_mastodon().log_in.return_value = 'some_auth_token'
    mock_mastodon().me.return_value = {
        'url': 'https://mastodon.example/@moo',
        'display_name': 'moo friend',
        'avatar_static': 'https://placekitten.com/1280/1024',
        'source': {
            'note':
            'a cow',
            'fields': [{
                'name': 'homepage',
                'value': 'https://moo.example'
            }, {
                'name': 'my pronouns',
                'value': 'moo/moo'
            }]
        }
    }

    requests_mock.get('https://mastodon.example/api/v1/instance',
                      text=json.dumps({
                          'uri': 'foo',
                          'version': '2.5.1',
                          'urls': 'foo.bar'
                      }))

    requests_mock.post('https://mastodon.example/oauth/revoke', text='ok')

    result = handler.initiate_auth('mastodon.example', 'https://cb',
                                   'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    mock_mastodon().auth_request_url.assert_called_with(
        redirect_uris='https://cb', scopes=['read:accounts'])

    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Verified)
    assert result.identity == 'https://mastodon.example/@moo'
    assert result.redir == 'qwerpoiu'
    assert result.profile == {  # pylint:disable=no-member
        # https://github.com/PyCQA/pylint/issues/4693
        'name': 'moo friend',
        'bio': 'a cow',
        'avatar': 'https://placekitten.com/1280/1024',
        'homepage': 'https://moo.example',
        'pronouns': 'moo/moo'
    }
Exemplo n.º 7
0
def test_please_wait(mocker):
    token_store = tokens.DictStore()
    pending = {}
    mock_send = mocker.MagicMock()
    handler = email_addr.EmailAddress(mock_send,
                                      "this is data",
                                      token_store,
                                      expires_time=60,
                                      pending_storage=pending)

    mock_time = mocker.patch('time.time')
    assert mock_send.call_count == 0
    mock_time.return_value = 10

    # First auth should call mock_send
    handler.initiate_auth('mailto:[email protected]', 'http://example/', 'blop')
    assert mock_send.call_count == 1
    assert '*****@*****.**' in pending
    token_value = pending['*****@*****.**']

    # Second auth should not
    handler.initiate_auth('mailto:[email protected]', 'http://example/', 'blop')
    assert mock_send.call_count == 1
    assert '*****@*****.**' in pending
    assert token_value == pending['*****@*****.**']

    # Using the link should remove the pending item
    handler.check_callback('http://example/', {'t': pending['*****@*****.**']},
                           {})
    assert '*****@*****.**' not in pending

    # Next auth should call mock_send again
    handler.initiate_auth('mailto:[email protected]', 'http://example/', 'blop')
    assert mock_send.call_count == 2
    assert '*****@*****.**' in pending
    assert token_value != pending['*****@*****.**']
    token_value = pending['*****@*****.**']

    # Timing out the token should cause it to send again
    mock_time.return_value = 1000
    handler.initiate_auth('mailto:[email protected]', 'http://example/', 'blop')
    assert mock_send.call_count == 3
    assert '*****@*****.**' in pending
    assert token_value != pending['*****@*****.**']
    token_value = pending['*****@*****.**']

    # And anything else that removes the token from the token_store should as well
    token_store.remove(pending['*****@*****.**'])
    handler.initiate_auth('mailto:[email protected]', 'http://example/', 'blop')
    assert mock_send.call_count == 4
    assert token_value != pending['*****@*****.**']
    token_value = pending['*****@*****.**']
Exemplo n.º 8
0
def test_attack_mitigations(requests_mock, mocker):
    store = tokens.DictStore()
    handler = fediverse.Fediverse('test',
                                  store,
                                  homepage='http://foo.example/')
    mock_mastodon = mocker.patch('mastodon.Mastodon')

    mock_mastodon.create_app.return_value = ('the id', 'the secret')
    mock_mastodon().auth_request_url.return_value = 'https://cb?code=12345'
    mock_mastodon().log_in.return_value = 'some_auth_token'

    requests_mock.get('https://mastodon.example/api/v1/instance',
                      text=json.dumps({
                          'uri': 'foo',
                          'version': '2.5.1',
                          'urls': 'foo.bar'
                      }))

    # domain hijack
    mock_mastodon().me.return_value = {
        'url': 'https://hijack.example/@moo',
    }
    result = handler.initiate_auth('mastodon.example', 'https://cb',
                                   'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert 'Domains do not match' in result.message

    # attempted replay attack
    mock_mastodon().me.return_value = {
        'url': 'https://mastodon.example/@moo',
    }
    result = handler.initiate_auth('mastodon.example', 'https://cb',
                                   'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    args = parse_args(result.url)
    result = handler.check_callback(result.url, args, {})
    assert isinstance(result, disposition.Verified)
    assert result.identity == 'https://mastodon.example/@moo'
    result = handler.check_callback('https://cb', args, {})
    assert isinstance(result, disposition.Error)
    assert 'Invalid transaction' in result.message
Exemplo n.º 9
0
def test_basics():
    handler = email_addr.EmailAddress(None, None, tokens.DictStore())
    assert handler.service_name == 'Email'
    assert handler.url_schemes
    assert 'email' in handler.description
    assert handler.cb_id == 'e'
    assert handler.logo_html[0][1] == 'Email'

    assert handler.handles_url('*****@*****.**') == 'mailto:[email protected]'
    assert handler.handles_url('mailto:[email protected]') == 'mailto:[email protected]'

    # email addresses must be well-formed
    assert not handler.handles_url('mailto:foobar.baz')

    # don't support other schemas
    assert not handler.handles_url('email:[email protected]')
    assert not handler.handles_url('@[email protected]')
    assert not handler.handles_url('https://example.com/')

    # handle leading/trailing spaces correctly
    assert handler.handles_url('  [email protected]') == 'mailto:[email protected]'
    assert handler.handles_url('mailto:  [email protected]') == 'mailto:[email protected]'
    assert handler.handles_url('mailto:[email protected]  ') == 'mailto:[email protected]'

    # but don't allow embedded spaces
    assert not handler.handles_url('   foo @bar.baz')

    # email address must be valid
    assert not handler.handles_url(' asdf[]@poiu_foo.baz!')

    # don't allow bang-paths
    assert not handler.handles_url('bang!path!is!fun!bob')
    assert not handler.handles_url('bang.com!path!is!fun!bob')
    assert not handler.handles_url('[email protected]')

    # strip out non-email-address components
    assert handler.handles_url(
        'mailto:[email protected]?subject=pwned') == 'mailto:[email protected]'

    # handle case correctly
    assert handler.handles_url(
        'MailtO:[email protected]') == 'mailto:[email protected]'
Exemplo n.º 10
0
def test_dictstore():
    store = tokens.DictStore({})

    # different values should have different keys
    token = store.put((1, 2, 3))
    token2 = store.put((1, 2, 3))
    assert token != token2
    assert isinstance(token, str)
    assert isinstance(token2, str)

    # getting repeatedly should succeed
    assert store.get(token) == (1, 2, 3)
    assert store.get(token) == (1, 2, 3)
    assert store.get(token) == (1, 2, 3)

    # popping should remove it
    assert store.pop(token) == (1, 2, 3)
    with pytest.raises(KeyError):
        store.get(token)
    with pytest.raises(KeyError):
        store.pop(token)

    # removal should also remove it
    store.remove(token2)
    with pytest.raises(KeyError):
        store.get(token2)
    with pytest.raises(KeyError):
        store.pop(token2)

    # getting nonexistent should fail
    with pytest.raises(KeyError):
        store.get('bogus')

    # removal should always work even if the token doesn't exist
    store.remove(token)
    store.remove(token2)
    store.remove('bogus')
Exemplo n.º 11
0
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')
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
def test_auth_failures(requests_mock, mocker):
    # pylint:disable=too-many-statements
    store = tokens.DictStore({})
    handler = fediverse.Fediverse('test',
                                  store,
                                  homepage='http://foo.example/')
    mock_mastodon = mocker.patch('mastodon.Mastodon')

    # nonexistent instance
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Error)
    assert 'Failed to register client' in result.message

    # not a mastodon instance
    requests_mock.get('https://fail.example/api/v1/instance', text="'lolwut'")
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Error)
    assert 'Failed to register client' in result.message

    # okay now it's an instance
    requests_mock.get('https://fail.example/api/v1/instance',
                      text=json.dumps({
                          'uri': 'foo',
                          'version': '2.5.1',
                          'urls': 'foo.bar'
                      }))
    mock_mastodon.create_app.return_value = ('the id', 'the secret')

    # missing auth code
    mock_mastodon().auth_request_url.return_value = 'https://cb?foo=bar'
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert "Missing 'code'" in result.message

    # Login was aborted
    mock_mastodon(
    ).auth_request_url.return_value = 'https://cb?code=12345&error=nope'
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert "Error signing into instance" in result.message

    mock_mastodon().auth_request_url.return_value = 'https://cb?code=yep'

    # login failed for some other reason
    mock_mastodon().log_in.side_effect = mastodon.MastodonRatelimitError(
        "stop it")
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert "Error signing into instance" in result.message

    mock_mastodon().log_in.side_effect = None
    mock_mastodon().log_in.return_value = 'some auth code'

    # login expired
    mock_time = mocker.patch('time.time')
    mock_time.return_value = 100
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)

    mock_time.return_value = 86400
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert 'Login timed out' in result.message

    # broken profile
    mock_mastodon().me.return_value = {}
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert 'Missing user profile' in result.message

    mock_mastodon().me.return_value = {
        'url': 'https://fail.example/@larry',
        'source': ['ha ha ha', 'i break you']
    }
    result = handler.initiate_auth('fail.example', 'https://cb', 'qwerpoiu')
    assert isinstance(result, disposition.Redirect)
    result = handler.check_callback(result.url, parse_args(result.url), {})
    assert isinstance(result, disposition.Error)
    assert 'Malformed user profile' in result.message
Exemplo n.º 14
0
def test_failures(mocker):
    store = {}
    pending = {}

    def accept(message):
        url = message.get_payload().strip()
        pending[message['To']] = url

    handler = email_addr.EmailAddress(accept,
                                      'some data',
                                      tokens.DictStore(store),
                                      10,
                                      email_template_text='{url}')

    # must be well-formed mailto: URL
    for malformed in ('*****@*****.**', 'http://foo.bar/', 'mailto:blahblahblah'):
        assert 'Malformed' in str(
            handler.initiate_auth(malformed, 'http://example.cb/',
                                  '/malformed'))

    # check for missing or invalid tokens
    assert 'Missing token' in str(handler.check_callback('foo', {}, {}))
    assert 'Invalid token' in str(
        handler.check_callback('foo', {'t': 'bogus'}, {}))

    def initiate(addr, redir):
        result = handler.initiate_auth('mailto:' + addr, 'http://example/',
                                       redir)
        assert isinstance(result, disposition.Notify)
        assert result.cdata == 'some data'

    def check_pending(addr):
        url = pending[addr]
        return handler.check_callback(url, parse_args(url), {})

    # check for timeout failure
    mock_time = mocker.patch('time.time')
    mock_time.return_value = 30

    assert len(store) == 0
    initiate('*****@*****.**', '/timeout')
    assert len(store) == 1

    mock_time.return_value = 20000

    result = check_pending('*****@*****.**')
    assert isinstance(result, disposition.Error)
    assert 'timed out' in result.message
    assert result.redir == '/timeout'
    assert len(store) == 0

    # check for replay attacks
    assert len(store) == 0
    initiate('*****@*****.**', '/replay')
    assert len(store) == 1
    result1 = check_pending('*****@*****.**')
    result2 = check_pending('*****@*****.**')
    assert len(store) == 0

    assert isinstance(result1, disposition.Verified)
    assert result1.identity == 'mailto:[email protected]'
    assert result1.redir == '/replay'
    assert isinstance(result2, disposition.Error)
    assert 'Invalid token' in str(result2)