Skip to content
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

Merged
merged 23 commits into from
Jan 15, 2021
Merged

Conversation

corvust
Copy link
Contributor

@corvust corvust commented Jan 11, 2021

Context:

The platform backend is moving to a new OAuth based authentication flow using offline refresh tokens and access tokens.

Description of the Change:

  • Adds an authentication call to retrieve a short-lived access token using the long lived refresh token stored in the configuration (offline token)
  • Wraps all API calls to retrieve a new access token if the current one has expired (resulting in a 401 response)

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

@codecov
Copy link

codecov bot commented Jan 11, 2021

Codecov Report

Merging #520 (1693576) into master (fd1b1aa) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           Coverage Diff           @@
##           master     #520   +/-   ##
=======================================
  Coverage   98.03%   98.04%           
=======================================
  Files          71       71           
  Lines        7489     7507   +18     
=======================================
+ Hits         7342     7360   +18     
  Misses        147      147           
Impacted Files Coverage Δ
strawberryfields/api/connection.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fd1b1aa...1693576. Read the comment docs.

Copy link
Contributor

@antalszava antalszava left a 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

strawberryfields/api/connection.py Show resolved Hide resolved
strawberryfields/api/connection.py Outdated Show resolved Hide resolved
strawberryfields/api/connection.py Outdated Show resolved Hide resolved
Copy link
Member

@josh146 josh146 left a 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}
Copy link
Member

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?

Suggested change
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

Copy link
Member

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

Copy link
Contributor Author

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.

Comment on lines 325 to 326
if response.status_code == 200:
self._headers["Authorization"] = "Bearer {}".format(response.cookies["access_token"])
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"Authorization failed for request"
)

def _request(self, method: str, path: str, headers: Dict = {}, **kwargs ):
Copy link
Member

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 {}

self._headers = {"Authorization": self.token, "Accept-Version": self.api_version}

self._headers = {"Accept-Version": self.api_version}
self._refresh_access_token()
Copy link
Contributor

@antalszava antalszava Jan 12, 2021

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.

Copy link
Member

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!

@antalszava antalszava self-assigned this Jan 13, 2021
@antalszava antalszava changed the title [WIP] Implement OAuth refresh token flow Implement OAuth refresh token flow Jan 13, 2021
@antalszava antalszava marked this pull request as ready for review January 13, 2021 19:10
@antalszava
Copy link
Contributor

[ch3931]

self._headers["Authorization"] = f"Bearer {access_token}"
else:
raise RequestFailedError(
"Authorization failed for request, please check your token provided."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Authorization failed for request, please check your token provided."
"Could not retrieve access token. Please check that your API key is correct."

?

Copy link
Contributor

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.

Copy link
Contributor

@antalszava antalszava Jan 14, 2021

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?

antalszava and others added 2 commits January 14, 2021 11:31
Co-authored-by: Jeremy Swinarton <jeremy@swinarton.com>
Copy link
Contributor

@thisac thisac left a 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)
Copy link
Contributor

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)
Copy link
Contributor

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. 😆

Comment on lines +338 to +349
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
"""
Copy link
Contributor

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! 💯

@antalszava antalszava merged commit 7b0a45d into XanaduAI:master Jan 15, 2021
nquesada added a commit that referenced this pull request Jan 19, 2021
* 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>
nquesada added a commit that referenced this pull request Jan 20, 2021
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants