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

Implements limited-input device auth flow, to replace deprecated OOB auth flow. #6107

Merged
merged 16 commits into from Jan 5, 2023

Conversation

arcra
Copy link
Member

@arcra arcra commented Dec 15, 2022

  • Motivation for features / changes
    The OOB auth flow has been deprecated. We concluded that the limited-input device flow is appropriate for our use case where the uploader runs in an environment where a browser is not available.

  • Technical description of changes
    Implements a new auth flow which calls an RPC to fetch a device_code, verification_url and user_code, and asks user to visit the verification_url in another device and enter the user_code; then starts polling for the access token after the user authorizes the access from another device.

  • Screenshots of UI changes
    N/A

  • Detailed steps to verify changes work correctly (as executed by you)

  • Wrote test script that uses this class, and tested the auth flow and was able to print the credentials.
  • Wrote tests.
  • Alternate designs / implementations considered
    Basically, implementing something similar to this flow or the OOB flow ourselves.

@arcra arcra requested a review from bmd3k December 15, 2022 02:53
Copy link
Contributor

@bmd3k bmd3k left a comment

Choose a reason for hiding this comment

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

Sorry, I only had limited time today so I haven't done a complete review nor even tried it out to make sure it works :). Most of the comments are relatively minor issues with documentation, style, and code structure.

tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
tensorboard/uploader/auth.py Show resolved Hide resolved
tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
return CustomInstalledAppFlow.from_client_config(
client_config, scopes=OPENID_CONNECT_SCOPES
)
class _TbUploaderAuthFlow(auth_flows.Flow):
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense for this to extend auth_flows.Flow? It seems that this class is now a higher level non-Flow object that is composed of two different Flows?

It it doesn't need to extend auth_flows.Flow, can we eliminate the layer entirely and move its init() and run() logic into authenticate_user() and make the other functions to be file-level? I feel like the class creates a confusing layer of indirection.

Alternatively, if you don't want to remove the layer of indirection, can we rename it to reflect that it is not actxually an auth_flows.Flow? Maybe _TbUploaderAuthenticator?

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to be a "clear" update in the sense that it maintains the same general interface with the rest of the code. The class also helps encapsulate several methods that are relevant only for the limited-input device flow, but I agree that it seemed like a weird abstraction to some extent (Ideally it would have been two separate classes, one for the limited-input device flow, and one custom flow that used one or the other in the underlying implementation).

I decided to make this class just about the LimitedInputDeviceFlow, and move the high-level logic of which one to use, to the authenticate_user method.

I don't have a strong opinion on whether we should actually extend that class or not (we don't really need it), but it made sense to me in the sense of exposing the same interface as the other flow. Let me know if you have any preferences.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ended up removing the base class. I think it was misleading and erroneous, because the base methods don't really make sense (or work, probably) for this flow.

tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
tensorboard/uploader/auth.py Outdated Show resolved Hide resolved
@bmd3k bmd3k self-requested a review December 16, 2022 18:01
Copy link
Contributor

@bmd3k bmd3k left a comment

Choose a reason for hiding this comment

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

I've gotten though everything except for the tests. Thanks for your patience!

to `localhost` with an authorization code that would then be received by the
local web server started here.

Notably, when the uploaoder is run from a colab notebook, this flow cannot
Copy link
Contributor

Choose a reason for hiding this comment

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

uploaoder => uploader

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

local web server started here.

Notably, when the uploaoder is run from a colab notebook, this flow cannot
be used, as colab notebooks are run in an environment where a browser is not
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this true? Colab is run in a browser, so the environment has access to a browser. I've never really thought about it before but I suspect the real problem is you can't start a local web server in colab.

On the other hand, I think the statement "a browser is not available" is true for the remote terminal environments.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure, but I rephrased in a less explicit way to avoid making false statements.

to `localhost` with an authorization code that would then be received by the
local web server started here.

Notably, when the uploaoder is run from a colab notebook, this flow cannot
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for calling out colab specifically. I think you should also call out remote terminals since that is a roughly equal important use case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


If any of the following is true, a different auth flow will be used:
- the flag `--auth_force_console` is set to true, or
- a browser is not available (e.g. when running in a colab notebook), or
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to other comment: I don't think "browser is not available" is actually the problem in the colab scenario.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed the example.

http://developers.google.com/identity/protocols/oauth2/limited-input-device)
will be used, in which the user is presented with a URL and a short code
that they'd need to use to authenticate and authorize access in a separate
browser or device, after which the uploader will poll for access until the
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the "after which" in relation to "user is presented with a URL" or the user's attempt to "authenticate and authorize access in a separate browser or device". It's not clear in this sentence. The sentence is sort of run-on anyway, so maybe just start a new one and drop "after", saying: "The uploader will poll until the access is granted or rejected . . . "

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

if resp.status_code in {400, 401}:
raise ValueError("There must be an error in the request.")

if "error" in r and r["error"] == "authorization_pending":
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move this as the 2nd case since it is really not an "error" case. It's just the expected response when user has not yet completed the authorization flow. (Would you also mind adding a comment?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

if "access_token" in r:
return r
if "error" in r and r["error"] == "access_denied":
raise PermissionError("Auth was denied.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "Access" or "Authorization" instead of "Auth"? Maybe also include "by user" at the end of the sentence?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

if "error" in r and r["error"] == "authorization_pending":
time.sleep(polling_interval)
elif "error" in r and r["error"] == "slow_down":
time.sleep(int(polling_interval * 1.5))
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you leave a comment explaining the reasoning for this reaction to the error code? My initial reaction was that this error should actually stop the program - it would appear to indicate some programming error that we are not honoring polling_interval. But I can imagine some justification for doing it this way instead - I'm just not sure which justification you are using :).

Copy link
Member Author

Choose a reason for hiding this comment

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

Added a comment. I don't really expect this to be present in reality, but since it's a retryable error, I opted to handle it somehow.

I was imagining some strange edge-case in which the subsequent request is sent "too quickly" after the interval, and for some reason the server says we're polling too fast. Perhaps this is unrealistic. It shouldn't happen.

time.sleep(int(polling_interval * 1.5))
else:
raise RuntimeError(
"An unexpected error occurred while authenticating."
Copy link
Contributor

Choose a reason for hiding this comment

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

On line 281 you use "authorization" instead of "authentication". Can we be consistent?

self, auth_response
) -> google.oauth2.credentials.Credentials:

expiration_timestamp = datetime.datetime.utcfromtimestamp(
Copy link
Contributor

Choose a reason for hiding this comment

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

expiration_timestamp should actually be named expiration_datetime (the timestamp is the input to utcfromtimestamp()).

I'm kind of shocked that Credential.expiry is a datetime instead of a timestamp - what could they possibly need from the datetime?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. I thought it was a bit weird as well, but this is how they build the credentials here:
github.com/googleapis/google-auth-library-python-oauthlib/blob/main/google_auth_oauthlib/helpers.py#L140

Copy link
Contributor

@bmd3k bmd3k left a comment

Choose a reason for hiding this comment

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

Thanks!

)
)

