Skip to content

Commit

Permalink
Merge pull request #97 from PlaidWeb/feature/indieauth-profile
Browse files Browse the repository at this point in the history
Let the IndieAuth server response profile override the h-card
  • Loading branch information
fluffy-critter committed Aug 29, 2021
2 parents a0a506e + f8fdd03 commit 36c5fcf
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 15 deletions.
55 changes: 41 additions & 14 deletions authl/handlers/indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,33 +161,57 @@ def get_url(prop, scheme=None) -> typing.Tuple[typing.Optional[str],
}


def get_profile(id_url: str, links=None, content: BeautifulSoup = None, endpoints=None) -> dict:
""" Given an identity URL, try to parse out an Authl profile """
if not content:
if id_url in _PROFILE_CACHE:
LOGGER.debug("Reusing %s profile from cache", id_url)
return _PROFILE_CACHE[id_url]
def get_profile(id_url: str,
server_profile: dict = None,
links=None,
content: BeautifulSoup = None,
endpoints=None) -> dict:
""" Given an identity URL, try to parse out an Authl profile
:param str id_url: The profile page to parse
:param dict server_profile: An IndieAuth response profile
:param dict links: Profile response's links dictionary
:param content: Pre-parsed page content
:param dict endpoints: Pre-parsed page endpoints
"""

if id_url in _PROFILE_CACHE:
LOGGER.debug("Reusing %s profile from cache", id_url)
profile = _PROFILE_CACHE[id_url]
else:
profile = {}

if not content and id_url not in _PROFILE_CACHE:
LOGGER.debug("get_profile: Retrieving %s", id_url)
request = utils.request_url(id_url)
if request is not None:
links = request.links
content = BeautifulSoup(request.text, 'html.parser')

h_cards = mf2py.Parser(doc=content).to_dict(filter_by_type="h-card")
LOGGER.debug("get_profile(%s): found %d h-cards", id_url, len(h_cards))
if content:
h_cards = mf2py.Parser(doc=content).to_dict(filter_by_type="h-card")
LOGGER.debug("get_profile(%s): found %d h-cards", id_url, len(h_cards))

for card in h_cards:
items = _parse_hcard(id_url, card)

profile = {}
for card in h_cards:
items = _parse_hcard(id_url, card)
profile.update({k: v for k, v in items.items() if v and k not in profile})

profile.update({k: v for k, v in items.items() if v and k not in profile})
if server_profile:
# The IndieAuth server also provided a profile, which should supercede the h-card
for in_key, out_key in (('name', 'name'),
('photo', 'avatar'),
('url', 'homepage'),
('email', 'email')):
if in_key in server_profile:
profile[out_key] = server_profile[in_key]

if not endpoints:
endpoints, _ = find_endpoints(id_url, links=links, content=content)
if endpoints:
profile['endpoints'] = endpoints

LOGGER.debug("Stashing %s profile", id_url)
_PROFILE_CACHE[id_url] = profile
return profile

Expand Down Expand Up @@ -304,7 +328,7 @@ def initiate_auth(self, id_url, callback_uri, redir):
'client_id': client_id,
'state': state,
'response_type': 'code',
'scope': 'profile',
'scope': 'profile email',
'me': id_url})
return disposition.Redirect(url)

Expand Down Expand Up @@ -346,7 +370,10 @@ def check_callback(self, url, get, data):
return disposition.Error("Got invalid response JSON", redir)

response_id = verify_id(id_url, response['me'])
return disposition.Verified(response_id, redir, get_profile(response_id))
return disposition.Verified(
response_id, redir,
get_profile(response_id,
server_profile=response.get('profile')))
except KeyError as key:
return disposition.Error("Missing " + str(key), redir)
except ValueError as err:
Expand Down
59 changes: 58 additions & 1 deletion tests/handlers/test_indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,11 @@ def verify_callback(request, _):
assert args['client_id'] == ['http://client/']
assert 'redirect_uri' in args
return json.dumps({
'me': 'https://example.user/bob'
'me': 'https://example.user/bob',
'profile': {
'email': 'foo@bar.baz',
'url': 'https://bob.example.user/'
}
})
requests_mock.post('https://auth.example/endpoint', text=verify_callback)

Expand All @@ -250,6 +254,13 @@ def verify_callback(request, _):
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': 'foo@bar.baz',
}.items():
assert response.profile[key] == val

# trying to replay the same transaction should fail
response = handler.check_callback(
user_get['redirect_uri'],
Expand Down Expand Up @@ -436,3 +447,49 @@ def test_profile_partial(requests_mock):
requests_mock.get('https://partial.example', text=profile_html)
profile = indieauth.get_profile('https://partial.example')
assert profile == profile_blob


def test_server_profile(requests_mock):
profile_html = r"""
<link rel="authorization_endpoint" href="https://endpoint.example/">
<div class="h-card">
<a class="u-url p-name" href="https://example.foo/~user/">larry</a>
<p class="e-note">I'm <em>Larry</em>. And you're not. <span class="p-pronouns">he/him</span> or
<span class="p-pronoun">whatever</span></p>
<a class="u-email" href="mailto:larry%40example.foo">larry at example dot foo</a>
<img class="u-photo" src="plop.jpg">
</div>"""

identity_profile = {
'email': 'larry-forreals@example.foo',
'url': 'https://meow.meow/',
'name': 'Larry Fairchild',
'photo': 'https://placekitten.com/1280/1024'
}

profile_blob = {
'avatar': "https://placekitten.com/1280/1024",
'bio': "I'm Larry. And you're not. he/him or whatever",
'email': "larry-forreals@example.foo",
'name': "Larry Fairchild",
'pronouns': "he/him",
'homepage': "https://meow.meow/",
'endpoints': {
'authorization_endpoint': 'https://endpoint.example/',
},
}

profile_mock = requests_mock.get('http://server.example', text=profile_html)

# prefill the cache without the server response
indieauth.get_profile('http://server.example')

# actually set the response profile, make sure it updates
profile = indieauth.get_profile('http://server.example', server_profile=identity_profile)
assert profile == profile_blob

# check to make sure it's still in the cache
profile = indieauth.get_profile('http://server.example')
assert profile == profile_blob

assert profile_mock.call_count == 1

0 comments on commit 36c5fcf

Please sign in to comment.