Skip to content
This repository has been archived by the owner on Aug 3, 2023. It is now read-only.

OAuth integration for Wrangler login #2048

Merged
merged 47 commits into from Sep 14, 2021
Merged

OAuth integration for Wrangler login #2048

merged 47 commits into from Sep 14, 2021

Conversation

ocsfrank
Copy link
Contributor

@ocsfrank ocsfrank commented Aug 30, 2021

Still in staging, but it should be ready for review.

Please also ignore the cargo.toml, I will update it once the service goes to prod. The CI test are failing because of oauth2 library.

@threepointone
Copy link
Contributor

This appears to be working well! Some feedback:

  • when it opens the browser, we log the browser url in the terminal. It's super verbose and feels unnecessary. @ocsfrank mentioned how it's for when the browser fails to open, which is a possibility in wsl. As a compromise, I suggest we only log the url in wsl.
  • I manually edited the token and deleted the characters, then when I run wrangler dev I get this message
    image
    but I would've expected the "invalid/corrupt token" messaging we recently added. This should probably be fixed? It shouldn't block landing this PR, it can happen in a followup PR.

Doing just a written approval here for the working, feel free to land after a code review.

src/commands/whoami.rs Outdated Show resolved Hide resolved
src/login/mod.rs Outdated

rsa.private_decrypt(&encrypted_token, &mut token_bytes, Padding::PKCS1)?;
let token = str::from_utf8(&token_bytes)?.trim_matches(char::from(0));
if wsl::is_wsl() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to check if the current process is run on WSL without having to include another dependency?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should just print this URL out despite its verbosity.. I get @threepointone's point for sure and generally agree.. but what about the case where users launch wrangler from within a Docker container? Probably not handled by this WSL crate, but wouldn't be able to open a host browser window either.

let mut user = user.clone();

// Check if oauth token is expired
let _res = check_update_oauth_token(&mut user).expect("Failed to refresh access token");
Copy link
Contributor

Choose a reason for hiding this comment

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

While it isn't likely that the user will be able to do anything if the expect is reached from an Err value, we may want to consider letting the dev caller handle this error or print something more useful here. "Failed to refresh access token" isn't as informative as whatever might come back from the API for this call.

@ocsfrank could you dig in a bit and see what other options are available here? The end-goal is providing as much to the wrangler user as possible for debugging.. as this stands, we just print this panic message and maybe a backtrace before suggesting they wrangler report the issue. But, it's unlikely that we can help them, or that they can help themselves without more info.

pub fn run() -> Result<()> {
let user_create = GlobalUser::new();
let mut has_auth = true;
if let Ok(user) = user_create {
Copy link
Contributor

Choose a reason for hiding this comment

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

can any/all of these be written as if let Ok(user) = GlobalUser::new() { ... } ? Doesn't look like you need the user_create binding anywhere. (I easily could be missing it though)

missing_permissions: &mut Vec<String>,
token_type: &str,
) -> Result<String> {
let token_auth_email = fetch_auth_token_email(user, missing_permissions)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly here, I think could be shortened to if let Some(token_auth_email) = fetch_auth_token_email(user, missing_permissions)? { ... }

"an API Token".to_string()
}
GlobalUser::ApiTokenAuth { .. } => get_token_type(user, &mut missing_permissions, "API")
.expect("Failed to get Api token type."),
Copy link
Contributor

Choose a reason for hiding this comment

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

s/Api/API

Also, I think this and the following match arm should bubble up this error, or print more of the returned error's context if its available. The panic will not be very useful, and an error report via wrangler report isn't likely to be resolved by our team.

src/login/mod.rs Outdated

rsa.private_decrypt(&encrypted_token, &mut token_bytes, Padding::PKCS1)?;
let token = str::from_utf8(&token_bytes)?.trim_matches(char::from(0));
if wsl::is_wsl() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should just print this URL out despite its verbosity.. I get @threepointone's point for sure and generally agree.. but what about the case where users launch wrangler from within a Docker container? Probably not handled by this WSL crate, but wouldn't be able to open a host browser window either.

src/login/mod.rs Outdated
let user = GlobalUser::TokenAuth {
api_token: token.to_string(),
// Get authorization code and CSRF state from local HTTP server
let params_response = match block_on(http_server_get_params()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason in particular to use the executor from futures here rather than tokio (asking because it looks like you're using tokio elsewhere) ? Not saying it doesn't work, but I think this means we're using different runtimes for various async operations here.

if result.is_err() {
log::debug!("Failed to invalidate OAuth token before login.");
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally in this file src/login/mod.rs (besides tests), there is a lot of unwrap and expect usage. I think some of these are in places that will create a lot of noise from wrangler report usage, and could be cleaned up a bit. Consider some ? usage in place of these where returning the error (even if it's printed later on) could give the user some way of debugging an issue.

Many of them might be fine. Just adding a comment here to suggest taking another pass at error handling along this path.

// Check if oauth token is expired
if let Ok(ref mut oauth_user) = new_user {
let _res =
check_update_oauth_token(oauth_user).expect("Failed to refresh access token");
Copy link
Contributor

Choose a reason for hiding this comment

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

Another case to consider more useful error handling / context-sharing with the end user.

|| ((oauth_token.is_ok() || refresh_token.is_ok() || expiration_time.is_ok())
&& (email.is_ok() || api_key.is_ok()))
{
let error_info = "\nMore than one authentication method (e.g. API token and OAuth token, or OAuth token and Global API key) has been found in the configuration file. Please use only one.";
Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably add more detail to this error:

  • print the path to the config file in case they want to manually edit it
  • suggest running wrangler logout (assuming this clears all auth info on disk?)

Comment on lines 200 to 214
} else if api_token.is_ok() {
Ok(Self::ApiTokenAuth {
api_token: api_token.expect("Failed to read API token"),
})
} else if email.is_ok() && api_key.is_ok() {
Ok(Self::GlobalKeyAuth {
email: email.expect("Failed to read email"),
api_key: api_key.expect("Failed to read api_key"),
})
} else if oauth_token.is_ok() && refresh_token.is_ok() && expiration_time.is_ok() {
Ok(Self::OAuthTokenAuth {
oauth_token: oauth_token.expect("Failed to read OAuth token"),
refresh_token: refresh_token.expect("Failed to read OAuth refresh token"),
expiration_time: expiration_time
.expect("Failed to read access token expiration time"),
Copy link
Contributor

Choose a reason for hiding this comment

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

I think all of these expect calls can just be unwrap, since you've already attested the are Ok in the if checks

})
} else {
// Empty configuration file and no environment variables, or missing variable for global API key
let error_info = "\nNo (valid) authentication method has been found.";
Copy link
Contributor

Choose a reason for hiding this comment

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

Could add more detail here too.. could running wrangler login help? let's offer some suggestions for a user to try.

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! That would help, and it's being printed in the next line with the call to Self::show_config_err_info(Some(error_info.to_string()), config).

}
});

// Receive authorization code and csrf state from HTTP server
let params = runtime.block_on(async {
let params = tokio::task::spawn(async move {
match rx.recv().await {
Some(values) => values,
None => "error".to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

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

since we have a history of difficult-to-debug channel errors, maybe we could be more descriptive and log this error (unless it is already being logged somewhere in the calling context).

do you know if it is possible that the rx can receive None at any point before it gets Some(values) and should therefore be polled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Logging is a good idea. The only case that I am aware of is when the sender side is closed for any reason, and rx still tries to read something. In this case, it should receive None.

Ok(Self::ApiTokenAuth {
api_token: api_token.unwrap(),
api_token: api_token_value,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: these could be re-written to be a bit more concise:

} else if let Ok(api_token) = api_token {
            Ok(Self::ApiTokenAuth { api_token})
// ... 

the same is true for all the branches below. if the variable binding (in this case inside the if let, shadowing the outer one) is the same as the field name, you can omit the field name and just pass in the variable name in its place. Not major, but might make the code a bit nicer to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see! Will include these changes in the next commit.

Copy link
Contributor

@nilslice nilslice left a comment

Choose a reason for hiding this comment

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

Nice work!

Copy link
Contributor

@nataliescottdavidson nataliescottdavidson left a comment

Choose a reason for hiding this comment

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

I have verified that the changes work as expected on my machine.

@ocsfrank ocsfrank merged commit 2416307 into master Sep 14, 2021
@delete-merged-branch delete-merged-branch bot deleted the frank/oauth branch September 14, 2021 15:30
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants