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

[WIP] Auth restructure #207

Closed
wants to merge 57 commits into from
Closed

Conversation

marioortizmanero
Copy link
Collaborator

@marioortizmanero marioortizmanero commented Apr 29, 2021

TODO:

  • Implement everything listed in Restructure the authentication process #173 for the new interface. This also allows us to remove derive_builder completely from the library.
  • Split the http module into a different crate
  • Implement PKCE authentication (maybe for another PR?)
  • Export some parts of the library as top-level imports (requested in Meta-Issue #127, but we might want to add more?)
  • Declare a common Config struct with things like token_cached or token_refreshing for Consider making the cache file a feature  #135. The former is implemented here, but the latter is left as a TODO.
  • Fix the examples
  • Rename tests like album.rs into client_credentials.rs, teaching about the authentication process instead.
  • Fix the tests
  • Make sure the endpoints are distributed properly into the different clients, and same for the tests.
  • Tidy up and self-review
  • Try to keep as few things from the Rspotify clients as possible in rspotify-http
  • Update changelog
  • Update documentation
  • Make sure the feature collision feature still works

Note that this only really separates the rspotify-http crate, as I don't think moving the auth logic to a separate crate is really worth it. We can discuss it but I'm not sure it's necessary at all.


Closes #207
Closes #208

@marioortizmanero marioortizmanero changed the base branch from master to separate-crates April 29, 2021 19:12
@marioortizmanero marioortizmanero marked this pull request as draft April 29, 2021 19:13
@marioortizmanero
Copy link
Collaborator Author

marioortizmanero commented Apr 29, 2021

Ok so turns out this trait-based implementation is only possible if we use -> Box<dyn Paginator> instead of -> impl Paginator.

This means that we are forced to use Box + dyn if we want it to work, for now. This is sub-optimal because there's a slight runtime overhead in comparison to generics, but on the other hand there should be less code bloat due to generics.

We already do something similar for every endpoint anyway because of maybe_async, which may add #[async_trait] to our HTTP client trait when an async client is configured. This proc macro does the same thing but for Future return types as a workaround until rust-lang/rust#63066 is implemented and stabilized.

This whole thing could also be fixed with delegation instead of a trait system, but that's not happening anytime soon, so I would keep going with traits for now: rust-lang/rfcs#2393 (comment).

@marioortizmanero
Copy link
Collaborator Author

Unfortunately the diff is going to be a mess because I'm splitting client.rs into different files and git isn't able to keep track of that. So I would appreciate no breaking/huge changes to the library while I'm working on this, if possible.

@marioortizmanero
Copy link
Collaborator Author

marioortizmanero commented Apr 30, 2021

So I've got a couple examples working now with the new interface. You can take a look and let me know your thoughts/suggestions:

* https://github.com/ramsayleung/rspotify/blob/auth-rewrite/examples/code_auth.rs
* https://github.com/ramsayleung/rspotify/blob/auth-rewrite/examples/client_credentials.rs

Actually all of them work now, except for the PKCE one which is still WIP. But I might leave the PKCE implementation for another PR to avoid changing too many things in this huge one.

ramsayleung
ramsayleung previously approved these changes May 13, 2021
@@ -25,19 +26,23 @@ resolver = "2"
[dependencies]
rspotify-macros = { path = "rspotify-macros", version = "0.10.0" }
rspotify-model = { path = "rspotify-model", version = "0.10.0" }
rspotify-http = { path = "rspotify-http", version = "0.10.0", default-features = false }
Copy link
Owner

Choose a reason for hiding this comment

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

Why doesn't rspotify-http enable by default?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because we need to manually specify which client to use. Perhaps rspotify-http shouldn't enable clients by default, and that should be done by rspotify itself?

Cargo.toml Outdated Show resolved Hide resolved
rspotify-http/Cargo.toml Outdated Show resolved Hide resolved

self.token = Some(self.fetch_access_token(&data).await?);

self.write_token_cache()
Copy link
Owner

Choose a reason for hiding this comment

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

Should we implement cache token feature in this PR or leave a TODO item?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I did implement cached tokens in this PR, but I've left refreshing ones for another. But you can just ignore them for now and review it later on.


/// Obtains the client access token for the app. The resulting token will be
/// saved internally.
#[maybe_async]
Copy link
Owner

Choose a reason for hiding this comment

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

The resulting token will be saved internally if the cache-token feature is enabled?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. This function calls write_token_cache at the end so it will be written (only if cached tokens are enabled, as you may see in said function's definition)

src/code_auth.rs Outdated
let token = self.fetch_access_token(&data).await?;
self.token = Some(token);

self.write_token_cache()
Copy link
Owner

Choose a reason for hiding this comment

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

Same as above

src/code_auth.rs Outdated
///
/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow
/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/code_auth.rs
/// [example-webapp]: https://github.com/ramsayleung/rspotify/tree/master/examples/webapp
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 will be better if we link reference with relative paths instead of HTTP links, which will be easily broken if I change username or repository

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we can link to the examples with relative paths, as they aren't uploaded to docs.rs (AFAIK). Or maybe I've misunderstood you?

src/code_auth.rs Outdated
}

/// This client has access to the base methods.
#[maybe_async(?Send)]
Copy link
Owner

Choose a reason for hiding this comment

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

Why would we need the Send trait?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This will probably explain it better than I can: https://docs.rs/async-trait/0.1.50/async_trait/#non-threadsafe-futures. I basically had to add + Send to each generic type in the trait functions, so passing (?Send) to the macro does that automatically.

///
/// Note: this method requires the `cli` feature.
#[cfg(feature = "cli")]
fn get_code_from_user(&self, url: &str) -> ClientResult<String> {
Copy link
Owner

Choose a reason for hiding this comment

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

Not sure it's suitable to put get_code_from_user into OAuthClient trait, since we could get code without token and cred parameters. Same as parse_response_code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But get_code_from_user doesn't really make sense if you're not using oauth, right? You need the code for the authorization flows, which is not needed in a base client.


/// Alias for `Iterator<Item = T>`, since sync mode is enabled.
pub trait Paginator<T>: Iterator<Item = T> {}
impl<T, I: Iterator<Item = T>> Paginator<T> for I {}
pub type Paginator<'a, T> = Box<dyn Iterator<Item = T> + 'a>;
Copy link
Owner

Choose a reason for hiding this comment

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

I don't totally get it now why would we need to declare type pub type Paginator<'a, T> = Box<dyn Iterator<Item = T> + 'a>; instead of the previous one ?

pub trait Paginator<T>: Iterator<Item = T> {}
impl<T, I: Iterator<Item = T>> Paginator<T> for I {}

Because we need some kinds of dynamic dispatch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A trait-based implementation of these pagination methods is only possible if we use -> Box<dyn Paginator> instead of -> impl Paginator. The compiler will complain about it the same way we can't have -> impl Future<...> in traits, which we fix with async_trait (a macro that basically adds the Box automatically) . This is technically temporary and will be fixed in future Rust versions, but I'd give it at least a year until that happens, really. So we are basically forced to use Box + dyn if we want it to work for now.

This is sub-optimal because there's a slight runtime overhead in comparison to generics, but on the other hand there should be less code bloat due to generics (not that either of these really matter, IMO).

@ramsayleung ramsayleung marked this pull request as ready for review May 13, 2021 14:51
@ramsayleung ramsayleung self-requested a review May 13, 2021 14:52
@ramsayleung ramsayleung marked this pull request as draft May 13, 2021 14:52
@ramsayleung
Copy link
Owner

Sorry for converting it from Draft to PR, my bad :(

What a huge PR, it takes me hours to give a rough review. The whole architecture of Auth process looks much clear and understandable than before, the current architecture looks good to me. Perhaps you could add some diagrams about the current Auth process.

  • Should ClientCredentialsSpotify be renamed to ClientCredsSpotify to shorten the name? It's quite lengthy and I did shorten CodeAuthorizationSpotify to CodeAuthSpotify.
  • Should CodeAuthSpotify be renamed to AuthCodeSpotify? It'd be more consistent with the real name, Authorization Code Flow
  • Should the endpoints module be renamed to clients? It's more fitting, as its contents aren't limited to endpoints anymore.

A big yes for all this suggestions.

By the ways, would you like splitting this PR into a Auth Rewrite PR and a Separate Crate PR, this PR has too many changes that it's a little hard to review line by line :)

@marioortizmanero
Copy link
Collaborator Author

marioortizmanero commented May 14, 2021

What a huge PR, it takes me hours to give a rough review. The whole architecture of Auth process looks much clear and understandable than before, the current architecture looks good to me. Perhaps you could add some diagrams about the current Auth process.

Yes, it's a complete rewrite so the diff is just very hard to follow. I did want to add some diagrams but I wanted to make sure you were ok with it first of all. Here's a quick summary: we now have the implementations in traits: rspotify::clients::BaseClient and rspotify::clients::OAuthClient. We can then create custom structs which implement their own methods in order to follow whatever auth flow they're for, like ClientCredsSpotify.

This has some advantages and some inconvenients:

  • GOOD: Initializing a struct for a specific flow is easier and more natural in comparison (no builder pattern needed, as you can see in the examples). It's basically a higher level representation of the spotify clients.

  • GOOD: The implementation is somewhat cleaner, since we don't have to check if the struct has an OAuth struct in order to access certain methods. We know we can access the OAuth member in OAuthClient because of the get_oauth required implementation for the trait. This eliminates the need for an InvalidAuth error in the client.

  • GOOD: The library is more type-safe, making it harder to make a mistake by calling an OAuth method in an unauthorized client.

  • BAD: code reuse isn't that great. For example we can't override a method in BaseClient when implemeting its "child" OAuthClient, which would have been neat for read_token_cache, for example. This is fixable by using different names or by not implementing it in the trait and instead doing so in the client struct itself, but it's more of a workaround.

    I've also had a bit of trouble sharing behavior between OAuth clients, namely AuthCodeSpotify and AuthCodePkceSpotify. I didn't know if e.g. prompt_for_token should go in the trait or in the client itself, and this happened for quite a few methods. I've ended up adding the methods to the trait if possible in order to reuse more code, but perhaps they should go in a different trait, as they might not fit as great as I thought. I don't know, it's subject to change.

    This whole architecture would probably be nicer to implement if we had delegation in Rust. But that's not supported officially as of now (there are plans for it, though). We could use a delegation macro but I didn't find the problems I mentioned earlier important enough. Not sure what you think about that.

  • BAD: users now have to import rspotify::prelude::* always. Which is not a big deal, but we have to make sure the documentation is clear, now that the endpoints are separated into different traits and clients (and so is the documentation).

If you have any doubts do let me know. I know it's hard to follow the first time after a rewrite.

By the ways, would you like splitting this PR into a Auth Rewrite PR and a Separate Crate PR, this PR has too many changes that it's a little hard to review line by line :)

How important is this? It would take me a bit of work to undo some changes and make two PRs instead. Which is not a problem, but as I said, I'd need a few more days until I'm done (and I'll be busier soon due to finals, so it might take more). Just letting you know, I understand that this PR is huge.

@ramsayleung
Copy link
Owner

ramsayleung commented May 14, 2021

I didn't know if e.g. prompt_for_token should go in the trait or in the client itself, and this happened for quite a few methods. I've ended up adding the methods to the trait if possible in order to reuse more code, but perhaps they should go in a different trait, as they might not fit as great as I thought. I don't know, it's subject to change.

IMHO, moving methods like prompt_for_token, parse_response_code, get_code_from_user into a different trait would be a better idea, since they have weak relationship with Auth flow.

How important is this? It would take me a bit of work to undo some changes and make two PRs instead.

It doesn't really matter, it's low priority.

@marioortizmanero
Copy link
Collaborator Author

IMHO, moving methods like prompt_for_token, parse_response_code, get_code_from_user into a different trait would be a better idea, since they have weak relationship with Auth flow.

How so? You only have to prompt for the user's authorization with the auth flows. Or am I missing something? I don't see why you'd use any of these in the client credentials flow.

@ramsayleung
Copy link
Owner

How so? You only have to prompt for the user's authorization with the auth flows. Or am I missing something? I don't see why you'd use any of these in the client credentials flow.

Oooh, I misunderstand something, you are right.

@marioortizmanero
Copy link
Collaborator Author

marioortizmanero commented Jun 19, 2021

Hello!

I've finally finished my finals, so I now have time to finish this. I've recreated the PR by splitting it up into four smaller PRs. The third one is still huge, but I don't think I can do anything about it; it separates the client into multiple ones, so the diff gets messed up. As I mentioned there, the endpoints themselves aren't changed, so you can ignore them.

If this still isn't digestible enough please let me know, I don't want to kill you with my reviews, @ramsayleung haha. I'll close this PR for now. I suggest you to start with the first PR up until the fourth. Afterwards we can merge all of them and push into master. But absolutely no rush.

Thanks a lot for the patience!

@marioortizmanero marioortizmanero deleted the auth-rewrite branch June 19, 2021 10:38
@ramsayleung
Copy link
Owner

If this still isn't digestible enough please let me know, I don't want to kill you with my reviews, @ramsayleung haha.

The good news is that there is a lot of reviews here, but it doesn't kill me. The bad news is it almost scares me out, haha :)

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.

Restructure the authentication process
2 participants