Skip to content

Commit cfd0830

Browse files
committed
Accept both base64 and base64url tokens in the bidstream
This behaviour matches that of the .NET SDK.
1 parent a2455fb commit cfd0830

File tree

3 files changed

+38
-13
lines changed

3 files changed

+38
-13
lines changed

tests/test_bidstream_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,19 @@ def test_refresh_keys(self, mock_refresh_bidstream_keys):
280280
client_secret_bytes)
281281

282282

283+
def test_decrypt_v4_token_encoded_as_base64(self):
284+
for scope in IdentityScope:
285+
with self.subTest(scope=scope):
286+
self.refresh(key_bidstream_response_json_default_keys(identity_scope=scope))
287+
288+
while True:
289+
token = generate_uid_token(scope, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4)
290+
token = base64.b64encode(Uid2Base64UrlCoder.decode(token)).decode('utf-8')
291+
if ("=" in token) and ("/" in token) and ("+" in token):
292+
break
293+
294+
self._decrypt_and_assert_success(token, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4, scope)
295+
296+
283297
if __name__ == '__main__':
284298
unittest.main()

tests/test_utils.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,18 @@
3939
phone_uid = "BEOGxroPLdcY7LrSiwjY52+X05V0ryELpJmoWAyXiwbZ"
4040

4141
test_cases_all_scopes_all_versions = [
42-
[IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V2],
43-
[IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V3],
44-
[IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4],
45-
[IdentityScope.EUID, AdvertisingTokenVersion.ADVERTISING_TOKEN_V2],
46-
[IdentityScope.EUID, AdvertisingTokenVersion.ADVERTISING_TOKEN_V3],
47-
[IdentityScope.EUID, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4]
42+
[scope, version]
43+
for scope in IdentityScope
44+
for version in AdvertisingTokenVersion
4845
]
4946

5047
test_cases_all_scopes_v3_v4_versions = [
51-
[IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V3],
52-
[IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4],
53-
[IdentityScope.EUID, AdvertisingTokenVersion.ADVERTISING_TOKEN_V3],
54-
[IdentityScope.EUID, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4]
48+
[scope, version]
49+
for scope in IdentityScope
50+
for version in [
51+
AdvertisingTokenVersion.ADVERTISING_TOKEN_V3,
52+
AdvertisingTokenVersion.ADVERTISING_TOKEN_V4,
53+
]
5554
]
5655

5756
YESTERDAY = now + dt.timedelta(days=-1)

uid2_client/encryption.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,25 @@ def _decrypt_token(token, keys, domain_name, client_type, now):
105105
elif token_bytes[1] == AdvertisingTokenVersion.ADVERTISING_TOKEN_V3.value:
106106
return _decrypt_token_v3(base64.b64decode(token), keys, domain_name, client_type, now, AdvertisingTokenVersion.ADVERTISING_TOKEN_V3)
107107
elif token_bytes[1] == AdvertisingTokenVersion.ADVERTISING_TOKEN_V4.value:
108-
# same as V3 but use Base64URL encoding
109-
return _decrypt_token_v3(Uid2Base64UrlCoder.decode(token), keys, domain_name, client_type, now, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4)
108+
# Accept either base64 or base64url encoding.
109+
return _decrypt_token_v3(base64.b64decode(_base64url_to_base64(token)), keys, domain_name, client_type, now, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4)
110110
else:
111111
return DecryptedToken.make_error(DecryptionStatus.VERSION_NOT_SUPPORTED)
112112

113113

114+
def _base64url_to_base64(value):
115+
value = value.replace('-', '+').replace('_', '/')
116+
input_size_mod4 = len(value) % 4
117+
if input_size_mod4 == 0:
118+
return value
119+
elif input_size_mod4 == 2:
120+
return value + '=='
121+
elif input_size_mod4 == 3:
122+
return value + '='
123+
else:
124+
raise EncryptionError('invalid payload')
125+
126+
114127
def _token_has_valid_lifetime(keys, client_type, generated_or_now, expires, now):
115128
# generated_or_now allows "now" for token v2, since v2 does not contain a "token generated" field.
116129
# v2 therefore checks against remaining lifetime rather than total lifetime
@@ -294,7 +307,6 @@ def encrypt(uid2, identity_scope, keys, keyset_id=None, **kwargs):
294307
return EncryptionDataResponse.make_error(EncryptionStatus.ENCRYPTION_FAILURE)
295308

296309

297-
298310
# DEPRECATED, DO NOT CALL
299311
def encrypt_data(data, identity_scope, **kwargs):
300312
"""Encrypt arbitrary binary data.

0 commit comments

Comments
 (0)