diff --git a/SpotifyAPI.Docs/docs/auth_introduction.md b/SpotifyAPI.Docs/docs/auth_introduction.md index ac1a210b..e6283cac 100644 --- a/SpotifyAPI.Docs/docs/auth_introduction.md +++ b/SpotifyAPI.Docs/docs/auth_introduction.md @@ -3,4 +3,18 @@ id: auth_introduction title: Introduction --- -Hello +Spotify does not allow unauthorized access to the api. Thus, you need an access token to make requets. This access token can be gathered via multiple schemes, all following the OAuth2 spec. Since it's important to choose the correct scheme for your usecase, make sure you have a grasp of the following terminology/docs: + +* OAuth2 +* [Spotify Authorization Flows](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow) + +Since every auth flow also needs an application in the [spotify dashboard](https://developer.spotify.com/dashboard/), make sure you have the necessary values (like `Client Id` and `Client Secret`). + +Then, continue with the docs of the specific auth flows: + +* [Client Credentials](client_credentials) +* [Implicit Grant](implicit_grant) +* [Authorization Code](authorization_code) +* Token Swap + +![auth comparison](/img/auth_comparison.png) diff --git a/SpotifyAPI.Docs/docs/authorization_code.md b/SpotifyAPI.Docs/docs/authorization_code.md new file mode 100644 index 00000000..8c097921 --- /dev/null +++ b/SpotifyAPI.Docs/docs/authorization_code.md @@ -0,0 +1,104 @@ +--- +id: authorization_code +title: Authorization Code +--- + +> This flow is suitable for long-running applications in which the user grants permission only once. It provides an access token that can be refreshed. Since the token exchange involves sending your secret key, perform this on a secure location, like a backend service, and not from a client such as a browser or from a mobile app. + +## Existing Web-Server + +If you are already in control of a Web-Server (like `ASP.NET`), you can start the flow by generating a login uri + +```csharp +// Make sure "http://localhost:5000" is in your applications redirect URIs! +var loginRequest = new LoginRequest( + new Uri("http://localhost:5000"), + "ClientId", + LoginRequest.ResponseType.Code +) +{ + Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative } +}; +var uri = loginRequest.ToUri(); +// Redirect user to uri via your favorite web-server +``` + +When the user is redirected to the generated uri, he will have to login with his spotify account and confirm, that your application wants to access his user data. Once confirmed, he will be redirect to `http://localhost:5000` and a `code` parameter is attached to the query. This `code` has to be exchanged for an `access_token` and `refresh_token`: + +```csharp +// This method should be called from your web-server when the user visits "http://localhost:5000" +public Task GetCallback(string code) +{ + var response = await new OAuthClient().RequestToken( + new AuthorizationCodeTokenRequest("ClientId", "ClientSecret", code, "http://localhost:5000") + ); + + var spotify = new SpotifyClient(response.AccessToken); + // Also important for later: response.RefreshToken +} +``` + +If the token expires at some point (check via `response.IsExpired`), you can refresh it: + +```csharp + var newResponse = await new OAuthClient().RequestToken( + new AuthorizationCodeRefreshRequest("ClientId", "ClientSecret", response.RefreshToken) + ); + + var spotify = new SpotifyClient(newResponse.AccessToken); +``` + +You can also let the `AuthorizationCodeAuthenticator` take care of the refresh part: + +```csharp +var response = await new OAuthClient().RequestToken( + new AuthorizationCodeTokenRequest("ClientId", "ClientSecret", code, "http://localhost:5000") +); +var config = SpotifyClientConfig + .CreateDefault() + .WithAuthenticator(new AuthorizationCodeAuthenticator("ClientId", "ClientSecret", response)); + +var spotify = new SpotifyClient(config); +``` + +## Using Spotify.Web.Auth + +For cross-platform CLI and desktop apps (non `UWP` apps), `Spotify.Web.Auth` can be used to supply a small embedded Web Server for the code retrieval. + +:::warning +You're client secret will be exposed when embedded in a desktop/cli app. This can be abused and is not preffered. If possible, let the user create an application in spotify dashboard or let a server handle the spotify communication. +::: + +```csharp +private static EmbedIOAuthServer _server; + +public static async Task Main() +{ + // Make sure "http://localhost:5000/callback" is in your spotify application as redirect uri! + _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000); + await _server.Start(); + + _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; + + var request = new LoginRequest(_server.BaseUri, "ClientId", LoginRequest.ResponseType.Code) + { + Scope = new List { Scopes.UserReadEmail } + }; + BrowserUtil.Open(uri); +} + +private static async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response) +{ + await _server.Stop(); + + var config = SpotifyClientConfig.CreateDefault(); + var tokenResponse = await new OAuthClient(config).RequestToken( + new AuthorizationCodeTokenRequest( + "ClientId", "ClientSecret", response.Code, "http://localhost:5000/callback" + ) + ); + + var spotify = new SpotifyClient(tokenResponse.AccessToken); + // do calls with spotify and save token? +} +``` diff --git a/SpotifyAPI.Docs/docs/client_credentials.md b/SpotifyAPI.Docs/docs/client_credentials.md new file mode 100644 index 00000000..32847982 --- /dev/null +++ b/SpotifyAPI.Docs/docs/client_credentials.md @@ -0,0 +1,45 @@ +--- +id: client_credentials +title: Client Credentials +--- + +> The Client Credentials flow is used in server-to-server authentication. +> Only endpoints that do not access user information can be accessed. + +By supplying your `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`, you get an access token. + +## Request token once + +To request an access token, build a `ClientCredentialsRequest` and send it via `OAuthClient`. This access token will expire after some time and you need to repeat the process. + +```csharp +public static async Task Main() +{ + var config = SpotifyClientConfig.CreateDefault(); + + var request = new ClientCredentialsRequest("CLIENT_ID", "CLIENT_SECRET"); + var response = await new OAuthClient(config).RequestToken(request); + + var spotify = new SpotifyClient(config.WithToken(response.AccessToken)); +} +``` + +## Request Token On-Demand + +You can also use `CredentialsAuthenticator`, which will make sure the spotify instance will always have an up-to-date access token by automatically refreshing the token on-demand. + +```csharp +public static async Task Main() +{ + var config = SpotifyClientConfig + .CreateDefault() + .WithAuthenticator(new CredentialsAuthenticator("CLIENT_ID", "CLIENT_SECRET")); + + var spotify = new SpotifyClient(config); +} +``` + +:::info +There is no thread safety guaranteed when using `CredentialsAuthenticator`. +::: + diff --git a/SpotifyAPI.Docs/docs/implicit_grant.md b/SpotifyAPI.Docs/docs/implicit_grant.md new file mode 100644 index 00000000..707c66e4 --- /dev/null +++ b/SpotifyAPI.Docs/docs/implicit_grant.md @@ -0,0 +1,106 @@ +--- +id: implicit_grant +title: Implicit Grant +--- + +> Implicit grant flow is for clients that are implemented entirely using JavaScript and running in the resource owner’s browser. You do not need any server-side code to use it. Rate limits for requests are improved but there is no refresh token provided. This flow is described in RFC-6749. + +This flow is useful for getting a user access token for a short timespan + +## Existing Web-Server + +If you are already in control of a Web-Server (like `ASP.NET`), you can start the flow by generating a login uri + +```csharp +// Make sure "http://localhost:5000" is in your applications redirect URIs! +var loginRequest = new LoginRequest( + new Uri("http://localhost:5000"), + "ClientId", + LoginRequest.ResponseType.Token +) +{ + Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative } +}; +var uri = loginRequest.ToUri(); +// Redirect user to uri via your favorite web-server +``` + +When the user is redirected to the generated uri, he will have to login with his spotify account and confirm, that your application wants to access his user data. Once confirmed, he will be redirect to `http://localhost:5000` and the fragment identifier (`#` part of URI) will contain an access token. + +:::warning +Note, this parameter is not sent to the server! You need JavaScript to access it. +::: + +## Using custom Protocols + +This flow can also be used with custom protocols instead of `http`/`https`. This is especially interesting for `UWP` apps, since your able to register custom protocol handlers quite easily. + +![protocol handlers](/img/auth_protocol_handlers.png) + +The process is very similar, you generate a uri and open it for the user + +```csharp +// Make sure "spotifyapi.web.oauth://token" is in your applications redirect URIs! +var loginRequest = new LoginRequest( + new Uri("spotifyapi.web.oauth://token"), + "ClientId", + LoginRequest.ResponseType.Token +) +{ + Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative } +}; +var uri = loginRequest.ToUri(); + +// This call requires Spotify.Web.Auth +BrowserUtil.Open(uri); +``` + +After the user logged in and consented your app, your `UWP` app will receive a callback: + +```csharp +protected override void OnActivated(IActivatedEventArgs args) +{ + if (args.Kind == ActivationKind.Protocol) + { + ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs; + var publisher = Mvx.IoCProvider.Resolve(); + + // This Uri contains your access token in the Fragment part + Console.WriteLine(eventArgs.Uri); + } +} +``` + +For a real example, have a look at the [UWP Example](https://github.com/JohnnyCrazy/SpotifyAPI-NET/tree/v6/SpotifyAPI.Web.Examples/Example.UWP) + +# Using Spotify.Web.Auth + +For cross-platform CLI and desktop apps (non `UWP` apps), custom protocol handlers are sometimes not an option. The fallback here is a small cross-platform embedded web server running on `http://localhost:5000` serving javascript. The javscript will parse the fragment part of the URI and sends a request to the web server in the background. The web server then notifies your appliciation via event. + +```csharp +private static EmbedIOAuthServer _server; + +public static async Task Main() +{ + // Make sure "http://localhost:5000/callback" is in your spotify application as redirect uri! + _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000); + await _server.Start(); + + _server.ImplictGrantReceived += OnImplictGrantReceived; + + var request = new LoginRequest(_server.BaseUri, "ClientId", LoginRequest.ResponseType.Code) + { + Scope = new List { Scopes.UserReadEmail } + }; + BrowserUtil.Open(uri); +} + +private static async Task OnImplictGrantReceived(object sender, ImplictGrantResponse response) +{ + await _server.Stop(); + var spotify = new SpotifyClient(response.AccessToken); + // do calls with spotify +} +``` + +For real examples, have a look at [Example.CLI.PersistentConfig](https://github.com/JohnnyCrazy/SpotifyAPI-NET/tree/v6/SpotifyAPI.Web.Examples/Example.CLI.PersistentConfig) and [Example.CLI.CustomHTML](https://github.com/JohnnyCrazy/SpotifyAPI-NET/tree/v6/SpotifyAPI.Web.Examples/Example.CLI.CustomHTML) diff --git a/SpotifyAPI.Docs/docs/unit_testing.md b/SpotifyAPI.Docs/docs/unit_testing.md new file mode 100644 index 00000000..f0da54a7 --- /dev/null +++ b/SpotifyAPI.Docs/docs/unit_testing.md @@ -0,0 +1,35 @@ +--- +id: unit_testing +title: Unit Testing +--- + +The modular structure of the library makes it easy to mock the API when unit testing. Consider the following method: + +```csharp +public static async Task IsAdmin(IUserProfileClient userProfileClient) +{ + // get loggedin user + var user = await userProfileClient.Current(); + + // only my user id is an admin + return user.Id == "1122095781"; +} +``` + +Using `Moq`, this can be tested without doing any network requests: + +```csharp +[Test] +public async Task IsAdmin_SuccessTest() +{ + var userProfileClient = new Mock(); + userProfileClient.Setup(u => u.Current()).Returns( + Task.FromResult(new PrivateUser + { + Id = "1122095781" + }) + ); + + Assert.AreEqual(true, await IsAdmin(userProfileClient.Object)); +} +``` diff --git a/SpotifyAPI.Docs/sidebars.js b/SpotifyAPI.Docs/sidebars.js index e3175973..f9d616f6 100644 --- a/SpotifyAPI.Docs/sidebars.js +++ b/SpotifyAPI.Docs/sidebars.js @@ -13,6 +13,7 @@ module.exports = { 'proxy', 'pagination', 'retry_handling', + 'unit_testing' ] }, { @@ -20,6 +21,9 @@ module.exports = { label: 'Authentication Guides', items: [ 'auth_introduction', + 'client_credentials', + 'implicit_grant', + 'authorization_code' ] }, { diff --git a/SpotifyAPI.Docs/static/img/auth_comparison.png b/SpotifyAPI.Docs/static/img/auth_comparison.png new file mode 100644 index 00000000..60553ef5 Binary files /dev/null and b/SpotifyAPI.Docs/static/img/auth_comparison.png differ diff --git a/SpotifyAPI.Docs/static/img/auth_protocol_handlers.png b/SpotifyAPI.Docs/static/img/auth_protocol_handlers.png new file mode 100644 index 00000000..f3053022 Binary files /dev/null and b/SpotifyAPI.Docs/static/img/auth_protocol_handlers.png differ diff --git a/SpotifyAPI.Web.Tests/Http/SimpleRetryHandlerTest.cs b/SpotifyAPI.Web.Tests/Http/SimpleRetryHandlerTest.cs index d2d3550a..4e13f516 100644 --- a/SpotifyAPI.Web.Tests/Http/SimpleRetryHandlerTest.cs +++ b/SpotifyAPI.Web.Tests/Http/SimpleRetryHandlerTest.cs @@ -36,7 +36,7 @@ namespace SpotifyAPI.Web Assert.AreEqual(2, retryCalled); Assert.AreEqual(setup.Response.Object, response); - setup.Sleep.Verify(s => s(TimeSpan.FromMilliseconds(50)), Times.Exactly(2)); + setup.Sleep.Verify(s => s(TimeSpan.FromSeconds(50)), Times.Exactly(2)); } [Test] @@ -67,7 +67,7 @@ namespace SpotifyAPI.Web Assert.AreEqual(1, retryCalled); Assert.AreEqual(successResponse.Object, response); - setup.Sleep.Verify(s => s(TimeSpan.FromMilliseconds(50)), Times.Once); + setup.Sleep.Verify(s => s(TimeSpan.FromSeconds(50)), Times.Once); } [Test] @@ -97,7 +97,7 @@ namespace SpotifyAPI.Web Assert.AreEqual(1, retryCalled); Assert.AreEqual(successResponse.Object, response); - setup.Sleep.Verify(s => s(TimeSpan.FromMilliseconds(50)), Times.Once); + setup.Sleep.Verify(s => s(TimeSpan.FromSeconds(50)), Times.Once); } [Test]