using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using SpotifyAPI.Web.Enums; using SpotifyAPI.Web.Models; namespace SpotifyAPI.Web.Auth { /// /// Returns a using the TokenSwapAuth process. /// public class TokenSwapWebAPIFactory { /// /// Access provided by Spotify expires after 1 hour. If true, will time the access tokens, and access will attempt to be silently (without opening a browser) refreshed automatically. This will not make fire, see for that. /// public bool AutoRefresh { get; set; } /// /// If true when calling , will time how long it takes for access to Spotify to expire. The event fires when the timer elapses. /// public bool TimeAccessExpiry { get; set; } /// /// 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. /// public int Timeout { get; set; } public Scope Scope { get; set; } /// /// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens. /// public string ExchangeServerUri { get; set; } /// /// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002 /// public string HostServerUri { get; set; } /// /// 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. /// public bool OpenBrowser { get; set; } /// /// The HTML to respond with when the callback server has been reached. By default, it is set to close the window on arrival. /// public string HtmlResponse { get; set; } /// /// 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. /// public bool ShowDialog { get; set; } /// /// The maximum amount of times to retry getting a token. /// /// A token get is attempted every time you and . Increasing this may improve how often these actions succeed - although it won't solve any underlying problems causing a get token failure. /// public int MaxGetTokenRetries { get; set; } = 10; /// /// Returns a SpotifyWebAPI using the TokenSwapAuth process. /// /// The URI (or URL) of the exchange server which exchanges the auth code for access and refresh tokens. /// /// The URI (or URL) of where a callback server to receive the auth code will be hosted. e.g. http://localhost:4002 /// 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. /// Access provided by Spotify expires after 1 hour. If true, access will attempt to be silently (without opening a browser) refreshed automatically. /// 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. 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; } } /// /// When the URI to get an authorization code is ready to be used to be visited. Not required if is true as the exchange URI will automatically be visited for you. /// public event EventHandler OnExchangeReady; /// /// Refreshes the access for a SpotifyWebAPI returned by this factory. /// /// 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() { } } /// /// When the authorization from Spotify expires. This will only occur if is true. /// public event EventHandler OnAccessTokenExpired; public class AuthSuccessEventArgs : EventArgs { public static new AuthSuccessEventArgs Empty { get; } = new AuthSuccessEventArgs(); public AuthSuccessEventArgs() { } } /// /// When an authorization attempt succeeds and gains authorization. /// public event EventHandler OnAuthSuccess; public class AuthFailureEventArgs : EventArgs { public static new AuthFailureEventArgs Empty { get; } = new AuthFailureEventArgs(""); public string Error { get; } public AuthFailureEventArgs(string error) { Error = error; } } /// /// When an authorization attempt fails to gain authorization. /// public event EventHandler OnAuthFailure; /// /// Manually triggers the timeout for any ongoing get web API request. /// 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; /// /// Gets an authorized and ready to use SpotifyWebAPI by following the SecureAuthorizationCodeAuth process with its current settings. /// /// public async Task GetWebApiAsync() { return await Task.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; }); } } }