-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support Apple team Migration (#31861)
* feat: LEARNER-8790 Support Apple team migration
- Loading branch information
Showing
13 changed files
with
675 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
common/djangoapps/third_party_auth/docs/how_tos/migrating_apple_users_in_teams.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
Migrating Apple users while switching teams on Apple | ||
----------------------------------------------- | ||
|
||
This document explains how to migrate apple signed-in users in the event of | ||
switching teams on the Apple Developer console. When a user uses Apple to sign in, | ||
LMS receives an `id_token from apple containing user information`_, including | ||
user's unique identifier with key `sub`. This unique identifier is unique to | ||
Apple team this user belongs to. Upon switching teams on Apple, developers need | ||
to migrate users from one team to another i.e. migrate users' unique | ||
identifiers. In the LMS, users' unique apple identifiers are stored in | ||
social_django.models.UserSocialAuth.uid. Following is an outline specifying the | ||
migration process. | ||
|
||
1. `Create transfer_identifiers for all apple users`_ using the current respective apple unique id. | ||
|
||
i. Run management command generate_and_store_apple_transfer_ids to generate and store apple transfer ids. | ||
|
||
ii. Transfer ids are stored in third_party_auth.models.AppleMigrationUserIdInfo to be used later on. | ||
|
||
2. Transfer/Migrate teams on Apple account. | ||
|
||
i. After the migration, `Apple continues to send the transfer identifier`_ with key `transfer_sub` in information sent after login. | ||
|
||
ii. These transfer identifiers are available in the login information for 60 days after team transfer. | ||
|
||
ii. The method get_user_id() in third_party_auth.appleid.AppleIdAuth enables existing users to sign in by matching the transfer_sub sent in the login information with stored records of old Apple unique identifiers in third_party_auth.models.AppleMigrationUserIdInfo. | ||
|
||
3. Update Apple Backend credentials in third_party_auth.models.OAuth2ProviderConfig for the Apple backend. | ||
|
||
4. Create new team-scoped apple unique ids' for users after the migration using transfer ids created in Step 1. | ||
|
||
i. Run management command generate_and_store_new_apple_ids to generate and store new team-scoped apple ids. | ||
|
||
5. Update apple unique identifiers in the Database with new team-scoped apple ids retrieved in step 3. | ||
|
||
i. Run management command update_new_apple_ids_in_social_auth. | ||
|
||
6. Apple user migration is complete! | ||
|
||
|
||
.. _id_token from apple containing user information: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple | ||
.. _Create transfer_identifiers for all apple users: https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team | ||
.. _Apple continues to send the transfer identifier: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple |
118 changes: 118 additions & 0 deletions
118
.../djangoapps/third_party_auth/management/commands/generate_and_store_apple_transfer_ids.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
""" | ||
Management command to generate Transfer Identifiers for users who signed in with Apple. | ||
These transfer identifiers are used in the event of migrating an app from one team to another. | ||
""" | ||
|
||
|
||
import logging | ||
import requests | ||
import time | ||
|
||
from django.core.management.base import BaseCommand, CommandError | ||
from django.db import transaction | ||
import jwt | ||
from social_django.models import UserSocialAuth | ||
from social_django.utils import load_strategy | ||
|
||
|
||
from common.djangoapps.third_party_auth.models import AppleMigrationUserIdInfo | ||
from common.djangoapps.third_party_auth.appleid import AppleIdAuth | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command to generate transfer identifiers for apple users using their apple_id | ||
stored in social_django.models.UserSocialAuth.uid. | ||
Usage: | ||
manage.py generate_and_store_apple_transfer_ids <target_team_id> | ||
""" | ||
|
||
def _generate_client_secret(self): | ||
""" | ||
Generate client secret for use in Apple API's | ||
""" | ||
now = int(time.time()) | ||
expiry = 60 * 60 * 3 # 3 hours | ||
|
||
backend = load_strategy().get_backend(AppleIdAuth.name) | ||
team_id = backend.setting('TEAM') | ||
key_id = backend.setting('KEY') | ||
private_key = backend.get_private_key() | ||
audience = backend.TOKEN_AUDIENCE | ||
|
||
headers = { | ||
"alg": "ES256", | ||
'kid': key_id | ||
} | ||
payload = { | ||
'iss': team_id, | ||
'iat': now, | ||
'exp': now + expiry, | ||
'aud': audience, | ||
'sub': "org.edx.mobile", | ||
} | ||
|
||
return jwt.encode(payload, key=private_key, algorithm='ES256', | ||
headers=headers) | ||
|
||
def _generate_access_token(self, client_secret): | ||
""" | ||
Generate access token for use in Apple API's | ||
""" | ||
access_token_url = 'https://appleid.apple.com/auth/token' | ||
app_id = "org.edx.mobile" | ||
payload = { | ||
"grant_type": "client_credentials", | ||
"scope": "user.migration", | ||
"client_id": app_id, | ||
"client_secret": client_secret | ||
} | ||
headers = { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
"Host": "appleid.apple.com" | ||
} | ||
response = requests.post(access_token_url, data=payload, headers=headers) | ||
access_token = response.json().get('access_token') | ||
return access_token | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('target_team_id', help='Team ID to which the app is to be migrated to.') | ||
|
||
@transaction.atomic | ||
def handle(self, *args, **options): | ||
target_team_id = options['target_team_id'] | ||
|
||
migration_url = "https://appleid.apple.com/auth/usermigrationinfo" | ||
app_id = "org.edx.mobile" | ||
|
||
client_secret = self._generate_client_secret() | ||
access_token = self._generate_access_token(client_secret) | ||
if not access_token: | ||
raise CommandError('Failed to create access token.') | ||
|
||
headers = { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
"Host": "appleid.apple.com", | ||
"Authorization": "Bearer " + access_token | ||
} | ||
payload = { | ||
"target": target_team_id, | ||
"client_id": app_id, | ||
"client_secret": client_secret | ||
} | ||
|
||
apple_ids = UserSocialAuth.objects.filter(provider=AppleIdAuth.name).values_list('uid', flat=True) | ||
for apple_id in apple_ids: | ||
payload['sub'] = apple_id | ||
response = requests.post(migration_url, data=payload, headers=headers) | ||
transfer_id = response.json().get('transfer_sub') | ||
if transfer_id: | ||
apple_user_id_info, _ = AppleMigrationUserIdInfo.objects.get_or_create(old_apple_id=apple_id) | ||
apple_user_id_info.transfer_id = transfer_id | ||
apple_user_id_info.save() | ||
log.info('Updated transfer_id for uid %s', apple_id) | ||
else: | ||
log.info('Unable to fetch transfer_id for uid %s', apple_id) |
111 changes: 111 additions & 0 deletions
111
common/djangoapps/third_party_auth/management/commands/generate_and_store_new_apple_ids.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
""" | ||
Management command to exchange apple transfer identifiers with Apple ID of the | ||
user for new migrated team. | ||
""" | ||
|
||
|
||
import logging | ||
import requests | ||
import time | ||
|
||
from django.core.management.base import BaseCommand, CommandError | ||
from django.db import transaction | ||
import jwt | ||
from social_django.utils import load_strategy | ||
|
||
from common.djangoapps.third_party_auth.models import AppleMigrationUserIdInfo | ||
from common.djangoapps.third_party_auth.appleid import AppleIdAuth | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command to exchange transfer identifiers for new team-scoped identifier for | ||
the user in new migrated team. | ||
Usage: | ||
manage.py generate_and_store_apple_transfer_ids | ||
""" | ||
|
||
def _generate_client_secret(self): | ||
""" | ||
Generate client secret for use in Apple API's | ||
""" | ||
now = int(time.time()) | ||
expiry = 60 * 60 * 3 # 3 hours | ||
|
||
backend = load_strategy().get_backend(AppleIdAuth.name) | ||
team_id = backend.setting('TEAM') | ||
key_id = backend.setting('KEY') | ||
private_key = backend.get_private_key() | ||
audience = backend.TOKEN_AUDIENCE | ||
|
||
headers = { | ||
"alg": "ES256", | ||
'kid': key_id | ||
} | ||
payload = { | ||
'iss': team_id, | ||
'iat': now, | ||
'exp': now + expiry, | ||
'aud': audience, | ||
'sub': "org.edx.mobile", | ||
} | ||
|
||
return jwt.encode(payload, key=private_key, algorithm='ES256', | ||
headers=headers) | ||
|
||
def _generate_access_token(self, client_secret): | ||
""" | ||
Generate access token for use in Apple API's | ||
""" | ||
access_token_url = 'https://appleid.apple.com/auth/token' | ||
app_id = "org.edx.mobile" | ||
payload = { | ||
"grant_type": "client_credentials", | ||
"scope": "user.migration", | ||
"client_id": app_id, | ||
"client_secret": client_secret | ||
} | ||
headers = { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
"Host": "appleid.apple.com" | ||
} | ||
response = requests.post(access_token_url, data=payload, headers=headers) | ||
access_token = response.json().get('access_token') | ||
return access_token | ||
|
||
@transaction.atomic | ||
def handle(self, *args, **options): | ||
migration_url = "https://appleid.apple.com/auth/usermigrationinfo" | ||
app_id = "org.edx.mobile" | ||
|
||
client_secret = self._generate_client_secret() | ||
access_token = self._generate_access_token(client_secret) | ||
if not access_token: | ||
raise CommandError('Failed to create access token.') | ||
|
||
headers = { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
"Host": "appleid.apple.com", | ||
"Authorization": "Bearer " + access_token | ||
} | ||
payload = { | ||
"client_id": app_id, | ||
"client_secret": client_secret | ||
} | ||
|
||
apple_user_ids_info = AppleMigrationUserIdInfo.objects.all() | ||
for apple_user_id_info in apple_user_ids_info: | ||
payload['transfer_sub'] = apple_user_id_info.transfer_id | ||
response = requests.post(migration_url, data=payload, headers=headers) | ||
new_apple_id = response.json().get('sub') | ||
if new_apple_id: | ||
apple_user_id_info.new_apple_id = new_apple_id | ||
apple_user_id_info.save() | ||
log.info('Updated new Apple ID for uid %s', | ||
apple_user_id_info.old_apple_id) | ||
else: | ||
log.info('Unable to fetch new Apple ID for uid %s', | ||
apple_user_id_info.old_apple_id) |
Oops, something went wrong.