Skip to content

Commit

Permalink
feat(socialaccount): Facebook Limited Login
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit b70b8ab8bc10be4341af3d3479f26a3324da6914
Author: Raymond Penners <[email protected]>
Date:   Tue Oct 22 20:22:21 2024 +0200

    tests(socialaccount): Facebook limited login

commit e165bb1ca3ba20005cdead1092b8245cfaa9b885
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 19:46:37 2024 +0200

    chore(facebook): remove more type hints.

commit 271eb4fd1a854a519128a60a8e25760e7be663a2
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 19:37:08 2024 +0200

    lint: fix isort.

commit 8f7c4942982c5e2e1e9e7436ebc7cc6488085198
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 19:36:19 2024 +0200

    chore(facebook): remove faulty type hint.

commit 265853c7bded414b984a5baee085c6105534488d
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 19:34:49 2024 +0200

    lint: fix linting

commit ba663d3c7f3e94afd6dc3ace1c669e7f89152211
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 19:09:44 2024 +0200

    chore(facebook): PR feedback, skip (redundant) JWT replay attack protection, fix super() call with proper verify_token call.

commit df30314747564715b57f09960b89b884fa2efc48
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 18:12:54 2024 +0200

    docs(facebook): update docs.

commit 0ace116ffeeb2ae0d473449c34e2652a7c1cbb3e
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 18:05:25 2024 +0200

    docs(changelog): update changelog.

commit 43de00922f5479f4d52574b01962ccce463817bc
Author: Andre Borie <[email protected]>
Date:   Sun Oct 20 18:01:22 2024 +0200

    feat(facebook): implement support for Facebook Limited Login.
  • Loading branch information
pennersr committed Oct 22, 2024
1 parent b45cf3b commit b3574f7
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 6 deletions.
6 changes: 6 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Note worthy changes
configure a different scope per app by including ``"scope": [...]`` in the app
settings.

- Facebook login: Facebook `Limited Login
<https://developers.facebook.com/docs/facebook-login/limited-login>`_ is now
supported via the Headless API. When you have a Limited Login JWT obtained
from the iOS SDK, you can use the Headless "provider token" flow to login with
it.


Fixes
-----
Expand Down
31 changes: 31 additions & 0 deletions allauth/socialaccount/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from contextlib import contextmanager
from unittest.mock import patch

import pytest

