Skip to content

Commit 2445945

Browse files
committed
Finished PKCE Docs & Implementation - Example.CLI.PersistentConfig now uses PKCE
1 parent 9d22c04 commit 2445945

File tree

5 files changed

+111
-32
lines changed

5 files changed

+111
-32
lines changed

SpotifyAPI.Docs/docs/auth_introduction.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
77

88
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:
99

10-
* OAuth2
11-
* [Spotify Authorization Flows](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow)
10+
- OAuth2
11+
- [Spotify Authorization Flows](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow)
1212

1313
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`).
1414

1515
Then, continue with the docs of the specific auth flows:
1616

17-
* [Client Credentials](client_credentials.md)
18-
* [Implicit Grant](implicit_grant.md)
19-
* [Authorization Code](authorization_code.md)
20-
* [Token Swap](token_swap.md)
17+
- [Client Credentials](client_credentials.md)
18+
- [Implicit Grant](implicit_grant.md)
19+
- [Authorization Code](authorization_code.md)
20+
- [PKCE](pkce.md)
21+
- [(Token Swap)](token_swap.md)
2122

2223
<img alt="auth comparison" src={useBaseUrl('img/auth_comparison.png')} />

SpotifyAPI.Docs/docs/pkce.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var (verifier, challenge) = PKCEUtil.GenerateCodes();
1616
// Generates a secure random verifier of length 120 and its challenge
1717
var (verifier, challenge) = PKCEUtil.GenerateCodes(120);
1818

19-
// Returns the passed string and its challenge (Make sure it's random and is long enough)
19+
// Returns the passed string and its challenge (Make sure it's random and long enough)
2020
var (verifier, challenge) = PKCEUtil.GenerateCodes("YourSecureRandomString");
2121
```
2222

@@ -37,7 +37,7 @@ var loginRequest = new LoginRequest(
3737
Scope = new[] { Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative }
3838
};
3939
var uri = loginRequest.ToUri();
40-
// Redirect user to uri via your favorite web-server
40+
// Redirect user to uri via your favorite web-server or open a local browser window
4141
```
4242

4343
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,23 +47,31 @@ When the user is redirected to the generated uri, he will have to login with his
4747
public Task GetCallback(string code)
4848
{
4949
// Note that we use the verifier calculated above!
50-
var response = await new OAuthClient().RequestToken(
50+
var initialResponse = await new OAuthClient().RequestToken(
5151
new PKCETokenRequest("ClientId", code, "http://localhost:5000", verifier)
5252
);
5353

54-
var spotify = new SpotifyClient(response.AccessToken);
54+
var spotify = new SpotifyClient(initialResponse.AccessToken);
5555
// Also important for later: response.RefreshToken
5656
}
5757
```
5858

5959
With PKCE you can also refresh tokens once they're expired:
6060

6161
```csharp
62-
var response = await new OAuthClient().RequestToken(
63-
new PKCETokenRefreshRequest("ClientId", oldResponse.RefreshToken)
62+
var newResponse = await new OAuthClient().RequestToken(
63+
new PKCETokenRefreshRequest("ClientId", initialResponse.RefreshToken)
6464
);
6565

66-
var spotify = new SpotifyClient(response.AccessToken);
66+
var spotify = new SpotifyClient(newResponse.AccessToken);
6767
```
6868

69+
If you do not want to take care of manually refreshing tokens, you can use `PKCEAuthenticator`:
6970

71+
```csharp
72+
var authenticator = new PKCEAuthenticator(clientId, initialResponse);
73+
74+
var config = SpotifyClientConfig.CreateDefault()
75+
.WithAuthenticator(authenticator);
76+
var spotify = new SpotifyClient(config);
77+
```
957 Bytes
Loading

SpotifyAPI.Web.Examples/Example.CLI.PersistentConfig/Program.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
using System.Threading.Tasks;
33
using System;
44
using SpotifyAPI.Web.Auth;
5-
using SpotifyAPI.Web.Http;
65
using SpotifyAPI.Web;
76
using System.Collections.Generic;
87
using Newtonsoft.Json;
98
using static SpotifyAPI.Web.Scopes;
9+
using Swan.Logging;
1010

1111
namespace Example.CLI.PersistentConfig
1212
{
@@ -18,15 +18,18 @@ public class Program
1818
{
1919
private const string CredentialsPath = "credentials.json";
2020
private static readonly string? clientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID");
21-
private static readonly string? clientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET");
2221
private static readonly EmbedIOAuthServer _server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000);
2322

23+
private static void Exiting() => Console.CursorVisible = true;
2424
public static async Task<int> Main()
2525
{
26-
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
26+
// This is a bug in the SWAN Logging library, need this hack to bring back the cursor
27+
AppDomain.CurrentDomain.ProcessExit += (sender, e) => Exiting();
28+
29+
if (string.IsNullOrEmpty(clientId))
2730
{
2831
throw new NullReferenceException(
29-
"Please set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET via environment variables before starting the program"
32+
"Please set SPOTIFY_CLIENT_ID via environment variables before starting the program"
3033
);
3134
}
3235

@@ -46,9 +49,9 @@ public static async Task<int> Main()
4649
private static async Task Start()
4750
{
4851
var json = await File.ReadAllTextAsync(CredentialsPath);
49-
var token = JsonConvert.DeserializeObject<AuthorizationCodeTokenResponse>(json);
52+
var token = JsonConvert.DeserializeObject<PKCETokenResponse>(json);
5053

51-
var authenticator = new AuthorizationCodeAuthenticator(clientId!, clientSecret!, token);
54+
var authenticator = new PKCEAuthenticator(clientId!, token);
5255
authenticator.TokenRefreshed += (sender, token) => File.WriteAllText(CredentialsPath, JsonConvert.SerializeObject(token));
5356

5457
var config = SpotifyClientConfig.CreateDefault()
@@ -68,12 +71,25 @@ private static async Task Start()
6871

6972
private static async Task StartAuthentication()
7073
{
74+
var (verifier, challenge) = PKCEUtil.GenerateCodes();
75+
7176
await _server.Start();
72-
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
77+
_server.AuthorizationCodeReceived += async (sender, response) =>
78+
{
79+
await _server.Stop();
80+
PKCETokenResponse token = await new OAuthClient().RequestToken(
81+
new PKCETokenRequest(clientId!, response.Code, _server.BaseUri, verifier)
82+
);
83+
84+
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
85+
await Start();
86+
};
7387

7488
var request = new LoginRequest(_server.BaseUri, clientId!, LoginRequest.ResponseType.Code)
7589
{
76-
Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate }
90+
CodeChallenge = challenge,
91+
CodeChallengeMethod = "S256",
92+
Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate, PlaylistReadCollaborative }
7793
};
7894

7995
Uri uri = request.ToUri();
@@ -86,16 +102,5 @@ private static async Task StartAuthentication()
86102
Console.WriteLine("Unable to open URL, manually open: {0}", uri);
87103
}
88104
}
89-
90-
private static async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
91-
{
92-
await _server.Stop();
93-
AuthorizationCodeTokenResponse token = await new OAuthClient().RequestToken(
94-
new AuthorizationCodeTokenRequest(clientId!, clientSecret!, response.Code, _server.BaseUri)
95-
);
96-
97-
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
98-
await Start();
99-
}
100105
}
101106
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using SpotifyAPI.Web.Http;
4+
5+
namespace SpotifyAPI.Web
6+
{
7+
/// <summary>
8+
/// This Authenticator requests new credentials token on demand and stores them into memory.
9+
/// It is unable to query user specifc details.
10+
/// </summary>
11+
public class PKCEAuthenticator : IAuthenticator
12+
{
13+
/// <summary>
14+
/// Initiate a new instance. The token will be refreshed once it expires.
15+
/// The initialToken will be updated with the new values on refresh!
16+
/// </summary>
17+
public PKCEAuthenticator(string clientId, PKCETokenResponse initialToken)
18+
{
19+
Ensure.ArgumentNotNull(clientId, nameof(clientId));
20+
Ensure.ArgumentNotNull(initialToken, nameof(initialToken));
21+
22+
InitialToken = initialToken;
23+
ClientId = clientId;
24+
}
25+
26+
/// <summary>
27+
/// This event is called once a new refreshed token was aquired
28+
/// </summary>
29+
public event EventHandler<PKCETokenResponse>? TokenRefreshed;
30+
31+
32+
/// <summary>
33+
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
34+
/// </summary>
35+
public string ClientId { get; }
36+
37+
/// <summary>
38+
/// The inital token passed to the authenticator. Fields will be updated on refresh.
39+
/// </summary>
40+
/// <value></value>
41+
public PKCETokenResponse InitialToken { get; }
42+
43+
public async Task Apply(IRequest request, IAPIConnector apiConnector)
44+
{
45+
Ensure.ArgumentNotNull(request, nameof(request));
46+
47+
if (InitialToken.IsExpired)
48+
{
49+
var tokenRequest = new PKCETokenRefreshRequest(ClientId, InitialToken.RefreshToken);
50+
var refreshedToken = await OAuthClient.RequestToken(tokenRequest, apiConnector).ConfigureAwait(false);
51+
52+
InitialToken.AccessToken = refreshedToken.AccessToken;
53+
InitialToken.CreatedAt = refreshedToken.CreatedAt;
54+
InitialToken.ExpiresIn = refreshedToken.ExpiresIn;
55+
InitialToken.Scope = refreshedToken.Scope;
56+
InitialToken.TokenType = refreshedToken.TokenType;
57+
InitialToken.RefreshToken = refreshedToken.RefreshToken;
58+
59+
TokenRefreshed?.Invoke(this, InitialToken);
60+
}
61+
62+
request.Headers["Authorization"] = $"{InitialToken.TokenType} {InitialToken.AccessToken}";
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)