mirror of
https://github.com/Sarsoo/Spotify.NET.git
synced 2024-12-23 22:56:25 +00:00
Finished PKCE Docs & Implementation - Example.CLI.PersistentConfig now uses PKCE
This commit is contained in:
parent
9d22c04764
commit
24459453a6
@ -7,16 +7,17 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
|
|||||||
|
|
||||||
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:
|
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
|
- OAuth2
|
||||||
* [Spotify Authorization Flows](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow)
|
- [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`).
|
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:
|
Then, continue with the docs of the specific auth flows:
|
||||||
|
|
||||||
* [Client Credentials](client_credentials.md)
|
- [Client Credentials](client_credentials.md)
|
||||||
* [Implicit Grant](implicit_grant.md)
|
- [Implicit Grant](implicit_grant.md)
|
||||||
* [Authorization Code](authorization_code.md)
|
- [Authorization Code](authorization_code.md)
|
||||||
* [Token Swap](token_swap.md)
|
- [PKCE](pkce.md)
|
||||||
|
- [(Token Swap)](token_swap.md)
|
||||||
|
|
||||||
<img alt="auth comparison" src={useBaseUrl('img/auth_comparison.png')} />
|
<img alt="auth comparison" src={useBaseUrl('img/auth_comparison.png')} />
|
||||||
|
@ -16,7 +16,7 @@ var (verifier, challenge) = PKCEUtil.GenerateCodes();
|
|||||||
// Generates a secure random verifier of length 120 and its challenge
|
// Generates a secure random verifier of length 120 and its challenge
|
||||||
var (verifier, challenge) = PKCEUtil.GenerateCodes(120);
|
var (verifier, challenge) = PKCEUtil.GenerateCodes(120);
|
||||||
|
|
||||||
// Returns the passed string and its challenge (Make sure it's random and is long enough)
|
// Returns the passed string and its challenge (Make sure it's random and long enough)
|
||||||
var (verifier, challenge) = PKCEUtil.GenerateCodes("YourSecureRandomString");
|
var (verifier, challenge) = PKCEUtil.GenerateCodes("YourSecureRandomString");
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ var loginRequest = new LoginRequest(
|
|||||||
Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative }
|
Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative }
|
||||||
};
|
};
|
||||||
var uri = loginRequest.ToUri();
|
var uri = loginRequest.ToUri();
|
||||||
// Redirect user to uri via your favorite web-server
|
// Redirect user to uri via your favorite web-server or open a local browser window
|
||||||
```
|
```
|
||||||
|
|
||||||
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/callback` and a `code` parameter is attached to the query. The redirect URI can also contain a custom protocol paired with UWP App Custom Protocol handler. This received `code` has to be exchanged for an `access_token` and `refresh_token`:
|
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/callback` and a `code` parameter is attached to the query. The redirect URI can also contain a custom protocol paired with UWP App Custom Protocol handler. This received `code` has to be exchanged for an `access_token` and `refresh_token`:
|
||||||
@ -47,11 +47,11 @@ When the user is redirected to the generated uri, he will have to login with his
|
|||||||
public Task GetCallback(string code)
|
public Task GetCallback(string code)
|
||||||
{
|
{
|
||||||
// Note that we use the verifier calculated above!
|
// Note that we use the verifier calculated above!
|
||||||
var response = await new OAuthClient().RequestToken(
|
var initialResponse = await new OAuthClient().RequestToken(
|
||||||
new PKCETokenRequest("ClientId", code, "http://localhost:5000", verifier)
|
new PKCETokenRequest("ClientId", code, "http://localhost:5000", verifier)
|
||||||
);
|
);
|
||||||
|
|
||||||
var spotify = new SpotifyClient(response.AccessToken);
|
var spotify = new SpotifyClient(initialResponse.AccessToken);
|
||||||
// Also important for later: response.RefreshToken
|
// Also important for later: response.RefreshToken
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -59,11 +59,19 @@ public Task GetCallback(string code)
|
|||||||
With PKCE you can also refresh tokens once they're expired:
|
With PKCE you can also refresh tokens once they're expired:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var response = await new OAuthClient().RequestToken(
|
var newResponse = await new OAuthClient().RequestToken(
|
||||||
new PKCETokenRefreshRequest("ClientId", oldResponse.RefreshToken)
|
new PKCETokenRefreshRequest("ClientId", initialResponse.RefreshToken)
|
||||||
);
|
);
|
||||||
|
|
||||||
var spotify = new SpotifyClient(response.AccessToken);
|
var spotify = new SpotifyClient(newResponse.AccessToken);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you do not want to take care of manually refreshing tokens, you can use `PKCEAuthenticator`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var authenticator = new PKCEAuthenticator(clientId, initialResponse);
|
||||||
|
|
||||||
|
var config = SpotifyClientConfig.CreateDefault()
|
||||||
|
.WithAuthenticator(authenticator);
|
||||||
|
var spotify = new SpotifyClient(config);
|
||||||
|
```
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
@ -2,11 +2,11 @@ using System.IO;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System;
|
using System;
|
||||||
using SpotifyAPI.Web.Auth;
|
using SpotifyAPI.Web.Auth;
|
||||||
using SpotifyAPI.Web.Http;
|
|
||||||
using SpotifyAPI.Web;
|
using SpotifyAPI.Web;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using static SpotifyAPI.Web.Scopes;
|
using static SpotifyAPI.Web.Scopes;
|
||||||
|
using Swan.Logging;
|
||||||
|
|
||||||
namespace Example.CLI.PersistentConfig
|
namespace Example.CLI.PersistentConfig
|
||||||
{
|
{
|
||||||
@ -18,15 +18,18 @@ namespace Example.CLI.PersistentConfig
|
|||||||
{
|
{
|
||||||
private const string CredentialsPath = "credentials.json";
|
private const string CredentialsPath = "credentials.json";
|
||||||
private static readonly string? clientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID");
|
private static readonly string? clientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID");
|
||||||
private static readonly string? clientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET");
|
|
||||||
private static readonly EmbedIOAuthServer _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000);
|
private static readonly EmbedIOAuthServer _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000);
|
||||||
|
|
||||||
|
private static void Exiting() => Console.CursorVisible = true;
|
||||||
public static async Task<int> Main()
|
public static async Task<int> Main()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
|
// This is a bug in the SWAN Logging library, need this hack to bring back the cursor
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exiting();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(clientId))
|
||||||
{
|
{
|
||||||
throw new NullReferenceException(
|
throw new NullReferenceException(
|
||||||
"Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET via environment variables before starting the program"
|
"Please set SPOTIFY_CLIENT_ID via environment variables before starting the program"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,9 +49,9 @@ namespace Example.CLI.PersistentConfig
|
|||||||
private static async Task Start()
|
private static async Task Start()
|
||||||
{
|
{
|
||||||
var json = await File.ReadAllTextAsync(CredentialsPath);
|
var json = await File.ReadAllTextAsync(CredentialsPath);
|
||||||
var token = JsonConvert.DeserializeObject<AuthorizationCodeTokenResponse>(json);
|
var token = JsonConvert.DeserializeObject<PKCETokenResponse>(json);
|
||||||
|
|
||||||
var authenticator = new AuthorizationCodeAuthenticator(clientId!, clientSecret!, token);
|
var authenticator = new PKCEAuthenticator(clientId!, token);
|
||||||
authenticator.TokenRefreshed += (sender, token) => File.WriteAllText(CredentialsPath, JsonConvert.SerializeObject(token));
|
authenticator.TokenRefreshed += (sender, token) => File.WriteAllText(CredentialsPath, JsonConvert.SerializeObject(token));
|
||||||
|
|
||||||
var config = SpotifyClientConfig.CreateDefault()
|
var config = SpotifyClientConfig.CreateDefault()
|
||||||
@ -68,12 +71,25 @@ namespace Example.CLI.PersistentConfig
|
|||||||
|
|
||||||
private static async Task StartAuthentication()
|
private static async Task StartAuthentication()
|
||||||
{
|
{
|
||||||
|
var (verifier, challenge) = PKCEUtil.GenerateCodes();
|
||||||
|
|
||||||
await _server.Start();
|
await _server.Start();
|
||||||
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
|
_server.AuthorizationCodeReceived += async (sender, response) =>
|
||||||
|
{
|
||||||
|
await _server.Stop();
|
||||||
|
PKCETokenResponse token = await new OAuthClient().RequestToken(
|
||||||
|
new PKCETokenRequest(clientId!, response.Code, _server.BaseUri, verifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
|
||||||
|
await Start();
|
||||||
|
};
|
||||||
|
|
||||||
var request = new LoginRequest(_server.BaseUri, clientId!, LoginRequest.ResponseType.Code)
|
var request = new LoginRequest(_server.BaseUri, clientId!, LoginRequest.ResponseType.Code)
|
||||||
{
|
{
|
||||||
Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate }
|
CodeChallenge = challenge,
|
||||||
|
CodeChallengeMethod = "S256",
|
||||||
|
Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate, PlaylistReadCollaborative }
|
||||||
};
|
};
|
||||||
|
|
||||||
Uri uri = request.ToUri();
|
Uri uri = request.ToUri();
|
||||||
@ -86,16 +102,5 @@ namespace Example.CLI.PersistentConfig
|
|||||||
Console.WriteLine("Unable to open URL, manually open: {0}", uri);
|
Console.WriteLine("Unable to open URL, manually open: {0}", uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
|
|
||||||
{
|
|
||||||
await _server.Stop();
|
|
||||||
AuthorizationCodeTokenResponse token = await new OAuthClient().RequestToken(
|
|
||||||
new AuthorizationCodeTokenRequest(clientId!, clientSecret!, response.Code, _server.BaseUri)
|
|
||||||
);
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
|
|
||||||
await Start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
65
SpotifyAPI.Web/Authenticators/PKCEAuthenticator.cs
Normal file
65
SpotifyAPI.Web/Authenticators/PKCEAuthenticator.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SpotifyAPI.Web.Http;
|
||||||
|
|
||||||
|
namespace SpotifyAPI.Web
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This Authenticator requests new credentials token on demand and stores them into memory.
|
||||||
|
/// It is unable to query user specifc details.
|
||||||
|
/// </summary>
|
||||||
|
public class PKCEAuthenticator : IAuthenticator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initiate a new instance. The token will be refreshed once it expires.
|
||||||
|
/// The initialToken will be updated with the new values on refresh!
|
||||||
|
/// </summary>
|
||||||
|
public PKCEAuthenticator(string clientId, PKCETokenResponse initialToken)
|
||||||
|
{
|
||||||
|
Ensure.ArgumentNotNull(clientId, nameof(clientId));
|
||||||
|
Ensure.ArgumentNotNull(initialToken, nameof(initialToken));
|
||||||
|
|
||||||
|
InitialToken = initialToken;
|
||||||
|
ClientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This event is called once a new refreshed token was aquired
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<PKCETokenResponse>? TokenRefreshed;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
|
||||||
|
/// </summary>
|
||||||
|
public string ClientId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The inital token passed to the authenticator. Fields will be updated on refresh.
|
||||||
|
/// </summary>
|
||||||
|
/// <value></value>
|
||||||
|
public PKCETokenResponse InitialToken { get; }
|
||||||
|
|
||||||
|
public async Task Apply(IRequest request, IAPIConnector apiConnector)
|
||||||
|
{
|
||||||
|
Ensure.ArgumentNotNull(request, nameof(request));
|
||||||
|
|
||||||
|
if (InitialToken.IsExpired)
|
||||||
|
{
|
||||||
|
var tokenRequest = new PKCETokenRefreshRequest(ClientId, InitialToken.RefreshToken);
|
||||||
|
var refreshedToken = await OAuthClient.RequestToken(tokenRequest, apiConnector).ConfigureAwait(false);
|
||||||
|
|
||||||
|
InitialToken.AccessToken = refreshedToken.AccessToken;
|
||||||
|
InitialToken.CreatedAt = refreshedToken.CreatedAt;
|
||||||
|
InitialToken.ExpiresIn = refreshedToken.ExpiresIn;
|
||||||
|
InitialToken.Scope = refreshedToken.Scope;
|
||||||
|
InitialToken.TokenType = refreshedToken.TokenType;
|
||||||
|
InitialToken.RefreshToken = refreshedToken.RefreshToken;
|
||||||
|
|
||||||
|
TokenRefreshed?.Invoke(this, InitialToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Headers["Authorization"] = $"{InitialToken.TokenType} {InitialToken.AccessToken}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user