Skip to content

Commit e3d9726

Browse files
committed
implemented publisher client and example
1 parent f0d3e3a commit e3d9726

File tree

10 files changed

+533
-30
lines changed

10 files changed

+533
-30
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import sys
2+
3+
from uid2_client import Uid2PublisherClient
4+
from uid2_client import TokenGenerateResponse
5+
from uid2_client import TokenGenerateInput
6+
7+
8+
def _usage():
9+
print('Usage: python3 sample_token_generate_refresh.py <base_url> <auth_key> <secret_key>', file=sys.stderr)
10+
sys.exit(1)
11+
12+
13+
if len(sys.argv) <= 3:
14+
_usage()
15+
16+
base_url = sys.argv[1]
17+
auth_key = sys.argv[2]
18+
secret_key = sys.argv[3]
19+
20+
21+
22+
publisher_client = Uid2PublisherClient(base_url, auth_key, secret_key)
23+
print("Generating Token")
24+
token_generate_response = publisher_client.generate_token(TokenGenerateInput.from_email("[email protected]"))
25+
26+
status = token_generate_response.status
27+
tokens = token_generate_response.get_identity()
28+
advertising_token = tokens.get_advertising_token()
29+
refresh_token = tokens.get_refresh_token()
30+
refresh_response_key = tokens.get_refresh_response_key()
31+
refresh_from = tokens.get_refresh_from()
32+
refresh_expires = tokens.get_refresh_expires()
33+
identity_expires = tokens.get_identity_expires()
34+
json_string = tokens.get_json_string()
35+
36+
print('Status =', status)
37+
print('Advertising Token =', advertising_token)
38+
print('Refresh Token =', refresh_token)
39+
print('Refresh Response Key =', refresh_response_key)
40+
print('Refresh From =', refresh_from)
41+
print('Refresh Expires =', refresh_expires)
42+
print('Identity Expires =', identity_expires)
43+
print('As Json String =', json_string, "\n")
44+
45+
print("Refreshing Token")
46+
token_refresh_response = publisher_client.refresh_token(tokens)
47+
status = token_refresh_response.status
48+
tokens = token_refresh_response.get_identity()
49+
advertising_token = tokens.get_advertising_token()
50+
refresh_token = tokens.get_refresh_token()
51+
refresh_response_key = tokens.get_refresh_response_key()
52+
refresh_from = tokens.get_refresh_from()
53+
refresh_expires = tokens.get_refresh_expires()
54+
identity_expires = tokens.get_identity_expires()
55+
json_string = tokens.get_json_string()
56+
57+
print('Status =', status)
58+
print('Advertising Token =', advertising_token)
59+
print('Refresh Token =', refresh_token)
60+
print('Refresh Response Key =', refresh_response_key)
61+
print('Refresh From =', refresh_from)
62+
print('Refresh Expires =', refresh_expires)
63+
print('Identity Expires =', identity_expires)
64+
print('As Json String =', json_string, "\n")

uid2_client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
from .client import *
1313
from .encryption import *
1414
from .keys import *
15+
from .publisher_client import *
1516

1617

uid2_client/client.py

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .encryption import _decrypt_gcm, _encrypt_gcm
1818
from .keys import EncryptionKey, EncryptionKeysCollection
1919
from .identity_scope import IdentityScope
20+
from .request_response_util import *
2021

2122

2223
def _make_dt(timestamp):
@@ -31,6 +32,9 @@ class Uid2Client:
3132
3233
Methods:
3334
refresh_keys: get the latest encryption keys for decrypting advertising tokens
35+
refresh_json: parse json to get encryption keys
36+
encrypt: encrypt a uid to a token
37+
decrypt: decrypt a token to a uid
3438
3539
Examples:
3640
Connect to the UID2 service and obtain the latest encryption keys:
@@ -47,6 +51,7 @@ def __init__(self, base_url, auth_key, secret_key, identity_scope):
4751
base_url (str): base URL for all requests to UID2 services (e.g. 'https://prod.uidapi.com')
4852
auth_key (str): authorization key for consuming the UID2 services
4953
secret_key (str): secret key for consuming the UID2 services
54+
identity_scope (IdentityScope): UID2 or EUID
5055
5156
Note:
5257
Your authorization key will determine which UID2 services you are allowed to use.
@@ -66,9 +71,9 @@ def refresh_keys(self):
6671
Returns:
6772
EncryptionKeysCollection containing the keys
6873
"""
69-
req, nonce = self._make_v2_request(dt.datetime.now(tz=timezone.utc))
70-
resp = self._post('/v2/key/sharing', headers=self._auth_headers(), data=req)
71-
resp_body = json.loads(self._parse_v2_response(resp.read(), nonce)).get('body')
74+
req, nonce = make_v2_request(self._secret_key, dt.datetime.now(tz=timezone.utc))
75+
resp = post(self._base_url, '/v2/key/sharing', headers=auth_headers(self._auth_key), data=req)
76+
resp_body = json.loads(parse_v2_response(self._secret_key, resp.read(), nonce)).get('body')
7277
return self._parse_keys_json(resp_body)
7378

7479
def refresh_json(self, json_str):
@@ -124,33 +129,6 @@ def _parse_keys_json(self, resp_body):
124129
return EncryptionKeysCollection(keys, resp_body["caller_site_id"], resp_body["master_keyset_id"],
125130
resp_body.get("default_keyset_id", None), resp_body["token_expiry_seconds"])
126131

127-
def _make_url(self, path):
128-
return self._base_url + path
129-
130-
def _auth_headers(self):
131-
return {'Authorization': 'Bearer ' + self._auth_key,
132-
"X-UID2-Client-Version": "uid2-client-python-" + pkg_resources.get_distribution("uid2_client").version}
133-
134-
def _make_v2_request(self, now):
135-
payload = int.to_bytes(int(now.timestamp() * 1000), 8, 'big')
136-
nonce = os.urandom(8)
137-
payload += nonce
138-
139-
envelope = int.to_bytes(1, 1, 'big')
140-
envelope += _encrypt_gcm(payload, None, self._secret_key)
141-
142-
return base64.b64encode(envelope), nonce
143-
144-
def _parse_v2_response(self, encrypted, nonce):
145-
payload = _decrypt_gcm(base64.b64decode(encrypted), self._secret_key)
146-
if nonce != payload[8:16]:
147-
raise ValueError("nonce mismatch")
148-
return payload[16:]
149-
150-
def _post(self, path, headers, data):
151-
req = request.Request(self._make_url(path), headers=headers, method='POST', data=data)
152-
return request.urlopen(req)
153-
154132

155133
class Uid2ClientError(Exception):
156134
"""Raised for problems encountered while interacting with UID2 services."""

uid2_client/identity_tokens.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import datetime
2+
import json
3+
4+
5+
class IdentityTokens:
6+
def __init__(self, advertising_token, refresh_token, refresh_response_key, identity_expires,
7+
refresh_expires, refresh_from, json_string):
8+
self.advertising_token = advertising_token
9+
self.refresh_token = refresh_token
10+
self.refresh_response_key = refresh_response_key
11+
self.identity_expires = identity_expires
12+
self.refresh_expires = refresh_expires
13+
self.refresh_from = refresh_from
14+
self.json_string = json_string
15+
16+
@staticmethod
17+
def from_json_string(json_string):
18+
assert json_string is not None, "jsonString must not be null"
19+
20+
try:
21+
return IdentityTokens.from_json(json.loads(json_string))
22+
except json.JSONDecodeError:
23+
print("Invalid json string")
24+
return None
25+
except KeyError:
26+
print("Missing field in json string")
27+
return None
28+
29+
@staticmethod
30+
def from_json(json_obj):
31+
return IdentityTokens(
32+
json_obj.get("advertising_token"),
33+
json_obj.get("refresh_token"),
34+
json_obj.get("refresh_response_key"),
35+
json_obj.get("identity_expires"),
36+
json_obj.get("refresh_expires"),
37+
json_obj.get("refresh_from"),
38+
json.dumps(json_obj)
39+
)
40+
41+
def is_due_for_refresh(self):
42+
return self.is_due_for_refresh_impl(datetime.datetime.now())
43+
44+
def get_advertising_token(self):
45+
return self.advertising_token
46+
47+
def get_refresh_token(self):
48+
return self.refresh_token
49+
50+
def get_json_string(self):
51+
return self.json_string
52+
53+
def is_refreshable(self):
54+
return self.is_refreshable_impl(datetime.datetime.now())
55+
56+
def is_refreshable_impl(self, timestamp):
57+
refresh_expires = self.refresh_expires
58+
if refresh_expires is None or timestamp.timestamp() > refresh_expires:
59+
return False
60+
return self.refresh_token is not None
61+
62+
def is_due_for_refresh_impl(self, timestamp):
63+
return timestamp.timestamp() > self.refresh_from or self.has_identity_expired(timestamp)
64+
65+
def has_identity_expired(self, timestamp):
66+
return timestamp.timestamp() > self.identity_expires
67+
68+
def get_refresh_response_key(self):
69+
return self.refresh_response_key
70+
71+
def get_identity_expires(self):
72+
return self.identity_expires
73+
74+
def get_refresh_expires(self):
75+
return self.refresh_expires
76+
77+
def get_refresh_from(self):
78+
return self.refresh_from

uid2_client/input_util.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import hashlib
2+
import base64
3+
from enum import Enum
4+
5+
6+
def is_phone_number_normalized(phone_number):
7+
MIN_PHONENUMBER_DIGITS = 10
8+
MAX_PHONENUMBER_DIGITS = 15
9+
10+
if phone_number is None or len(phone_number) < MIN_PHONENUMBER_DIGITS:
11+
return False
12+
13+
if phone_number[0] != '+':
14+
return False
15+
16+
total_digits = sum(char.isdigit() for char in phone_number[1:])
17+
18+
return MIN_PHONENUMBER_DIGITS <= total_digits <= MAX_PHONENUMBER_DIGITS
19+
20+
21+
def normalize_email_string(email):
22+
pre_sb = []
23+
pre_sb_specialized = []
24+
sb = []
25+
ws_buffer = []
26+
27+
class EmailParsingState:
28+
Starting = 1
29+
Pre = 2
30+
SubDomain = 3
31+
32+
parsing_state = EmailParsingState.Starting
33+
34+
in_extension = False
35+
36+
for i in range(len(email)):
37+
c_given = email[i]
38+
if 'A' <= c_given <= 'Z':
39+
c = chr(ord(c_given) + 32)
40+
else:
41+
c = c_given
42+
43+
if parsing_state == EmailParsingState.Starting:
44+
if c == ' ':
45+
continue
46+
else:
47+
parsing_state = EmailParsingState.Pre
48+
49+
if parsing_state == EmailParsingState.Pre:
50+
if c == '@':
51+
parsing_state = EmailParsingState.SubDomain
52+
elif c == '.':
53+
pre_sb.append(c)
54+
elif c == '+':
55+
pre_sb.append(c)
56+
in_extension = True
57+
else:
58+
pre_sb.append(c)
59+
if not in_extension:
60+
pre_sb_specialized.append(c)
61+
elif parsing_state == EmailParsingState.SubDomain:
62+
if c == '@':
63+
return None
64+
elif c == ' ':
65+
ws_buffer.append(c)
66+
continue
67+
if len(ws_buffer) > 0:
68+
sb.extend(ws_buffer)
69+
ws_buffer = []
70+
sb.append(c)
71+
72+
if len(sb) == 0:
73+
return None
74+
domain_part = ''.join(sb)
75+
76+
GMAIL_DOMAIN = "gmail.com"
77+
if GMAIL_DOMAIN == domain_part:
78+
address_part_to_use = pre_sb_specialized
79+
else:
80+
address_part_to_use = pre_sb
81+
82+
if len(address_part_to_use) == 0:
83+
return None
84+
85+
return ''.join(address_part_to_use + ['@'] + [domain_part])
86+
87+
88+
def is_ascii_digit(d):
89+
return '0' <= d <= '9'
90+
91+
92+
def base64_to_byte_array(str):
93+
return base64.b64decode(str)
94+
95+
96+
def byte_array_to_base64(b):
97+
return base64.b64encode(b).decode()
98+
99+
100+
def get_base64_encoded_hash(input):
101+
return byte_array_to_base64(get_sha256_bytes(input))
102+
103+
104+
def get_sha256_bytes(input):
105+
return hashlib.sha256(input.encode()).digest()
106+

0 commit comments

Comments
 (0)