def test_authenticate_user__no_force_console_override__has_display__uses_installed_app_flow(
Copy link
Contributor

Choose a reason for hiding this comment

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

Trying to get these names to be more concise, I'd recommend just naming this:

test_uses_installed_app_flow(self):

  • The test is already called AuthenticateUserTest, so perhaps "authenticate_user" is redundant.
  • I also consider the "installed app flow" the happy path so it seems unnecessary to include all the scenarios where it might not be used in the name. It might just be preferable to include a comment in the test about the conditions in which its used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Totally agree with the redundant authenticate_user.

For the rest... I think it's not always clear what the "happy path" is or why, and it could even change over time, so I like detailing the conditions for each test instead of assuming or describing one as the happy path. But I guess I agree it can be considerably less readable when trying to include the entire state in the test name, so I made them shorter.

Related to the verbosity of the state, I was trying to avoid using when and and, to make them shorter, but with your suggestions I removed some of the state part, and I think I found a reasonable brief name that allows better readability.

Thanks for the review and all the helpful comments!

self.assertTrue(fake_auth_flow.run_local_server_was_called)
self.mocked_device_auth_flow.assert_not_called()

def test_authenticate_user__no_force_console_override__no_display__uses_device_flow(
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps just name "test_uses_device_flow_when_no_display(self):"

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

self.mocked_device_auth_flow.assert_called_once()
self.mocked_device_auth_flow.return_value.run.assert_called_once()

def test_authenticate_user__no_force_console_override__has_display__webbrowser_error__uses_device_flow(
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps just name "test_uses_device_flow_on_webbrowser_error(self):"

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to something different, but it's a bit shorter and easier to read.

self.mocked_device_auth_flow.assert_called_once()
self.mocked_device_auth_flow.return_value.run.assert_called_once()

def test_authenticate_user__force_console_override__uses_device_flow(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps just name "test_uses_device_flow_when_force_console(self):"

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


self.flow.run()

device_params = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rename expected_device_params and expected_polling_params?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@arcra arcra merged commit 8da06b5 into tensorflow:master Jan 5, 2023
@arcra arcra deleted the uploader_auth branch January 5, 2023 23:18
arcra added a commit to arcra/tensorboard that referenced this pull request Jan 12, 2023
…auth flow. (tensorflow#6107)

* Motivation for features / changes
The OOB auth flow has been deprecated. We concluded that the
limited-input device flow is appropriate for our use case where the
uploader runs in an environment where a browser is not available.

* Technical description of changes
Implements a new auth flow which calls an RPC to fetch a device_code,
verification_url and user_code, and asks user to visit the
verification_url in another device and enter the user_code; then starts
polling for the access token after the user authorizes the access from
another device.

* Screenshots of UI changes
N/A

* Detailed steps to verify changes work correctly (as executed by you)
- Wrote test script that uses this class, and tested the auth flow and
was able to print the credentials.
- Wrote tests.

* Alternate designs / implementations considered
Basically, implementing something similar to this flow or the OOB flow
ourselves.

(cherry picked from commit 8da06b5)
@arcra arcra mentioned this pull request Jan 12, 2023
arcra added a commit to arcra/tensorboard that referenced this pull request Jan 12, 2023
…auth flow. (tensorflow#6107)

* Motivation for features / changes
The OOB auth flow has been deprecated. We concluded that the
limited-input device flow is appropriate for our use case where the
uploader runs in an environment where a browser is not available.

* Technical description of changes
Implements a new auth flow which calls an RPC to fetch a device_code,
verification_url and user_code, and asks user to visit the
verification_url in another device and enter the user_code; then starts
polling for the access token after the user authorizes the access from
another device.

* Screenshots of UI changes
N/A

* Detailed steps to verify changes work correctly (as executed by you)
- Wrote test script that uses this class, and tested the auth flow and
was able to print the credentials.
- Wrote tests.

* Alternate designs / implementations considered
Basically, implementing something similar to this flow or the OOB flow
ourselves.

(cherry picked from commit 8da06b5)
arcra added a commit that referenced this pull request Jan 13, 2023
…auth flow. (#6107)

* Motivation for features / changes
The OOB auth flow has been deprecated. We concluded that the
limited-input device flow is appropriate for our use case where the
uploader runs in an environment where a browser is not available.

* Technical description of changes
Implements a new auth flow which calls an RPC to fetch a device_code,
verification_url and user_code, and asks user to visit the
verification_url in another device and enter the user_code; then starts
polling for the access token after the user authorizes the access from
another device.

* Screenshots of UI changes
N/A

* Detailed steps to verify changes work correctly (as executed by you)
- Wrote test script that uses this class, and tested the auth flow and
was able to print the credentials.
- Wrote tests.

* Alternate designs / implementations considered
Basically, implementing something similar to this flow or the OOB flow
ourselves.

(cherry picked from commit 8da06b5)
@arcra arcra mentioned this pull request Jan 13, 2023
yatbear pushed a commit to yatbear/tensorboard that referenced this pull request Mar 27, 2023
…auth flow. (tensorflow#6107)

* Motivation for features / changes
The OOB auth flow has been deprecated. We concluded that the
limited-input device flow is appropriate for our use case where the
uploader runs in an environment where a browser is not available.

* Technical description of changes
Implements a new auth flow which calls an RPC to fetch a device_code,
verification_url and user_code, and asks user to visit the
verification_url in another device and enter the user_code; then starts
polling for the access token after the user authorizes the access from
another device.

* Screenshots of UI changes
N/A

* Detailed steps to verify changes work correctly (as executed by you)
- Wrote test script that uses this class, and tested the auth flow and
was able to print the credentials.
- Wrote tests.

* Alternate designs / implementations considered
Basically, implementing something similar to this flow or the OOB flow
ourselves.
@arcra arcra mentioned this pull request Mar 31, 2023
dna2github pushed a commit to dna2fork/tensorboard that referenced this pull request May 1, 2023
…auth flow. (tensorflow#6107)

* Motivation for features / changes
The OOB auth flow has been deprecated. We concluded that the
limited-input device flow is appropriate for our use case where the
uploader runs in an environment where a browser is not available.

* Technical description of changes
Implements a new auth flow which calls an RPC to fetch a device_code,
verification_url and user_code, and asks user to visit the
verification_url in another device and enter the user_code; then starts
polling for the access token after the user authorizes the access from
another device.

* Screenshots of UI changes
N/A

* Detailed steps to verify changes work correctly (as executed by you)
- Wrote test script that uses this class, and tested the auth flow and
was able to print the credentials.
- Wrote tests.

* Alternate designs / implementations considered
Basically, implementing something similar to this flow or the OOB flow
ourselves.
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.

None yet

2 participants