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

async API #46

Closed
wants to merge 6 commits into from
Closed

async API #46

wants to merge 6 commits into from

Conversation

legokichi
Copy link

@legokichi legokichi commented Oct 26, 2018

@@ -273,15 +276,18 @@ fn test_exchange_code_successful_with_complete_json_response() {
fn test_exchange_client_credentials_with_basic_auth() {
let mock = mock("POST", "/token")
// base64(urlencode("aaa/;&") + ":" + urlencode("bbb/;&"))
.match_header("Authorization", "Basic YWFhJTJGJTNCJTI2OmJiYiUyRiUzQiUyNg==")
.match_header("Authorization", "basic YWFhJTJGJTNCJTI2OmJiYiUyRiUzQiUyNg==")
Copy link
Author

Choose a reason for hiding this comment

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

Copy link
Owner

Choose a reason for hiding this comment

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

thanks for the reference! looking at https://tools.ietf.org/html/rfc7235#section-2.1, the auth scheme is indeed supposed to be case insensitive. hopefully this won't cause any compatibility issues with off-spec server implementations.

@legokichi
Copy link
Author

legokichi commented Oct 26, 2018

@ramosbugs
Copy link
Owner

thanks for the PR! I'm traveling this weekend, but I'll take a look next week.

@@ -236,6 +237,24 @@ use prelude::*;

const CONTENT_TYPE_JSON: &str = "application/json";

#[derive(Clone)]
/// This struct is used to maintain the strict URI encoding standard as proposed by RFC 3986
struct StrictEncodeSet;
Copy link
Author

@legokichi legokichi Oct 26, 2018

Choose a reason for hiding this comment

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

Copy link
Owner

Choose a reason for hiding this comment

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

I'm finding the chain of specs referenced by https://tools.ietf.org/html/rfc6749#appendix-B to be a bit hard to follow, so it's not super clear to me exactly which characters need to be escaped (especially since we're dealing with an HTTP header here, rather than a URI). This code does seem to maintain the current behavior as implemented in https://github.com/curl/curl/blob/master/lib/escape.c#L40, but I wonder if it would suffice to use
USERINFO_ENCODE_SET, which seems to be intended for username and password escaping (according to https://github.com/dtolnay/url/blob/master/UPGRADING.md).

@DeeUnderscore do you have any thoughts here, as the author of #41? (see usage below)

Copy link
Contributor

Choose a reason for hiding this comment

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

By my reading, this adheres to application/x-www-form-urlencoded—everything outside of the permitted bytes is encoded. This WHATWG spec seems to be essentially equivalent to what RFC6749 wanted when it described the encoding in Appendix B.

URLs can use forms of percent encoding which aren't application/x-www-form-urlencoded, since they permit more byte ranges (presumably to make URLs more readable). USERINFO_ENCODE_SET (WHATWG ref), in particular, is intended for the userinfo component of a URL, ie. the user:password in https://user:password@example.org/.

In practical terms, you could probably get away with a less strict encoding, and I suspect most OAuth implementations in the wild avoid using characters which could cause problems anyway. But, for closest adherence to the standard I'd recommend what this PR is doing.

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for the reference and rationale! That makes sense; I agree with the recommendation.

The encoding here does seem to deviate ever so slightly from the WHATWG spec, though:

  • spaces encoded with %20 instead of +
  • asterisks (0x2A) excluded from the encoding set but mentioned by WHATWG
  • tildes (0x7E) included in encoding set but excluded by WHATWG

None of these deviations seems particularly problematic, though it's probably safer for interoperability to err on the side of percent-encoding more values rather than fewer. Unless there's an error in the WHATWG spec, we should probably remove tildes from this set.

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems reasonable. While the RFC specifically gives spaces as + in an example, a properly implemented decoder should consume %20 fine, too.

src/lib.rs Outdated
) -> Result<RequestTokenResponse, curl::Error> {
let mut easy = Easy::new();
) -> impl Future<Item = RequestTokenResponse, Error = reqwest::Error> + Send + 'static {
let mut req_builder = ::reqwest::async::Client::new().post(&token_url.to_string()[..]);
Copy link
Author

Choose a reason for hiding this comment

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

I think add hyper::Client to oauth2::Client structure as member and create oauth2::Client::with_connector static method.

@ramosbugs
Copy link
Owner

Sorry for the delay! Looking this PR over now.

I'm thinking of using hyper instead of reqwest.
reqwest does not have HttpConnector API -
https://docs.rs/hyper/0.12.12/hyper/client/struct.HttpConnector.html

I think it's definitely useful (often required) for clients to be able to specify connection parameters, including TLS settings (trusted CA certs, cipher suites, etc.) and proxy configs. This functionality has been missing from this crate so far, so thank you for bringing it up!

However, it doesn't look like hyper::client::HttpConnector really lets us specify these parameters. Rather, that interface seems to deal with lower level network options. What sort of customization were you hoping to support via HttpConnector?

As an alternative, reqwest::async::ClientBuilder does seem to allow significant customization: https://docs.rs/reqwest/0.9.4/reqwest/async/struct.ClientBuilder.html. What if we add a set_http_client method to oauth2::Client so that users can pass in their own reqwest::async::Clients?

it looks like this one refers to a different interface (openssl's SslConnector struct rather than hyper's Connect trait)


I'll submit comments on the rest of the PR soon.

Copy link
Owner

@ramosbugs ramosbugs left a comment

Choose a reason for hiding this comment

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

Thanks again for the PR!

In addition to supporting the async API, I think we should still provide a sync API. That should reduce the boilerplate for callers that want sync behavior, and in particular make this breaking change easier for existing code to adopt.

We probably want both sync and async versions of Client, similar to what reqwest does. What I'm not sure of is which interface should be the default. We could either have oauth2::Client be sync and oauth2::async::Client be async or vice versa (oauth2::Client is async and oauth2::sync::Client is sync).

The former is nice because the default interface matches the underlying implementation, while the latter is nice because it avoids making a breaking change altogether (I think). What do you think?

Cargo.toml Outdated
@@ -18,3 +19,4 @@ url = "1.0"

[dev-dependencies]
mockito = "0.8.2"
tokio = "0.1"
Copy link
Owner

Choose a reason for hiding this comment

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

let's add a newline at the end here

src/lib.rs Outdated
@@ -211,21 +211,22 @@
//!

extern crate base64;
extern crate curl;
extern crate futures;
extern crate reqwest;
Copy link
Owner

Choose a reason for hiding this comment

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

nit: sort imports

Suggested change
extern crate reqwest;

src/lib.rs Show resolved Hide resolved
src/lib.rs Outdated
use std::fmt::{Debug, Display, Error as FormatterError, Formatter};
use std::marker::{Send, Sync, PhantomData};
use std::ops::Deref;
use std::time::Duration;

use curl::easy::Easy;
use url::percent_encoding::{utf8_percent_encode, EncodeSet};
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
use url::percent_encoding::{utf8_percent_encode, EncodeSet};

src/lib.rs Show resolved Hide resolved
src/lib.rs Show resolved Hide resolved
src/lib.rs Outdated
let content_type = res
.headers()
.get(::reqwest::header::CONTENT_TYPE)
.map(|o| o.to_str().unwrap().to_string());
Copy link
Owner

Choose a reason for hiding this comment

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

a panic is probably undesirable here. it seems like we have a few options:

  • return an Err if the header contains non-ASCII chars
  • set content_type to None in this case
  • change the type of RequestTokenResponse::content_type to Option<reqwest::header::HeaderValue>

I think the last option is probably the cleanest here, but the first seems fine too.

src/lib.rs Outdated
let token_url = self.token_url.as_ref().unwrap();
let fut = self.post_request_token(token_url, params).map_err(RequestTokenError::Request)
.and_then(|token_response|{
Ok(()).and_then(|()| -> Result<_, _> {
Copy link
Owner

Choose a reason for hiding this comment

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

I think we can simplify this to:

.and_then(|token_response| {
    if token_response.http_status != 200 {
        ....
    }
    ...
})

In other words, the inner closure is already returning a Result, which transforms automatically into FutureResult courtesy of impl From<Result<T, E>> for FutureResult<T, E>. This lets us remove the redundant:

Ok(()).and_then(|| {
    ...
}).into_future()

@@ -236,6 +237,24 @@ use prelude::*;

const CONTENT_TYPE_JSON: &str = "application/json";

#[derive(Clone)]
/// This struct is used to maintain the strict URI encoding standard as proposed by RFC 3986
struct StrictEncodeSet;
Copy link
Owner

Choose a reason for hiding this comment

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

I'm finding the chain of specs referenced by https://tools.ietf.org/html/rfc6749#appendix-B to be a bit hard to follow, so it's not super clear to me exactly which characters need to be escaped (especially since we're dealing with an HTTP header here, rather than a URI). This code does seem to maintain the current behavior as implemented in https://github.com/curl/curl/blob/master/lib/escape.c#L40, but I wonder if it would suffice to use
USERINFO_ENCODE_SET, which seems to be intended for username and password escaping (according to https://github.com/dtolnay/url/blob/master/UPGRADING.md).

@DeeUnderscore do you have any thoughts here, as the author of #41? (see usage below)

tests/lib.rs Outdated
@@ -202,8 +203,9 @@ fn test_exchange_code_successful_with_minimal_json_response() {
Url::parse(&(SERVER_URL.to_string() + "/token")).unwrap(),
)),
);
let token = client
.exchange_code(AuthorizationCode::new("ccc".to_string()))
let token = ::tokio::runtime::Runtime::new().unwrap().block_on(
Copy link
Owner

Choose a reason for hiding this comment

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

since each test needs to do this, let's define a function that takes a future, creates a runtime, blocks on it, and returns the result. it's probably worth trying to minimize the test boilerplate.

}

/// async edition
pub fn exchange_code_async(
Copy link
Author

Choose a reason for hiding this comment

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

futures trait also changes greatly in Rust 2018.
How about doing this as a provisional response for the past few months?

  • exchange_code_async
  • exchange_password_async
  • exchange_client_credentials_async
  • exchange_refresh_token_async

Copy link
Author

@legokichi legokichi Nov 8, 2018

Choose a reason for hiding this comment

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

Alternative choice: just constructs and returns http::request::Request object.
We can chose own http request library e.g. curl, reqwest, hyper, actix-web ...etc.

Copy link
Owner

Choose a reason for hiding this comment

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

I think it's fine to put the pre-Rust 2018 async functionality into this crate. However, I think we should do this via a separate Client implementation (i.e., oauth2::async::Client). Then, when we're ready to add support for Rust 2018 async, we can call that oauth2::async2018::Client or something. Each of these can present the same exchange_* functions, just with different return types. This change will probably require some refactoring to share as much code as possible between the different Client implementations.

Copy link
Owner

Choose a reason for hiding this comment

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

also, the reason I don't think returning Request objects would work well is that we'd then have to offer an interface for callers to pass the response back in for us to parse

@legokichi
Copy link
Author

legokichi commented Nov 8, 2018

let underscore = byte == 0x5f;
let tilde = byte == 0x7e;
let period = byte == 0x2e;
!(upper || lower || numeric || hyphen || underscore || tilde || period)
Copy link
Owner

Choose a reason for hiding this comment

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

based on the conversation above, I think we should probably remove tilde

}

/// async edition
pub fn exchange_code_async(
Copy link
Owner

Choose a reason for hiding this comment

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

I think it's fine to put the pre-Rust 2018 async functionality into this crate. However, I think we should do this via a separate Client implementation (i.e., oauth2::async::Client). Then, when we're ready to add support for Rust 2018 async, we can call that oauth2::async2018::Client or something. Each of these can present the same exchange_* functions, just with different return types. This change will probably require some refactoring to share as much code as possible between the different Client implementations.

Copy link
Owner

@ramosbugs ramosbugs left a comment

Choose a reason for hiding this comment

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

Looks like there are some merge conflicts now based some other PRs I merged

}
let token_url = self.token_url.as_ref().unwrap();
let fut = self.post_request_token(token_url, params).map_err(RequestTokenError::Request)
.and_then(|token_response|{
Copy link
Owner

Choose a reason for hiding this comment

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

Added some minor cleanup here to 3ca27ff. Mind pulling in those changes, or giving me permissions to update this PR?

src/lib.rs Show resolved Hide resolved
}

/// async edition
pub fn exchange_code_async(
Copy link
Owner

Choose a reason for hiding this comment

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

also, the reason I don't think returning Request objects would work well is that we'd then have to offer an interface for callers to pass the response back in for us to parse

@ramosbugs
Copy link
Owner

Here are the implementations of https_connector.

Ohh, I see. So it looks like what we really want to accept is a struct that implements the hyper::client::connect::Connect trait, which can do anything it wants as long as it yields a (hyper::client::connect::Connect::Transport, hyper::client::connect::Connected). Something like what you mentioned above should work, e.g.:

pub struct Client<C: Connect, EF: ExtraTokenFields, TT: TokenType, TE: ErrorResponseType> {
    connector: C,
    ...
}

// new() sets a sensible default (and secure) connector (require HTTPS, etc.)

pub fn with_connector(mut self, connector: C) -> Self {
    self.connector = connector;

    self
}

@bachp
Copy link

bachp commented Dec 27, 2018

I would like to use this in the context of actix-web. So I would be very happy if there is a way to specify the client library to use (reqwest, actix, ...). Did you already give any toughts on how this could be done?

If it would be desired to have this as a feature I might be able to come up with a PR that allows to select actix-web as a replacement for reqwest.

@ramosbugs
Copy link
Owner

I would like to use this in the context of actix-web. So I would be very happy if there is a way to specify the client library to use (reqwest, actix, ...). Did you already give any toughts on how this could be done?

I think the simplest approach to supporting multiple HTTP clients is probably via a trait. Since the hyper::client::connect::Connect trait is quite low level, I think it's a good candidate. This also lets people use hyper by default, which seems to have a significantly larger volume of downloads on crates.io than reqwest or actix-web. Other client libraries could potentially be used by writing wrappers that implement the hyper::client::connect::Connect trait on top of those libraries.

Alternatively, we could probably do something at compile time based on feature flags, but I haven't looked into that much, and it feels like that would be harder to maintain over time.

Was there specific functionality from actix-web that you were looking to use in conjunction with this crate? Understanding the use case a bit more might help illuminate the right solution.

I think it's also probably worth filing a separate Github issue for this so that we can refocus #44 on the async discussion, and the new issue on the need for a generic HTTP client interface.

@ramosbugs
Copy link
Owner

latest master now features an async API with pluggable HTTP clients

@ramosbugs ramosbugs closed this Jun 9, 2019
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

4 participants