from allauth.account.models import EmailAddress
Expand Down Expand Up @@ -33,3 +36,31 @@ def factory(
return sociallogin

return factory


@pytest.fixture
def jwt_decode_bypass():
@contextmanager
def f(jwt_data):
with patch("allauth.socialaccount.internal.jwtkit.verify_and_decode") as m:
data = {
"iss": "https://accounts.google.com",
"aud": "client_id",
"sub": "123sub",
"hd": "example.com",
"email": "[email protected]",
"email_verified": True,
"at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
"name": "Raymond Penners",
"picture": "https://lh5.googleusercontent.com/photo.jpg",
"given_name": "Raymond",
"family_name": "Penners",
"locale": "en",
"iat": 123,
"exp": 456,
}
data.update(jwt_data)
m.return_value = data
yield

return f
43 changes: 43 additions & 0 deletions allauth/socialaccount/providers/facebook/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
from datetime import timedelta

from django.core.cache import cache
from django.http import HttpRequest
from django.utils import timezone

from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.internal import jwtkit
from allauth.socialaccount.models import SocialLogin, SocialToken
from allauth.socialaccount.providers.base import Provider
from allauth.socialaccount.providers.facebook.constants import GRAPH_API_URL


# maps fields from the Limited Login JWT to Graph API response fields
JWT_FIELD_TO_GRAPH_API_FIELD_MAP = {
"sub": "id",
"email": "email",
"given_name": "first_name",
"family_name": "last_name",
"name": "name",
"user_link": "link",
}


def compute_appsecret_proof(app, token):
# Generate an appsecret_proof parameter to secure the Graph API call
# see https://developers.facebook.com/docs/graph-api/securing-requests
Expand Down Expand Up @@ -135,3 +148,33 @@ def verify_token(
login = complete_login(request, provider, token)
login.token = token
return login


def verify_limited_login_token(
request: HttpRequest, provider, id_token: str
) -> SocialLogin:
"""
Verifies a Facebook Limited Login token.
See https://developers.facebook.com/docs/facebook-login/limited-login/token/validating.
We validate the JWT, then convert its data/claims into
a fake Facebook Graph API response, which is then passed to
`provider.sociallogin_from_response` to be handled as normal.
"""

# note: this already does replay protection internally
jwt_data = jwtkit.verify_and_decode(
credential=id_token,
keys_url=provider.limited_login_jwks_url,
issuer=provider.limited_login_expected_jwt_issuer,
audience=provider.app.client_id,
lookup_kid=jwtkit.lookup_kid_jwk,
)

fake_response = {
graph_field: jwt_data[jwt_field]
for jwt_field, graph_field in JWT_FIELD_TO_GRAPH_API_FIELD_MAP.items()
if jwt_field in jwt_data
}

return provider.sociallogin_from_response(request, fake_response)
28 changes: 24 additions & 4 deletions allauth/socialaccount/providers/facebook/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class FacebookProvider(OAuth2Provider):
oauth2_adapter_class = FacebookOAuth2Adapter
supports_token_authentication = True

# TODO: populate these from https://www.facebook.com/.well-known/openid-configuration/
# just like in a normal OIDC provider (as that's what "Limited Login" really is)
limited_login_expected_jwt_issuer = "https://www.facebook.com"
limited_login_jwks_url = (
"https://limited.facebook.com/.well-known/oauth/openid/jwks/"
)

def __init__(self, *args, **kwargs):
self._locale_callable_cache = None
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -204,17 +211,30 @@ def extract_email_addresses(self, data):
ret.append(EmailAddress(email=email, verified=False, primary=True))
return ret

def verify_token(self, request, token):
def verify_token(self, request, token: dict):
"""
Verifies both normal oAuth2-style "access_token"s as well
as OIDC-style "Limited Login" JWTs.
Limited Login is an OIDC-based form of Facebook Login
that their iOS SDK uses when App Tracking Transparency consent is denied.
"""
from allauth.socialaccount.providers.facebook import flows

access_token = token.get("access_token")
if not access_token:
id_token = token.get("id_token")

if not any([access_token, id_token]):
raise get_adapter().validation_error("invalid_token")

try:
login = flows.verify_token(request, self, access_token)
if access_token:
return flows.verify_token(request, self, access_token)
else:
assert id_token
return flows.verify_limited_login_token(request, self, id_token)
except (OAuth2Error, requests.RequestException) as e:
raise get_adapter().validation_error("invalid_token") from e
return login


provider_classes = [FacebookProvider]
26 changes: 26 additions & 0 deletions allauth/socialaccount/providers/facebook/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from allauth.account import app_settings as account_settings
from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase, mocked_response
Expand Down Expand Up @@ -163,3 +164,28 @@ def test_login_unverified(self):
def _login_verified(self):
self.login(self.get_mocked_response())
return EmailAddress.objects.get(email="[email protected]")


def test_limited_token(rf, db, settings, jwt_decode_bypass):
settings.SOCIALACCOUNT_PROVIDERS = {
"facebook": {
"AUTH_PARAMS": {},
"VERIFIED_EMAIL": False,
"APPS": [{"client_id": "123"}],
}
}
request = rf.get("/")
adapter = get_adapter(request)
provider = adapter.get_provider(request, FacebookProvider.id)
token = {"id_token": "X"}
with jwt_decode_bypass(
{
"sub": "f123",
"email": "[email protected]",
"given_name": "John",
"family_name": "Doe",
}
):
login = provider.verify_token(request, token)
assert login.account.uid == "f123"
assert login.email_addresses[0].email == "[email protected]"
14 changes: 12 additions & 2 deletions docs/socialaccount/providers/facebook.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
Facebook
--------

For Facebook both OAuth2 and the Facebook Connect Javascript SDK are
supported. You can even mix the two.
For Facebook both OAuth2, Facebook Connect Javascript SDK and even
`Limited Login <https://developers.facebook.com/docs/facebook-login/limited-login>`_
are supported. You can even mix and match.

An advantage of the Javascript SDK may be a more streamlined user
experience as you do not leave your site. Furthermore, you do not need
Expand Down Expand Up @@ -124,3 +125,12 @@ Development callback URL
Leave your App Domains empty and put ``http://localhost:8000`` in the
section labeled ``Website with Facebook Login``. Note that you'll need to
add your site's actual domain to this section once it goes live.

For Limited Login, it is exclusively supported via the Headless API's "provider
token" flow.

Pass your Limited Login JWT (obtained from the Facebook iOS SDK) to that
endpoint as an ``id_token``.

Note that Limited Login is purely used for login and does not allow access to
the user's Facebook account - no ``SocialToken`` is created.

0 comments on commit b3574f7

Please sign in to comment.