Skip to content

Commit

Permalink
Merge pull request #91 from PlaidWeb/feature/update-indieauth
Browse files Browse the repository at this point in the history
Update to latest IndieAuth spec
  • Loading branch information
fluffy-critter committed Dec 4, 2020
2 parents d2dc509 + f624ae9 commit 6cd8242
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 45 deletions.
30 changes: 6 additions & 24 deletions authl/handlers/indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
def find_endpoint(id_url: str,
links: typing.Dict = None,
content: BeautifulSoup = None) -> typing.Tuple[typing.Optional[str],
typing.Optional[str]]:
str]:
""" Given an identity URL, discover its IndieAuth endpoint
:param str id_url: an identity URL to check
Expand Down Expand Up @@ -153,49 +153,31 @@ def verify_id(request_id: str, response_id: str) -> str:
Given an ID from an identity request and its verification response, ensure
that the verification response is a valid URL for the request. A response is
considered valid if it is on the same domain and declares the same
authorization_endpoint.
considered valid if it declares the same authorization_endpoint.
:param str request_id: The original requested identity
:param str response_id: The authorized response identity
:returns: the verified response ID
:raises: :py:class:`ValueError` if verification failed
This is a provisional extension to IndieAuth; see
`IndieAuth issue 35 <https://github.com/indieweb/indieauth/issues/35>`_ for
more information.
"""

# exact match is always okay
if request_id == response_id:
return response_id

orig = urllib.parse.urlparse(request_id)
resp = urllib.parse.urlparse(response_id)
LOGGER.debug('orig=%s resp=%s', orig, resp)

# The host must match
if orig.netloc != resp.netloc:
LOGGER.debug("netloc mismatch %s %s", orig.netloc, resp.netloc)
raise ValueError("Domain mismatch")

req_endpoint, _ = find_endpoint(request_id)
resp_endpoint, resp_profile = find_endpoint(response_id)

# Need to make sure the domains match with the final profile URLs too
resp = urllib.parse.urlparse(resp_profile) # type:ignore
if orig.netloc != resp.netloc:
LOGGER.debug("redirected netloc mismatch %s %s", orig.netloc, resp.netloc)
raise ValueError("Domain mismatch (profile redirection)")
if resp_endpoint is None:
raise ValueError(f'Profile {resp_profile} missing IndieAuth endpoint')

# Both the original and final profile must have the same endpoint
LOGGER.debug('request endpoint=%s response endpoint=%s', req_endpoint, resp_endpoint)
if req_endpoint != resp_endpoint:
raise ValueError("Authorization endpoint mismatch")
raise ValueError(f'Authorization endpoint mismatch for {request_id} and {response_id}')

return response_id
return resp_profile


class IndieAuth(Handler):
Expand Down
54 changes: 33 additions & 21 deletions tests/handlers/test_indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,42 +120,45 @@ def test_verify_id(requests_mock):
assert indieauth.verify_id('https://matching.example',
'https://matching.example') == 'https://matching.example'

# Different URL is allowed as long as the domain and endpoint match
# Different URL is allowed as long as the endpoints match
requests_mock.get('https://different.example/1', headers=endpoint_1)
requests_mock.get('https://different.example/2', headers=endpoint_1)
assert indieauth.verify_id('https://different.example/1',
'https://different.example/2') == 'https://different.example/2'

# Don't allow if the domain doesn't match, even if the endpoint does
requests_mock.get('https://one.example', headers=endpoint_1)
requests_mock.get('https://two.example', headers=endpoint_1)
with pytest.raises(ValueError):
indieauth.verify_id('https://one.example', 'https://two.example')
# Different domain is allowed as long as the endpoints match
requests_mock.get('https://different.domain/1', headers=endpoint_1)
assert indieauth.verify_id('https://different.example/1',
'https://different.domain/1') == 'https://different.domain/1'

# Don't allow if the endpoints mismatch, even if the domain matches
requests_mock.get('https://same.example/alice', headers=endpoint_1)
requests_mock.get('https://same.example/bob', headers=endpoint_2)
with pytest.raises(ValueError):
indieauth.verify_id('https://same.example/alice', 'https://same.example/bob')

# scheme upgrade is allowed as long as the endpoint stays the same
# scheme change is allowed as long as the endpoint stays the same
requests_mock.get('http://upgrade.example', headers=endpoint_2)
requests_mock.get('https://upgrade.example', headers=endpoint_2)
assert indieauth.verify_id('http://upgrade.example', 'https://upgrade.example')

# redirect is fine as long as the domain matches
# redirect is fine as long as the final endpoint matches
requests_mock.get('https://redir.example/user', headers=endpoint_1)
requests_mock.get('https://redir.example/me', status_code=301,
requests_mock.get('https://redir.example/perm', status_code=301,
headers={'Location': 'https://redir.example/target'})
requests_mock.get('https://redir.example/temp', status_code=302,
headers={'Location': 'https://redir.example/target'})
requests_mock.get('https://redir.example/target', headers=endpoint_1)
assert indieauth.verify_id('https://redir.example/user', 'https://redir.example/me')

# redirect is NOT fine if the domain changes
requests_mock.get('https://redir.example/offsite', status_code=301,
headers={'Location': 'https://redir.target/'})
requests_mock.get('https://redir.target/', headers=endpoint_1)
assert indieauth.verify_id('https://redir.example/user',
'https://redir.example/perm') == 'https://redir.example/target'
assert indieauth.verify_id('https://redir.example/user',
'https://redir.example/temp') == 'https://redir.example/temp'

# Target page must have an endpoint
requests_mock.get('https://missing.example/src', headers=endpoint_1)
requests_mock.get('https://missing.example/dest', text='foo')
with pytest.raises(ValueError):
indieauth.verify_id('https://redir.example/user', 'https://redir.example/offsite')
indieauth.verify_id('https://matching.example/src', 'https://missing.example/dest')


def test_handler_success(requests_mock):
Expand Down Expand Up @@ -275,7 +278,10 @@ def check_failure(message):
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'}
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
Expand All @@ -289,10 +295,16 @@ def check_failure(message):
requests_mock.post('http://endpoint/', text='invalid json')
check_failure('invalid response JSON')

# callback returns invalid identity URL
requests_mock.post('http://endpoint/', text=json.dumps({'me': 'http://whitehouse.gov'}))
requests_mock.get('http://whitehouse.gov', text='hello there')
check_failure('Domain mismatch')
# 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_login_timeout(mocker, requests_mock):
Expand Down

0 comments on commit 6cd8242

Please sign in to comment.