mirror of
https://github.com/Sarsoo/Spotify.NET.git
synced 2024-12-23 14:46:26 +00:00
Secure, user-friendly and feature-rich authorization method (#286)
* Add new auth method and api factory for the auth method * Add process documentation * Solve random crash issues related to token collection * Add customizable html responses, add more process control * Add show dialog support, fix nulled html response * Fix false trigger spam of access expiry event * Add auto-retry GetToken, add fire auth fail on timeout * Improve auto-refresh and refresh auth token validity checks * Add ability to change number of get token retries * Add get web API request cancelling * Solve compiler warning * Remove comment links, rename secure auth for clarity * Rename secure auth for clarity in the docs * Improve token swap usage example in docs * Abstract the exchange server doc info for clarity * Fix simontaen SpotifyTokenSwap link * Change access expiry timing to be skippable * Adapt TokenSwap for new convention, separate autorefresh and timer options
This commit is contained in:
parent
278927f704
commit
ea70fbc77b
@ -12,13 +12,15 @@ After you created your Application, you will have following important values:
|
||||
>**Client_Secret**: Never use this in one of your client-side apps!! Keep it secret!
|
||||
>**Redirect URIs**: Add "http://localhost", if you want full support for this API
|
||||
|
||||
Now you can start with the User-authentication, Spotify provides 3 ways:
|
||||
Now you can start with the user-authentication, Spotify provides 3 ways (4 if you consider different implementations):
|
||||
|
||||
* [ImplicitGrantAuth](/SpotifyWebAPI/auth#implicitgrantauth)
|
||||
|
||||
* [AutorizationCodeAuth](/SpotifyWebAPI/auth#autorizationcodeauth)
|
||||
* [TokenSwapAuth](/SpotifyWebAPI/auth#tokenswapauth) (**Recommended**, server-side code mandatory, most secure method. The necessary code is shown here so you do not have to code it yourself.)
|
||||
|
||||
* [ClientCredentialsAuth](/SpotifyWebAPI/auth#clientcredentialsauth)
|
||||
* [AutorizationCodeAuth](/SpotifyWebAPI/auth#autorizationcodeauth) (Not recommended, server-side code needed, else it's unsecure)
|
||||
|
||||
* [ClientCredentialsAuth](/SpotifyWebAPI/auth#clientcredentialsauth) (Not recommended, server-side code needed, else it's unsecure)
|
||||
|
||||
## Notes
|
||||
|
||||
@ -29,10 +31,10 @@ Overview:
|
||||
|
||||
After implementing one of the provided auth-methods, you can start doing requests with the token you get from one of the auth-methods.
|
||||
|
||||
##ImplicitGrantAuth
|
||||
## ImplicitGrantAuth
|
||||
|
||||
With this approach, you directly get a Token object after the user authed your application.
|
||||
You won't be able to refresh the token. If you want to use the internal Http server, make sure the redirect URI is in your spotify application redirects.
|
||||
This way is **recommended** and the only auth-process which does not need a server-side exchange of keys. With this approach, you directly get a Token object after the user authed your application.
|
||||
You won't be able to refresh the token. If you want to use the internal Http server, please add "http://localhost" to your application redirects.
|
||||
|
||||
More info: [here](https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow)
|
||||
|
||||
@ -52,7 +54,115 @@ static async void Main(string[] args)
|
||||
}
|
||||
```
|
||||
|
||||
##AutorizationCodeAuth
|
||||
## TokenSwapAuth
|
||||
|
||||
This way uses server-side code or at least access to an exchange server, otherwise, compared to other
|
||||
methods, it is impossible to use.
|
||||
|
||||
With this approach, you provide the URI/URL to your desired exchange server to perform all necessary
|
||||
requests to Spotify, as well as requests that return back to the "server URI".
|
||||
|
||||
The exchange server **must** be able to:
|
||||
|
||||
* Return the authorization code from Spotify API authenticate page via GET request to the "server URI".
|
||||
* Request the token response object via POST to the Spotify API token page.
|
||||
* Request a refreshed token response object via POST to the Spotify API token page.
|
||||
|
||||
**The good news is that you do not need to code it yourself.**
|
||||
|
||||
The advantages of this method are that the client ID and redirect URI are very well hidden and almost unexposed, but more importantly, your client secret is **never** exposed and is completely hidden compared to other methods (excluding [ImplicitGrantAuth](/SpotifyWebAPI/auth#implicitgrantauth)
|
||||
as it does not deal with a client secret). This means
|
||||
your Spotify app **cannot** be spoofed by a malicious third party.
|
||||
|
||||
### Using TokenSwapWebAPIFactory
|
||||
The TokenSwapWebAPIFactory will create and configure a SpotifyWebAPI object for you.
|
||||
|
||||
It does this through the method GetWebApiAsync **asynchronously**, which means it will not halt execution of your program while obtaining it for you. If you would like to halt execution, which is **synchronous**, use `GetWebApiAsync().Result` without using **await**.
|
||||
|
||||
```c#
|
||||
TokenSwapWebAPIFactory webApiFactory;
|
||||
SpotifyWebAPI spotify;
|
||||
|
||||
// You should store a reference to WebAPIFactory if you are using AutoRefresh or want to manually refresh it later on. New WebAPIFactory objects cannot refresh SpotifyWebAPI object that they did not give to you.
|
||||
webApiFactory = new TokenSwapWebAPIFactory("INSERT LINK TO YOUR index.php HERE")
|
||||
{
|
||||
Scope = Scope.UserReadPrivate | Scope.UserReadEmail | Scope.PlaylistReadPrivate | Scope.UserLibraryRead | Scope.UserReadPrivate | Scope.UserFollowRead | Scope.UserReadBirthdate | Scope.UserTopRead | Scope.PlaylistReadCollaborative | Scope.UserReadRecentlyPlayed | Scope.UserReadPlaybackState | Scope.UserModifyPlaybackState | Scope.PlaylistModifyPublic,
|
||||
AutoRefresh = true
|
||||
};
|
||||
// You may want to react to being able to use the Spotify service.
|
||||
// webApiFactory.OnAuthSuccess += (sender, e) => authorized = true;
|
||||
// You may want to react to your user's access expiring.
|
||||
// webApiFactory.OnAccessTokenExpired += (sender, e) => authorized = false;
|
||||
|
||||
try
|
||||
{
|
||||
spotify = await webApiFactory.GetWebApiAsync();
|
||||
// Synchronous way:
|
||||
// spotify = webApiFactory.GetWebApiAsync().Result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Example way to handle error reporting gracefully with your SpotifyWebAPI wrapper
|
||||
// UpdateStatus($"Spotify failed to load: {ex.Message}");
|
||||
}
|
||||
```
|
||||
|
||||
### Using TokenSwapAuth
|
||||
Since the TokenSwapWebAPIFactory not only simplifies the whole process but offers additional functionality too
|
||||
(such as AutoRefresh and AuthSuccess AuthFailure events), use of this way is very verbose and is only
|
||||
recommended if you are having issues with TokenSwapWebAPIFactory or need access to the tokens.
|
||||
|
||||
```c#
|
||||
TokenSwapAuth auth = new TokenSwapAuth(
|
||||
exchangeServerUri: "INSERT LINK TO YOUR index.php HERE",
|
||||
serverUri: "http://localhost:4002",
|
||||
scope: Scope.UserReadPrivate | Scope.UserReadEmail | Scope.PlaylistReadPrivate | Scope.UserLibraryRead | Scope.UserReadPrivate | Scope.UserFollowRead | Scope.UserReadBirthdate | Scope.UserTopRead | Scope.PlaylistReadCollaborative | Scope.UserReadRecentlyPlayed | Scope.UserReadPlaybackState | Scope.UserModifyPlaybackState | Scope.PlaylistModifyPublic
|
||||
);
|
||||
auth.AuthReceived += async (sender, response) =>
|
||||
{
|
||||
lastToken = await auth.ExchangeCodeAsync(response.Code);
|
||||
|
||||
spotify = new SpotifyWebAPI()
|
||||
{
|
||||
TokenType = lastToken.TokenType,
|
||||
AccessToken = lastToken.AccessToken
|
||||
};
|
||||
|
||||
authenticated = true;
|
||||
auth.Stop();
|
||||
};
|
||||
auth.OnAccessTokenExpired += async (sender, e) => spotify.AccessToken = (await auth.RefreshAuthAsync(lastToken.RefreshToken)).AccessToken;
|
||||
auth.Start();
|
||||
auth.OpenBrowser();
|
||||
```
|
||||
|
||||
### Token Swap Endpoint
|
||||
To keep your client secret completely secure and your client ID and redirect URI as secure as possible, use of a web server (such as a php website) is required.
|
||||
|
||||
To use this method, an external HTTP Server (that you may need to create) needs to be able to supply the following HTTP Endpoints to your application:
|
||||
|
||||
`/swap` - Swaps out an `authorization_code` with an `access_token` and `refresh_token` - The following parameters are required in the JSON POST Body:
|
||||
- `grant_type` (set to `"authorization_code"`)
|
||||
- `code` (the `authorization_code`)
|
||||
- `redirect_uri`
|
||||
- - **Important** The page that the redirect URI links to must return the authorization code json to your `serverUri` (default is 'http://localhost:4002') but to the folder 'auth', like this: 'http://localhost:4002/auth'.
|
||||
|
||||
`/refresh` - Refreshes an `access_token` - The following parameters are required in the JSON POST Body:
|
||||
- `grant_type` (set to `"refresh_token"`)
|
||||
- `refresh_token`
|
||||
|
||||
The following open-source token swap endpoint code can be used for your website:
|
||||
- [rollersteaam/spotify-token-swap-php](https://github.com/rollersteaam/spotify-token-swap-php)
|
||||
- [simontaen/SpotifyTokenSwap](https://github.com/simontaen/SpotifyTokenSwap)
|
||||
|
||||
#### Remarks
|
||||
It should be noted that GitHub Pages does not support hosting php scripts. Hosting php scripts through it will cause the php to render as plain HTML, potentially compromising your client secret while doing absolutely nothing.
|
||||
|
||||
Be sure you have whitelisted your redirect uri in the Spotify Developer Dashboard otherwise the authorization will always fail.
|
||||
|
||||
If you did not use the WebAPIFactory or you provided a `serverUri` different from its default, you must make sure your redirect uri's script at your endpoint will properly redirect to your `serverUri` (such as changing the areas which refer to `localhost:4002` if you had changed `serverUri` from its default), otherwise it will never reach your new `serverUri`.
|
||||
|
||||
## AutorizationCodeAuth
|
||||
|
||||
This way is **not recommended** and requires server-side code to run securely.
|
||||
With this approach, you first get a code which you need to trade against the access-token.
|
||||
@ -79,7 +189,7 @@ static async void Main(string[] args)
|
||||
}
|
||||
```
|
||||
|
||||
##ClientCredentialsAuth
|
||||
## ClientCredentialsAuth
|
||||
|
||||
With this approach, you make a POST Request with a base64 encoded string (consists of ClientId + ClientSecret). You will directly get the token (Without a local HTTP Server), but it will expire and can't be refreshed.
|
||||
If you want to use it securely, you would need to do it all server-side.
|
||||
|
217
SpotifyAPI.Web.Auth/TokenSwapAuth.cs
Normal file
217
SpotifyAPI.Web.Auth/TokenSwapAuth.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using Unosquare.Labs.EmbedIO;
|
||||
using Unosquare.Labs.EmbedIO.Constants;
|
||||
using Unosquare.Labs.EmbedIO.Modules;
|
||||
using SpotifyAPI.Web.Models;
|
||||
using Newtonsoft.Json;
|
||||
#if NETSTANDARD2_0
|
||||
using System.Net.Http;
|
||||
#endif
|
||||
#if NET46
|
||||
using System.Net.Http;
|
||||
using HttpListenerContext = Unosquare.Net.HttpListenerContext;
|
||||
#endif
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A version of <see cref="AuthorizationCodeAuth"/> that does not store your client secret, client ID or redirect URI, enforcing a secure authorization flow. Requires an exchange server that will return the authorization code to its callback server via GET request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It's recommended that you use <see cref="TokenSwapWebAPIFactory"/> if you would like to use the TokenSwap method.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class TokenSwapAuth : SpotifyAuthServer<AuthorizationCode>
|
||||
{
|
||||
string exchangeServerUri;
|
||||
|
||||
/// <summary>
|
||||
/// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival.
|
||||
/// </summary>
|
||||
public string HtmlResponse { get; set; } = "<script>window.close();</script>";
|
||||
/// <summary>
|
||||
/// If true, will time how long it takes for access to expire. On expiry, the <see cref="OnAccessTokenExpired"/> event fires.
|
||||
/// </summary>
|
||||
public bool TimeAccessExpiry { get; set; }
|
||||
|
||||
/// <param name="exchangeServerUri">The URI to an exchange server that will perform the key exchange.</param>
|
||||
/// <param name="serverUri">The URI to host the server at that your exchange server should return the authorization code to by GET request. (e.g. http://localhost:4002)</param>
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="state">Stating none will randomly generate a state parameter.</param>
|
||||
/// <param name="htmlResponse">The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival.</param>
|
||||
public TokenSwapAuth(string exchangeServerUri, string serverUri, Scope scope = Scope.None, string state = "", string htmlResponse = "") : base("code", "", "", serverUri, scope, state)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(htmlResponse))
|
||||
{
|
||||
HtmlResponse = htmlResponse;
|
||||
}
|
||||
|
||||
this.exchangeServerUri = exchangeServerUri;
|
||||
}
|
||||
|
||||
protected override void AdaptWebServer(WebServer webServer)
|
||||
{
|
||||
webServer.Module<WebApiModule>().RegisterController<TokenSwapAuthController>();
|
||||
}
|
||||
|
||||
public override string GetUri()
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(exchangeServerUri);
|
||||
builder.Append("?");
|
||||
builder.Append("response_type=code");
|
||||
builder.Append("&state=" + State);
|
||||
builder.Append("&scope=" + Scope.GetStringAttribute(" "));
|
||||
builder.Append("&show_dialog=" + ShowDialog);
|
||||
return Uri.EscapeUriString(builder.ToString());
|
||||
}
|
||||
|
||||
static readonly HttpClient httpClient = new HttpClient();
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of times to retry getting a token.
|
||||
/// <para/>
|
||||
/// A token get is attempted every time you <see cref="RefreshAuthAsync(string)"/> and <see cref="ExchangeCodeAsync(string)"/>.
|
||||
/// </summary>
|
||||
public int MaxGetTokenRetries { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a HTTP request to obtain a token object.<para/>
|
||||
/// Parameter grantType can only be "refresh_token" or "authorization_code". authorizationCode and refreshToken are not mandatory, but at least one must be provided for your desired grant_type request otherwise an invalid response will be given and an exception is likely to be thrown.
|
||||
/// <para>
|
||||
/// Will re-attempt on error, on null or on no access token <see cref="MaxGetTokenRetries"/> times before finally returning null.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="grantType">Can only be "refresh_token" or "authorization_code".</param>
|
||||
/// <param name="authorizationCode">This needs to be defined if "grantType" is "authorization_code".</param>
|
||||
/// <param name="refreshToken">This needs to be defined if "grantType" is "refresh_token".</param>
|
||||
/// <param name="currentRetries">Does not need to be defined. Used internally for retry attempt recursion.</param>
|
||||
/// <returns>Attempts to return a full <see cref="Token"/>, but after retry attempts, may return a <see cref="Token"/> with no <see cref="Token.AccessToken"/>, or null.</returns>
|
||||
async Task<Token> GetToken(string grantType, string authorizationCode = "", string refreshToken = "", int currentRetries = 0)
|
||||
{
|
||||
var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", grantType },
|
||||
{ "code", authorizationCode },
|
||||
{ "refresh_token", refreshToken }
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var siteResponse = await httpClient.PostAsync(exchangeServerUri, content);
|
||||
Token token = JsonConvert.DeserializeObject<Token>(await siteResponse.Content.ReadAsStringAsync());
|
||||
// Don't need to check if it was null - if it is, it will resort to the catch block.
|
||||
if (!token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
return token;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (currentRetries >= MaxGetTokenRetries)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentRetries++;
|
||||
// The reason I chose to implement the retries system this way is because a static or instance
|
||||
// variable keeping track would inhibit parallelism i.e. using this function on multiple threads/tasks.
|
||||
// It's not clear why someone would like to do that, but it's better to cater for all kinds of uses.
|
||||
return await GetToken(grantType, authorizationCode, refreshToken, currentRetries);
|
||||
}
|
||||
}
|
||||
|
||||
System.Timers.Timer accessTokenExpireTimer;
|
||||
/// <summary>
|
||||
/// When Spotify authorization has expired. Will only trigger if <see cref="TimeAccessExpiry"/> is true.
|
||||
/// </summary>
|
||||
public event EventHandler OnAccessTokenExpired;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="TimeAccessExpiry"/> is true, sets a timer for how long access will take to expire.
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
void SetAccessExpireTimer(Token token)
|
||||
{
|
||||
if (!TimeAccessExpiry) return;
|
||||
|
||||
if (accessTokenExpireTimer != null)
|
||||
{
|
||||
accessTokenExpireTimer.Stop();
|
||||
accessTokenExpireTimer.Dispose();
|
||||
}
|
||||
|
||||
accessTokenExpireTimer = new System.Timers.Timer
|
||||
{
|
||||
Enabled = true,
|
||||
Interval = token.ExpiresIn * 1000,
|
||||
AutoReset = false
|
||||
};
|
||||
accessTokenExpireTimer.Elapsed += (sender, e) => OnAccessTokenExpired?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the authorization code to silently (doesn't open a browser) obtain both an access token and refresh token, where the refresh token would be required for you to use <see cref="RefreshAuthAsync(string)"/>.
|
||||
/// </summary>
|
||||
/// <param name="authorizationCode"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Token> ExchangeCodeAsync(string authorizationCode)
|
||||
{
|
||||
Token token = await GetToken("authorization_code", authorizationCode: authorizationCode);
|
||||
if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
SetAccessExpireTimer(token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the refresh token to silently (doesn't open a browser) obtain a fresh access token, no refresh token is given however (as it does not change).
|
||||
/// </summary>
|
||||
/// <param name="refreshToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Token> RefreshAuthAsync(string refreshToken)
|
||||
{
|
||||
Token token = await GetToken("refresh_token", refreshToken: refreshToken);
|
||||
if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
SetAccessExpireTimer(token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TokenSwapAuthController : WebApiController
|
||||
{
|
||||
public TokenSwapAuthController(IHttpContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
[WebApiHandler(HttpVerbs.Get, "/auth")]
|
||||
public Task<bool> GetAuth()
|
||||
{
|
||||
string state = Request.QueryString["state"];
|
||||
SpotifyAuthServer<AuthorizationCode> auth = TokenSwapAuth.GetByState(state);
|
||||
|
||||
string code = null;
|
||||
string error = Request.QueryString["error"];
|
||||
if (error == null)
|
||||
{
|
||||
code = Request.QueryString["code"];
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(() => auth?.TriggerAuth(new AuthorizationCode
|
||||
{
|
||||
Code = code,
|
||||
Error = error
|
||||
}));
|
||||
return this.StringResponseAsync(((TokenSwapAuth)auth).HtmlResponse);
|
||||
}
|
||||
}
|
||||
}
|
282
SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs
Normal file
282
SpotifyAPI.Web.Auth/TokenSwapWebAPIFactory.cs
Normal file
@ -0,0 +1,282 @@
|
||||
using SpotifyAPI.Web.Enums;
|
||||
using SpotifyAPI.Web.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SpotifyAPI.Web.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a <see cref="SpotifyWebAPI"/> using the TokenSwapAuth process.
|
||||
/// </summary>
|
||||
public class TokenSwapWebAPIFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Access provided by Spotify expires after 1 hour. If true, <see cref="TokenSwapAuth"/> will time the access tokens, and access will attempt to be silently (without opening a browser) refreshed automatically. This will not make <see cref="OnAccessTokenExpired"/> fire, see <see cref="TimeAccessExpiry"/> for that.
|
||||
/// </summary>
|
||||
public bool AutoRefresh { get; set; }
|
||||
/// <summary>
|
||||
/// If true when calling <see cref="GetWebApiAsync"/>, will time how long it takes for access to Spotify to expire. The event <see cref="OnAccessTokenExpired"/> fires when the timer elapses.
|
||||
/// </summary>
|
||||
public bool TimeAccessExpiry { get; set; }
|
||||
/// <summary>
|
||||
/// The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.
|
||||
/// </summary>
|
||||
public int Timeout { get; set; }
|
||||
public Scope Scope { get; set; }
|
||||
/// <summary>
|
||||
/// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.
|
||||
/// </summary>
|
||||
public string ExchangeServerUri { get; set; }
|
||||
/// <summary>
|
||||
/// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002
|
||||
/// </summary>
|
||||
public string HostServerUri { get; set; }
|
||||
/// <summary>
|
||||
/// Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.
|
||||
/// </summary>
|
||||
public bool OpenBrowser { get; set; }
|
||||
/// <summary>
|
||||
/// The HTML to respond with when the callback server has been reached. By default, it is set to close the window on arrival.
|
||||
/// </summary>
|
||||
public string HtmlResponse { get; set; }
|
||||
/// <summary>
|
||||
/// Whether or not to show a dialog saying "Is this you?" during the initial key exchange. It should be noted that this would allow a user the opportunity to change accounts.
|
||||
/// </summary>
|
||||
public bool ShowDialog { get; set; }
|
||||
/// <summary>
|
||||
/// The maximum amount of times to retry getting a token.
|
||||
/// <para/>
|
||||
/// A token get is attempted every time you <see cref="GetWebApiAsync"/> and <see cref="RefreshAuthAsync"/>. Increasing this may improve how often these actions succeed - although it won't solve any underlying problems causing a get token failure.
|
||||
/// </summary>
|
||||
public int MaxGetTokenRetries { get; set; } = 10;
|
||||
/// <summary>
|
||||
/// Returns a SpotifyWebAPI using the TokenSwapAuth process.
|
||||
/// </summary>
|
||||
/// <param name="exchangeServerUri">The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens.</param>
|
||||
/// <param name="scope"></param>
|
||||
/// <param name="hostServerUri">The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002</param>
|
||||
/// <param name="timeout">The maximum time in seconds to wait for a SpotifyWebAPI to be returned. The timeout is cancelled early regardless if an auth success or failure occured.</param>
|
||||
/// <param name="autoRefresh">Access provided by Spotify expires after 1 hour. If true, access will attempt to be silently (without opening a browser) refreshed automatically.</param>
|
||||
/// <param name="openBrowser">Opens the user's browser and visits the exchange server for you, triggering the key exchange. This should be true unless you want to handle the key exchange in a nicer way.</param>
|
||||
public TokenSwapWebAPIFactory(string exchangeServerUri, Scope scope = Scope.None, string hostServerUri = "http://localhost:4002", int timeout = 10, bool autoRefresh = false, bool openBrowser = true)
|
||||
{
|
||||
AutoRefresh = autoRefresh;
|
||||
Timeout = timeout;
|
||||
Scope = scope;
|
||||
ExchangeServerUri = exchangeServerUri;
|
||||
HostServerUri = hostServerUri;
|
||||
OpenBrowser = openBrowser;
|
||||
|
||||
OnAccessTokenExpired += async (sender, e) =>
|
||||
{
|
||||
if (AutoRefresh)
|
||||
{
|
||||
await RefreshAuthAsync();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Token lastToken;
|
||||
SpotifyWebAPI lastWebApi;
|
||||
TokenSwapAuth lastAuth;
|
||||
|
||||
public class ExchangeReadyEventArgs : EventArgs
|
||||
{
|
||||
public string ExchangeUri { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// When the URI to get an authorization code is ready to be used to be visited. Not required if <see cref="OpenBrowser"/> is true as the exchange URI will automatically be visited for you.
|
||||
/// </summary>
|
||||
public event EventHandler<ExchangeReadyEventArgs> OnExchangeReady;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the access for a SpotifyWebAPI returned by this factory.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task RefreshAuthAsync()
|
||||
{
|
||||
Token token = await lastAuth.RefreshAuthAsync(lastToken.RefreshToken);
|
||||
|
||||
if (token == null)
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"Token not returned by server."));
|
||||
}
|
||||
else if (token.HasError())
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs($"{token.Error} {token.ErrorDescription}"));
|
||||
}
|
||||
else if (string.IsNullOrEmpty(token.AccessToken))
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Token had no access token attached."));
|
||||
}
|
||||
else
|
||||
{
|
||||
lastWebApi.AccessToken = token.AccessToken;
|
||||
OnAuthSuccess?.Invoke(this, new AuthSuccessEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
// By defining empty EventArgs objects, you can specify additional information later on as you see fit and it won't
|
||||
// be considered a breaking change to consumers of this API.
|
||||
//
|
||||
// They don't even need to be constructed for their associated events to be invoked - just pass the static Empty property.
|
||||
public class AccessTokenExpiredEventArgs : EventArgs
|
||||
{
|
||||
public static new AccessTokenExpiredEventArgs Empty { get; } = new AccessTokenExpiredEventArgs();
|
||||
|
||||
public AccessTokenExpiredEventArgs()
|
||||
{
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// When the authorization from Spotify expires. This will only occur if <see cref="AutoRefresh"/> is true.
|
||||
/// </summary>
|
||||
public event EventHandler<AccessTokenExpiredEventArgs> OnAccessTokenExpired;
|
||||
|
||||
public class AuthSuccessEventArgs : EventArgs
|
||||
{
|
||||
public static new AuthSuccessEventArgs Empty { get; } = new AuthSuccessEventArgs();
|
||||
|
||||
public AuthSuccessEventArgs()
|
||||
{
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// When an authorization attempt succeeds and gains authorization.
|
||||
/// </summary>
|
||||
public event EventHandler<AuthSuccessEventArgs> OnAuthSuccess;
|
||||
|
||||
public class AuthFailureEventArgs : EventArgs
|
||||
{
|
||||
public static new AuthFailureEventArgs Empty { get; } = new AuthFailureEventArgs("");
|
||||
|
||||
public string Error { get; }
|
||||
|
||||
public AuthFailureEventArgs(string error)
|
||||
{
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// When an authorization attempt fails to gain authorization.
|
||||
/// </summary>
|
||||
public event EventHandler<AuthFailureEventArgs> OnAuthFailure;
|
||||
|
||||
/// <summary>
|
||||
/// Manually triggers the timeout for any ongoing get web API request.
|
||||
/// </summary>
|
||||
public void CancelGetWebApiRequest()
|
||||
{
|
||||
if (webApiTimeoutTimer == null) return;
|
||||
|
||||
// The while loop in GetWebApiSync() will react and trigger the timeout.
|
||||
webApiTimeoutTimer.Stop();
|
||||
webApiTimeoutTimer.Dispose();
|
||||
webApiTimeoutTimer = null;
|
||||
}
|
||||
|
||||
System.Timers.Timer webApiTimeoutTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an authorized and ready to use SpotifyWebAPI by following the SecureAuthorizationCodeAuth process with its current settings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<SpotifyWebAPI> GetWebApiAsync()
|
||||
{
|
||||
return await Task<SpotifyWebAPI>.Factory.StartNew(() =>
|
||||
{
|
||||
bool currentlyAuthorizing = true;
|
||||
|
||||
// Cancel any ongoing get web API requests
|
||||
CancelGetWebApiRequest();
|
||||
|
||||
lastAuth = new TokenSwapAuth(
|
||||
exchangeServerUri: ExchangeServerUri,
|
||||
serverUri: HostServerUri,
|
||||
scope: Scope,
|
||||
htmlResponse: HtmlResponse)
|
||||
{
|
||||
ShowDialog = ShowDialog,
|
||||
MaxGetTokenRetries = MaxGetTokenRetries,
|
||||
TimeAccessExpiry = AutoRefresh || TimeAccessExpiry
|
||||
};
|
||||
lastAuth.AuthReceived += async (sender, response) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(response.Error) || string.IsNullOrEmpty(response.Code))
|
||||
{
|
||||
// We only want one auth failure to be fired, if the request timed out then don't bother.
|
||||
if (!webApiTimeoutTimer.Enabled) return;
|
||||
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs(response.Error));
|
||||
currentlyAuthorizing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
lastToken = await lastAuth.ExchangeCodeAsync(response.Code);
|
||||
|
||||
if (lastToken == null || lastToken.HasError() || string.IsNullOrEmpty(lastToken.AccessToken))
|
||||
{
|
||||
// We only want one auth failure to be fired, if the request timed out then don't bother.
|
||||
if (!webApiTimeoutTimer.Enabled) return;
|
||||
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Exchange token not returned by server."));
|
||||
currentlyAuthorizing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWebApi != null)
|
||||
{
|
||||
lastWebApi.Dispose();
|
||||
}
|
||||
lastWebApi = new SpotifyWebAPI()
|
||||
{
|
||||
TokenType = lastToken.TokenType,
|
||||
AccessToken = lastToken.AccessToken
|
||||
};
|
||||
|
||||
lastAuth.Stop();
|
||||
|
||||
OnAuthSuccess?.Invoke(this, AuthSuccessEventArgs.Empty);
|
||||
currentlyAuthorizing = false;
|
||||
};
|
||||
lastAuth.OnAccessTokenExpired += async (sender, e) =>
|
||||
{
|
||||
if (TimeAccessExpiry)
|
||||
{
|
||||
OnAccessTokenExpired?.Invoke(sender, AccessTokenExpiredEventArgs.Empty);
|
||||
}
|
||||
|
||||
if (AutoRefresh)
|
||||
{
|
||||
await RefreshAuthAsync();
|
||||
}
|
||||
};
|
||||
lastAuth.Start();
|
||||
OnExchangeReady?.Invoke(this, new ExchangeReadyEventArgs { ExchangeUri = lastAuth.GetUri() });
|
||||
if (OpenBrowser)
|
||||
{
|
||||
lastAuth.OpenBrowser();
|
||||
}
|
||||
|
||||
webApiTimeoutTimer = new System.Timers.Timer
|
||||
{
|
||||
AutoReset = false,
|
||||
Enabled = true,
|
||||
Interval = Timeout * 1000
|
||||
};
|
||||
|
||||
while (currentlyAuthorizing && webApiTimeoutTimer.Enabled) ;
|
||||
|
||||
// If a timeout occurred
|
||||
if (lastWebApi == null && currentlyAuthorizing)
|
||||
{
|
||||
OnAuthFailure?.Invoke(this, new AuthFailureEventArgs("Authorization request has timed out."));
|
||||
}
|
||||
|
||||
return lastWebApi;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user