Skip to content

Commit 04147c0

Browse files
authored
[ BB2-1142 ] Hook Work Together (#5)
* Update package version/naming * Rename to cms_bluebutton * Move test files in to src/tests/ * Update for cms-bluebutton and move fixtures to test * Reorganize package directory structure * Add package __init__.py files * Update requests,add FHIR wrappers,add tests * Update refresh_access_token use data in post() * Refactor auth - Remove AuthRequest class and convert needed methods to functions in auth.py - Rename functions to better match the Node SDK in auth.py - Add methods to BlueButton class for refresh_auth_token(), generate_auth_data(), generate_authorize_url(), get_authorization_token() - Update tests in test_auth.py for changes. BB2-1142 * Refactor fhir_request - Update refresh_auth_token() in auth.py to use DATA instead of PARAMS for the post() call. This improves security. - Update fhir_request() in fhir_request.py to utilize AuthorizationToken class object. - Remove refresh_token() function fhir_request.py to utilize BlueButton refresh_auth_token() method instead. - Add AuthorizationToken to __init__.py for import from cms_bluebutton module. - Update MOCK_BB_CONFIG in tests to utilize AuthorizationToken class object in test_fhir_request.py BB2-1142 * Update README.md for package dir change * Add SDK_HEADER to auth calls
1 parent b6ae427 commit 04147c0

26 files changed

+551
-433
lines changed

README.md

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Introduction goes here!
2929

3030
## Build
3131

32-
To build the cms-bb2 package do the following:
32+
To build the cms_bluebutton package do the following:
3333

3434
- Build the package:
3535

@@ -49,42 +49,39 @@ $ pip install -e .
4949

5050
## Usage
5151

52-
To test it out with Python interactively:
52+
Usage goes here!
5353

54-
```
55-
$ python
56-
Python 3.10.1 ...
57-
Type "help", "copyright", "credits" or "license" for more information.
58-
>>> from bb2 import Bb2
59-
>>>
60-
>>> a = Bb2()
61-
>>>
62-
>>> a.hello()
63-
Hello from BB2 SDK Class method!!!
64-
>>>
65-
```
66-
67-
## Developing the Blue Button 2.0 SDK (for BB2 devs)
54+
## Developing the Blue Button 2.0 SDK (for BB2 team SDK developers)
6855

6956
### Install Development
7057

7158
To install with the tools you need to develop and run tests do the following:
7259

60+
From the repository base directory:
61+
7362
```
7463
$ pip install -e .[dev]
7564
```
7665

7766
To run the tests, use the following commands:
7867

68+
From the package base directory:
69+
7970
```
80-
# From the repo base directory
71+
$ cd src/cms_bluebutton
72+
73+
$ # To run all tests:
8174
$ pytest
75+
76+
$ # To run a specific test and show console debugging output:
77+
$ pytest tests/test_fhir_request.py -s
8278
```
8379

8480
To run the tests with coverage, use the following commands:
8581

82+
From the package base directory:
83+
8684
```
87-
# From the repo base directory
8885
$ coverage run -m pytest
8986
9087
# Check report
@@ -95,6 +92,8 @@ $ coverage report -m
9592

9693
To create a distribution run the following command:
9794

95+
From the repository base directory:
96+
9897
```
9998
$ python setup.py sdist
10099
```
@@ -111,5 +110,4 @@ To create a MANIFEST.in file run the following commands:
111110
$ pip install check-manifest # If not already installed.
112111
$ check-manifest --create
113112
$ python setup.py sdist
114-
```
115-
113+
```

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
long_description = fh.read()
66

77
setup(
8-
name="cms-bb2",
9-
version="0.0.1",
8+
name="cms_bluebutton",
9+
version="1.0.0",
1010
author="CMS Blue Button 2.0 Team",
1111
author_email="[email protected]", # TODO: Do we want to include?
1212
license="Apache Software License",
@@ -25,7 +25,7 @@
2525
"Topic :: Software Development",
2626
"Topic :: Software Development :: Libraries :: Python Modules",
2727
],
28-
py_modules=["blueButton"],
28+
py_modules=["cms_bluebutton"],
2929
package_dir={"": "src"},
3030
python_requires=">=3.6",
3131
install_requires=[

src/auth.py

Lines changed: 0 additions & 131 deletions
This file was deleted.

src/cms_bluebutton/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .cms_bluebutton import BlueButton # NOQA
2+
from .auth import AuthorizationToken # NOQA

src/cms_bluebutton/auth.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import base64
2+
import hashlib
3+
import requests
4+
import random
5+
import string
6+
import datetime
7+
import urllib
8+
from requests_toolbelt.multipart.encoder import MultipartEncoder
9+
10+
from .constants import SDK_HEADER, SDK_HEADER_KEY
11+
12+
13+
class AuthorizationToken:
14+
def __init__(self, auth_token):
15+
self.access_token = auth_token.get("access_token")
16+
self.expires_in = auth_token.get("expires_in")
17+
self.expires_at = (
18+
auth_token.get("expires_at")
19+
if auth_token.get("expires_at")
20+
else datetime.datetime.now(datetime.timezone.utc)
21+
+ datetime.timedelta(seconds=self.expires_in)
22+
)
23+
self.patient = auth_token.get("patient")
24+
self.refresh_token = auth_token.get("refresh_token")
25+
self.scope = auth_token.get("scope")
26+
self.token_type = auth_token.get("token_type")
27+
28+
def access_token_expired(self):
29+
return self.expires_at < datetime.datetime.now(datetime.timezone.utc)
30+
31+
32+
def refresh_auth_token(bb, auth_token):
33+
data = {
34+
"client_id": bb.client_id,
35+
"grant_type": "refresh_token",
36+
"refresh_token": auth_token.refresh_token,
37+
}
38+
39+
headers = {
40+
SDK_HEADER_KEY: SDK_HEADER,
41+
}
42+
43+
token_response = requests.post(
44+
url=bb.auth_token_url,
45+
data=data,
46+
headers=headers,
47+
auth=(bb.client_id, bb.client_secret),
48+
)
49+
50+
token_response.raise_for_status()
51+
return AuthorizationToken(token_response.json())
52+
53+
54+
def generate_authorize_url(bb, auth_data):
55+
params = {
56+
"client_id": bb.client_id,
57+
"redirect_uri": bb.client_secret,
58+
"state": auth_data["state"],
59+
"response_type": "code",
60+
"code_challenge_method": "S256",
61+
"code_challenge": auth_data["code_challenge"],
62+
}
63+
64+
return (
65+
bb.auth_base_url
66+
+ "?"
67+
+ urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
68+
)
69+
70+
71+
def base64_url_encode(buffer):
72+
buffer_bytes = base64.urlsafe_b64encode(buffer.encode("utf-8"))
73+
buffer_result = str(buffer_bytes, "utf-8")
74+
return buffer_result
75+
76+
77+
def get_random_string(length):
78+
letters = string.ascii_letters + string.digits + string.punctuation
79+
result = "".join(random.choice(letters) for i in range(length))
80+
return result
81+
82+
83+
def generate_pkce_data():
84+
verifier = generate_random_state(32)
85+
code_challenge = base64.urlsafe_b64encode(
86+
hashlib.sha256(verifier.encode("ASCII")).digest()
87+
)
88+
return {"code_challenge": code_challenge.decode("utf-8"), "verifier": verifier}
89+
90+
91+
def generate_random_state(num):
92+
return base64_url_encode(get_random_string(num))
93+
94+
95+
def generate_auth_data():
96+
auth_data = {"state": generate_random_state(32)}
97+
auth_data.update(generate_pkce_data())
98+
return auth_data
99+
100+
101+
def get_access_token_from_code(bb, auth_data, callback_code):
102+
data = {
103+
"client_id": bb.client_id,
104+
"client_secret": bb.client_secret,
105+
"code": callback_code,
106+
"grant_type": "authorization_code",
107+
"redirect_uri": bb.callback_url,
108+
"code_verifier": auth_data["verifier"],
109+
"code_challenge": auth_data["code_challenge"],
110+
}
111+
112+
mp_encoder = MultipartEncoder(data)
113+
token_response = requests.post(
114+
url=bb.auth_token_url,
115+
data=mp_encoder,
116+
headers={"content-type": mp_encoder.content_type, SDK_HEADER_KEY: SDK_HEADER},
117+
)
118+
token_response.raise_for_status()
119+
token_dict = token_response.json()
120+
token_dict["expires_at"] = datetime.datetime.now(
121+
datetime.timezone.utc
122+
) + datetime.timedelta(seconds=token_dict["expires_in"])
123+
124+
return token_dict
125+
126+
127+
def get_authorization_token(bb, auth_data, callback_code, callback_state):
128+
if callback_code is None:
129+
raise ValueError("Authorization code missing.")
130+
131+
if callback_state is None:
132+
raise ValueError("Callback parameter 'state' missing.")
133+
134+
if callback_state != auth_data["state"]:
135+
raise ValueError("Provided callback state does not match.")
136+
137+
return AuthorizationToken(get_access_token_from_code(bb, auth_data, callback_code))

0 commit comments

Comments
 (0)