Skip to content

Restructure the authentication process #173

@marioortizmanero

Description

@marioortizmanero

Back when I first rewrote the authentication process I did improve a few things I thought were necessary, like only having a single http client per Spotify instance, and being more ergonomic and easy to use.

But it still leaves a few things to be desired. For example, an InvalidAuth error variant is needed for whenever a user calls a method from Spotify that isn't available due to the authentication process. If one were to use the Client Credentials Flow, the endpoints with access to the user's data shouldn't be available (say me or currently_playing_track). You need to use the Authentication Code Flow for that. This worked well enough because we only had two different methods: the Client Credentials and the Authentication Code.

Now that the PKCE Code Authentication Flow has to be implemented, I think it's time to re-think the architecture we're following and make it more idiomatic. This auth flow can access the same endpoints the regular Code Authentication Flow can, but it requires a different setup.

We mainly need three levels for this problem:

  • The HTTP client to actually make the requests, which already exists as rspotify::http::BaseClient. I'll call it HTTPClient from now on.
  • BaseClient, a base Spotify client, with access to endpoints such as tracks or search that don't need access to the user's data. This uses HTTPClient to make requests.
  • UserClient, an user authenticated Spotify client, with access to endpoints such as me. This may inherit from BaseClient and implement the extra endpoints over it, or use HTTPClient directly and only have the user-authenticated ones.

Rust doesn't really have OOP, but we can work it out with traits. Here's the first solution, where the UserClient inherits from BaseClient so that the Code Auth flows can access both UserClient and BaseClient methods:

image

And here's another solution with a more horizontal hierarchy. UserClient only includes the user authenticated methods because it doesn't inherit from BaseClient, so the Code Auth flows have to implement both of them. I feel like this is more natural in Rust, and it seems more modular because it doesn't assume that user authenticated flows can access the regular ones.

image

In both of these models the implementations take place in the traits, which only needs access to some HTTPClient methods, so the structs would only need to implement at most a couple getters for their internal fields, kinda like how Iterator works.

/// Just a rough idea of how it would look like to use the proposed models.
fn main() {
    // This is the same
    let credentials = CredentialsBuilder::from_env().build().unwrap();

    let spotify1 = CodeCredentialsFlow::default()
        .credentials(credentials)
        .build()
        .unwrap();

    // This wouldn't work, as `currently_playing_track` is not a member of
    // `CodeCredentialsFlow` (which only implements `BaseClient`).
    // spotify1.currently_playing_track();

    // This is the same
    let oauth = OAuthBuilder::from_env()
        .scope("user-read-playback-state")
        .build()
        .unwrap();

    let spotify2 = AuthCodeFlow::default()
        .credentials(credentials)
        .oauth(oauth)
        .build()
        .unwrap();

    let spotify3 = PKCEAuthCodeFlow::default()
        .credentials(credentials)
        .oauth(oauth)
        .build()
        .unwrap();
}

/// Now that we know what flow is being followed, we could also avoid
/// `derive_builder` so that we don't need `.build().unwrap()`.
fn main() {
    let spotify = CodeCredentialsFlow::new(credentials).unwrap();

    let spotify = AuthCodeFlow::new(credentials, oauth).unwrap();

    let spotify = PKCEAuthCodeFlow::new(credentials, oauth).unwrap();
}

This is purely theoretical so I'm not sure how easy it is to implement. I'll try to code a rough sketch of the latter model, although I'll be quite busy soon with finals so I'm not sure when that'll be possible.

What do you think? Any more ideas?

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionSome discussion is required before a decision is made or something is implementedenhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions