using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using SpotifyAPI.Web.Enums; using SpotifyAPI.Web.Models; using Unosquare.Labs.EmbedIO; using Unosquare.Labs.EmbedIO.Constants; using Unosquare.Labs.EmbedIO.Modules; namespace SpotifyAPI.Web.Auth { /// /// /// A version of 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. /// /// /// It's recommended that you use if you would like to use the TokenSwap method. /// /// public class TokenSwapAuth : SpotifyAuthServer { readonly string _exchangeServerUri; /// /// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival. /// public string HtmlResponse { get; set; } = ""; /// /// If true, will time how long it takes for access to expire. On expiry, the event fires. /// public bool TimeAccessExpiry { get; set; } public ProxyConfig ProxyConfig { get; set; } /// The URI to an exchange server that will perform the key exchange. /// 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) /// /// Stating none will randomly generate a state parameter. /// The HTML to respond with when the callback server (serverUri) is reached. The default value will close the window on arrival. 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; } _exchangeServerUri = exchangeServerUri; } protected override void AdaptWebServer(WebServer webServer) { webServer.Module().RegisterController(); } 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()); } /// /// The maximum amount of times to retry getting a token. /// /// A token get is attempted every time you and . /// public int MaxGetTokenRetries { get; set; } = 10; /// /// Creates a HTTP request to obtain a token object. /// 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. /// /// Will re-attempt on error, on null or on no access token times before finally returning null. /// /// /// Can only be "refresh_token" or "authorization_code". /// This needs to be defined if "grantType" is "authorization_code". /// This needs to be defined if "grantType" is "refresh_token". /// Does not need to be defined. Used internally for retry attempt recursion. /// Attempts to return a full , but after retry attempts, may return a with no , or null. async Task GetToken(string grantType, string authorizationCode = "", string refreshToken = "", int currentRetries = 0) { FormUrlEncodedContent content = new FormUrlEncodedContent(new Dictionary { { "grant_type", grantType }, { "code", authorizationCode }, { "refresh_token", refreshToken } }); try { HttpClientHandler handler = ProxyConfig.CreateClientHandler(ProxyConfig); HttpClient client = new HttpClient(handler); HttpResponseMessage siteResponse = await client.PostAsync(_exchangeServerUri, content); Token token = JsonConvert.DeserializeObject(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; /// /// When Spotify authorization has expired. Will only trigger if is true. /// public event EventHandler OnAccessTokenExpired; /// /// If is true, sets a timer for how long access will take to expire. /// /// 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); } /// /// 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 . /// /// /// public async Task ExchangeCodeAsync(string authorizationCode) { Token token = await GetToken("authorization_code", authorizationCode : authorizationCode); if (token != null && !token.HasError() && !string.IsNullOrEmpty(token.AccessToken)) { SetAccessExpireTimer(token); } return token; } /// /// 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). /// /// /// public async Task 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 GetAuth() { string state = Request.QueryString["state"]; SpotifyAuthServer 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 HttpContext.HtmlResponseAsync(((TokenSwapAuth) auth).HtmlResponse); } } }