-
Notifications
You must be signed in to change notification settings - Fork 192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement OAuth refresh token flow #520
Conversation
Codecov Report
@@ Coverage Diff @@
## master #520 +/- ##
=======================================
Coverage 98.03% 98.04%
=======================================
Files 71 71
Lines 7489 7507 +18
=======================================
+ Hits 7342 7360 +18
Misses 147 147
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great so far! 💯 Had a few suggestions/questions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @corvust
self._headers.pop("Authorization", None) | ||
# TODO: Make sure this is the right path | ||
path = "/auth/token" | ||
headers = {**self._headers} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps better to do the following instead of the pop above?
headers = {**self._headers} | |
headers = {**self._headers} | |
headers.pop("Authorization", None) |
It feels slightly 'safer' than mutating the instance attribute above, even if it has the same effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way, if the method fails midway, there are no side effects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thought here was that since we are in the refresh token method, we already know the existing Authorization header is invalid, so removing it from the instance headers is a housekeeping action before we replace it with a new one. Whether the method fails midway or not, any future requests are still going to fail regardless of if we've popped the header or not.
strawberryfields/api/connection.py
Outdated
if response.status_code == 200: | ||
self._headers["Authorization"] = "Bearer {}".format(response.cookies["access_token"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the only place where the token is stored?
Originally, I was wondering if it made sense to store it locally (temporarily), but I suppose it doesn't matter and is cleaner this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes the access token is only stored in the Authorization header. I decided against storing it independently since it isn't used anywhere else, is short lived, and a new one can be fetched if it is somehow lost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
strawberryfields/api/connection.py
Outdated
"Authorization failed for request" | ||
) | ||
|
||
def _request(self, method: str, path: str, headers: Dict = {}, **kwargs ): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to have headers=None
here, and then inside the method have
headers = headers or {}
Formatting changes Co-authored-by: antalszava <antalszava@gmail.com>
strawberryfields/api/connection.py
Outdated
self._headers = {"Authorization": self.token, "Accept-Version": self.api_version} | ||
|
||
self._headers = {"Accept-Version": self.api_version} | ||
self._refresh_access_token() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth noting, that due to this call from now on instantiating a Connection
object will already try to establish connection with the server.
As users are encouraged to use the higher level RemoteEngine
, this would mean that they could face the following:
In [12]: os.environ["SF_API_AUTHENTICATION_TOKEN"] = "WrongToken"
In [13]: sf.RemoteEngine("X8")
---------------------------------------------------------------------------
RequestFailedError Traceback (most recent call last)
<ipython-input-13-a1e528990489> in <module>
----> 1 sf.RemoteEngine("X8")
~//strawberryfields/strawberryfields/engine.py in __init__(self, target, connection, backend_options)
548 self._target = self.DEFAULT_TARGETS.get(target, target)
549 self._spec = None
--> 550 self._connection = connection or Connection()
551 self._backend_options = backend_options or {}
552 self.log = create_logger(__name__)
~//strawberryfields/strawberryfields/api/connection.py in __init__(self, token, host, port, use_ssl, verbose)
99
100 self._headers = {"Accept-Version": self.api_version}
--> 101 self._refresh_access_token()
102
103 self.log = create_logger(__name__)
~//strawberryfields/strawberryfields/api/connection.py in _refresh_access_token(self)
334 self._headers["Authorization"] = f"Bearer {access_token}"
335 else:
--> 336 raise RequestFailedError("Authorization failed for request, please check your token provided.")
337
338 def _request(self, method: str, path: str, headers: Dict = None, **kwargs):
RequestFailedError: Authorization failed for request, please check your token provided.
@josh146 would you think that this causes any complications?
Edit: Removed this line in the latest commit. With it being removed, the first request generates the access token. It also made testing easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Thanks for adding these @antalszava!
[ch3931] |
strawberryfields/api/connection.py
Outdated
self._headers["Authorization"] = f"Bearer {access_token}" | ||
else: | ||
raise RequestFailedError( | ||
"Authorization failed for request, please check your token provided." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Authorization failed for request, please check your token provided." | |
"Could not retrieve access token. Please check that your API key is correct." |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, you could do response.raise_for_status()
instead on line 330, which will raise a requests exception automatically if the status code isn't 200. That could be helpful because you could catch and re-raise the way you're doing here, but chain the exceptions together to get a more detailed traceback that includes the actual status code it failed with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh @jswinarton, it seems that it would allow other non-error response status codes though (like 201
), correct?
Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great @corvust and @antalszava! As far as I can tell, this seems to do what it's supposed to. 😄 💯
@@ -156,7 +157,7 @@ def get_device_spec(self, target: str) -> DeviceSpec: | |||
def _get_device_dict(self, target: str) -> dict: | |||
"""Returns the device specifications as a dictionary""" | |||
path = f"/devices/{target}/specifications" | |||
response = requests.get(self._url(path), headers=self._headers) | |||
response = self._request("GET", self._url(path), headers=self._headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for my own understanding; what does this change do? Figured it out!
return response.status_code == 200 | ||
|
||
def _url(self, path: str) -> str: | ||
return self._base_url + path | ||
|
||
def _refresh_access_token(self): | ||
"""Use the offline token to request a new access token.""" | ||
self._headers.pop("Authorization", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(minor / thinking aloud) I'm always partial to using del
instead of pop
(since the latter unnecessarily returns the item as well), but I see that pop
might be a cleaner solution, since it works even if the item doesn't exist. 😆
def _request(self, method: str, path: str, headers: Dict = None, **kwargs): | ||
"""Wrap all API requests with an authentication token refresh if a 401 status | ||
is received from the initial request. | ||
|
||
Args: | ||
method (str): the HTTP request method to use | ||
path (str): path of the endpoint to use | ||
headers (dict): dictionary containing the headers of the request | ||
|
||
Returns: | ||
requests.Response: the response received for the sent request | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, this explains my question above! 💯
* Fix issue with single parameter list (#503) * Fix issue with single parameter list * Fix pyliny import complaint * Update changelog * removes unused variables in test_tdmprogram * Fix run_options not being used (#500) * Fix run_options not being used * Update changelog * Add tests * removes unused variables in test_tdmprogram (#504) Co-authored-by: Josh Izaac <josh146@gmail.com> * Add program code generator function (#496) * Add serialize function * Run black * Remove f * Updates from code review * Improve operations handling * Remove duplicate line * Fix generate_code + add tests * Tidy things up a bit * Add forgotten factor * Run black * Update changelog * Fixes from code review * Update strawberryfields/io.py Co-authored-by: Josh Izaac <josh146@gmail.com> * change argument name Co-authored-by: Josh Izaac <josh146@gmail.com> * Add wigner plotting (#495) * Add wigner plotting to program * Move plotting to plot module * Remove numpy import * Fixes after code-review * Tidy up wigner plotting * Run black * Add test * Update colours * Update changelog * rename tests * Update strawberryfields/plot.py Co-authored-by: antalszava <antalszava@gmail.com> * add contours arg * Update tests * Update changelog * Apply suggestions from code review Co-authored-by: antalszava <antalszava@gmail.com> * Remove url * fix string * fix arg name Co-authored-by: antalszava <antalszava@gmail.com> * Adds api_version property, adds 'Accept-Version' to headers (#512) * Fock state and quadrature plotting (#510) * Fock state plotting draft * Updates, error, test * test & imports * Test chart generation * Formatting * Remove unnecessary comment * Two-mode test * cutoff in test adjust * quad and adjust fock prev chart * Formatting * Adjust * quad test * generate_quad_chart test * Update tests/frontend/test_sf_plot.py * update * data always has two elements * reset Makefile * changelog, adjust chart title * ket latex render * latex in fock plot title * adjust tests for latex * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * adjust * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * marginal * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * docstring * adjust tests for Fock * quad tests * docstring * Move obtaining the Wigner function of the state to generate_wigner_chart * Docstring * add plot page * Update strawberryfields/plot.py Co-authored-by: Theodor <theodor@xanadu.ai> * base state render * docstring basestate render change Co-authored-by: Josh Izaac <josh146@gmail.com> Co-authored-by: Theodor <theodor@xanadu.ai> * Fix bug in Dgate, Coherent, and DisplacedSqueezed (#507) * Support TF tensors in batch form Co-authored-by: Josh Izaac <josh146@gmail.com> * Add test * Add to changelog * Update PR in changelog * New line * Extend to other gates * Update changelog * Update * Update test * Update .github/CHANGELOG.md Co-authored-by: Josh Izaac <josh146@gmail.com> * Fix typo * Fix Co-authored-by: Josh Izaac <josh146@gmail.com> * Update tdm docstring (#519) * removes unused variables in test_tdmprogram * updates docstring * Implement OAuth refresh token flow (#520) * Implement OAuth refresh token flow * Apply suggestions from code review Formatting changes Co-authored-by: antalszava <antalszava@gmail.com> * Add correct token refresh path * update path, update getting the access token, remove url wrapping in _request * Formatting with black * move dict init into func * Updates * refresh access token unit tests * no print * User request.post directly, update tests * Wrapped request test, updates * Updates * Formatting * Remove access token refreshing from init * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update strawberryfields/api/connection.py Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com> * update msg in test * changelog Co-authored-by: antalszava <antalszava@gmail.com> Co-authored-by: Antal Szava <antalszava@example.com> Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com> * Increment version number to v0.17.0 (#523) * bump version and update changelog * update about * remove old current release * minor tweaks * dev bump (#524) * Update photonic_hardware.rst (#526) Co-authored-by: Theodor <theodor@xanadu.ai> Co-authored-by: Josh Izaac <josh146@gmail.com> Co-authored-by: antalszava <antalszava@gmail.com> Co-authored-by: Jack Brown <jack@xanadu.ai> Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com> Co-authored-by: Tim Leisti <corvust@users.noreply.github.com> Co-authored-by: Antal Szava <antalszava@example.com> Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com>
* Fix issue with single parameter list (#503) * Fix issue with single parameter list * Fix pyliny import complaint * Update changelog * Fix run_options not being used (#500) * Fix run_options not being used * Update changelog * Add tests * removes unused variables in test_tdmprogram (#504) Co-authored-by: Josh Izaac <josh146@gmail.com> * Add program code generator function (#496) * Add serialize function * Run black * Remove f * Updates from code review * Improve operations handling * Remove duplicate line * Fix generate_code + add tests * Tidy things up a bit * Add forgotten factor * Run black * Update changelog * Fixes from code review * Update strawberryfields/io.py Co-authored-by: Josh Izaac <josh146@gmail.com> * change argument name Co-authored-by: Josh Izaac <josh146@gmail.com> * Add wigner plotting (#495) * Add wigner plotting to program * Move plotting to plot module * Remove numpy import * Fixes after code-review * Tidy up wigner plotting * Run black * Add test * Update colours * Update changelog * rename tests * Update strawberryfields/plot.py Co-authored-by: antalszava <antalszava@gmail.com> * add contours arg * Update tests * Update changelog * Apply suggestions from code review Co-authored-by: antalszava <antalszava@gmail.com> * Remove url * fix string * fix arg name Co-authored-by: antalszava <antalszava@gmail.com> * Adds api_version property, adds 'Accept-Version' to headers (#512) * Fock state and quadrature plotting (#510) * Fock state plotting draft * Updates, error, test * test & imports * Test chart generation * Formatting * Remove unnecessary comment * Two-mode test * cutoff in test adjust * quad and adjust fock prev chart * Formatting * Adjust * quad test * generate_quad_chart test * Update tests/frontend/test_sf_plot.py * update * data always has two elements * reset Makefile * changelog, adjust chart title * ket latex render * latex in fock plot title * adjust tests for latex * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * adjust * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * marginal * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * Update strawberryfields/plot.py Co-authored-by: Josh Izaac <josh146@gmail.com> * docstring * adjust tests for Fock * quad tests * docstring * Move obtaining the Wigner function of the state to generate_wigner_chart * Docstring * add plot page * Update strawberryfields/plot.py Co-authored-by: Theodor <theodor@xanadu.ai> * base state render * docstring basestate render change Co-authored-by: Josh Izaac <josh146@gmail.com> Co-authored-by: Theodor <theodor@xanadu.ai> * Fix bug in Dgate, Coherent, and DisplacedSqueezed (#507) * Support TF tensors in batch form Co-authored-by: Josh Izaac <josh146@gmail.com> * Add test * Add to changelog * Update PR in changelog * New line * Extend to other gates * Update changelog * Update * Update test * Update .github/CHANGELOG.md Co-authored-by: Josh Izaac <josh146@gmail.com> * Fix typo * Fix Co-authored-by: Josh Izaac <josh146@gmail.com> * Update tdm docstring (#519) * removes unused variables in test_tdmprogram * updates docstring * Implement OAuth refresh token flow (#520) * Implement OAuth refresh token flow * Apply suggestions from code review Formatting changes Co-authored-by: antalszava <antalszava@gmail.com> * Add correct token refresh path * update path, update getting the access token, remove url wrapping in _request * Formatting with black * move dict init into func * Updates * refresh access token unit tests * no print * User request.post directly, update tests * Wrapped request test, updates * Updates * Formatting * Remove access token refreshing from init * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update tests/api/test_connection.py * Update strawberryfields/api/connection.py Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com> * update msg in test * changelog Co-authored-by: antalszava <antalszava@gmail.com> Co-authored-by: Antal Szava <antalszava@example.com> Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com> * Increment version number to v0.17.0 (#523) * bump version and update changelog * update about * remove old current release * minor tweaks * dev bump (#524) * Update photonic_hardware.rst (#526) * dummy change * Update strawberryfields/backends/bosonicbackend/backend.py Co-authored-by: Theodor <theodor@xanadu.ai> Co-authored-by: Josh Izaac <josh146@gmail.com> Co-authored-by: antalszava <antalszava@gmail.com> Co-authored-by: Jack Brown <jack@xanadu.ai> Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com> Co-authored-by: Tim Leisti <corvust@users.noreply.github.com> Co-authored-by: Antal Szava <antalszava@example.com> Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com>
Context:
The platform backend is moving to a new OAuth based authentication flow using offline refresh tokens and access tokens.
Description of the Change:
Benefits:
More secure and easier user credential management.
Possible Drawbacks:
Not backwards compatible, all users will be required to generate new tokens and update strawberry fields when the new authentication mechanism is activated.
Related GitHub Issues:
N/A