First draft of SpotifyAPI.Web.Auth

This commit is contained in:
Jonas Dellinger 2020-05-15 20:06:24 +02:00
parent 255bbd5c2f
commit ff9d03ffb0
36 changed files with 754 additions and 136 deletions

View File

@ -0,0 +1,20 @@
namespace SpotifyAPI.Web.Auth
{
[System.Serializable]
public class AuthException : System.Exception
{
public AuthException(string error, string state)
{
Error = error;
State = state;
}
public AuthException(string message) : base(message) { }
public AuthException(string message, System.Exception inner) : base(message, inner) { }
protected AuthException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
public string Error { get; set; }
public string State { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System;
namespace SpotifyAPI.Web.Auth
{
public static class BrowserUtil
{
public static void Open(Uri uri)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var uriStr = uri.ToString().Replace("&", "^&");
Process.Start(new ProcessStartInfo($"cmd", $"/c start {uriStr}"));
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Process.Start("xdg-open", uri.ToString());
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Process.Start("open", uri.ToString());
}
}
}
}

View File

@ -0,0 +1,120 @@
using System.Reflection;
using System.Threading;
using System.Web;
using System.Globalization;
using System.Text;
using System;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Actions;
namespace SpotifyAPI.Web.Auth
{
public class EmbedIOAuthServer : IAuthServer
{
public event Func<object, AuthorizationCodeResponse, Task> AuthorizationCodeReceived;
public event Func<object, ImplictGrantResponse, Task> ImplictGrantReceived;
private const string CallbackPath = "/";
private const string DefaultResourcePath = "SpotifyAPI.Web.Auth.Resources.DefaultHTML";
private CancellationTokenSource _cancelTokenSource;
private readonly WebServer _webServer;
public EmbedIOAuthServer(Uri baseUri, int port, string resourcePath = DefaultResourcePath)
{
Ensure.ArgumentNotNull(baseUri, nameof(baseUri));
BaseUri = baseUri;
Port = port;
_webServer = new WebServer(port)
.WithModule(new ActionModule("/", HttpVerbs.Post, (ctx) =>
{
var query = ctx.Request.QueryString;
if (query["error"] != null)
{
throw new AuthException(query["error"], query["state"]);
}
var requestType = query.Get("request_type");
if (requestType == "token")
{
ImplictGrantReceived?.Invoke(this, new ImplictGrantResponse(
query["access_token"], query["token_type"], int.Parse(query["expires_in"])
)
{
State = query["state"]
});
}
if (requestType == "code")
{
AuthorizationCodeReceived?.Invoke(this, new AuthorizationCodeResponse(query["code"])
{
State = query["state"]
});
}
return ctx.SendStringAsync("OK", "text/plain", Encoding.UTF8);
}))
.WithEmbeddedResources("/", Assembly.GetExecutingAssembly(), resourcePath);
}
public Uri BaseUri { get; }
public Uri RedirectUri { get => new Uri(BaseUri, CallbackPath); }
public int Port { get; }
public Task Start()
{
_cancelTokenSource = new CancellationTokenSource();
_webServer.Start(_cancelTokenSource.Token);
return Task.CompletedTask;
}
public Task Stop()
{
_cancelTokenSource.Cancel();
return Task.CompletedTask;
}
public Uri BuildLoginUri(LoginRequest request)
{
Ensure.ArgumentNotNull(request, nameof(request));
var callbackUri = new Uri(BaseUri, CallbackPath);
StringBuilder builder = new StringBuilder(SpotifyUrls.Authorize.ToString());
builder.Append($"?client_id={request.ClientId}");
builder.Append($"&response_type={request.ResponseTypeParam.ToString().ToLower()}");
builder.Append($"&redirect_uri={HttpUtility.UrlEncode(callbackUri.ToString())}");
if (!string.IsNullOrEmpty(request.State))
{
builder.Append($"&state={HttpUtility.UrlEncode(request.State)}");
}
if (request.Scope != null)
{
builder.Append($"&scope={HttpUtility.UrlEncode(string.Join(" ", request.Scope))}");
}
if (request.ShowDialog != null)
{
builder.Append($"&show_dialog={request.ShowDialog.Value}");
}
return new Uri(builder.ToString());
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_webServer?.Dispose();
}
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
namespace SpotifyAPI.Web.Auth
{
public interface IAuthServer : IDisposable
{
event Func<object, AuthorizationCodeResponse, Task> AuthorizationCodeReceived;
event Func<object, ImplictGrantResponse, Task> ImplictGrantReceived;
Task Start();
Task Stop();
Uri BuildLoginUri(LoginRequest request);
Uri RedirectUri { get; }
}
}

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace SpotifyAPI.Web.Auth
{
public class LoginRequest
{
public LoginRequest(string clientId, ResponseType responseType)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
ClientId = clientId;
ResponseTypeParam = responseType;
}
public ResponseType ResponseTypeParam { get; }
public string ClientId { get; }
public string State { get; set; }
public ICollection<string> Scope { get; set; }
public bool? ShowDialog { get; set; }
public enum ResponseType
{
Code,
Token
}
}
}

View File

@ -0,0 +1,15 @@
namespace SpotifyAPI.Web.Auth
{
public class AuthorizationCodeResponse
{
public AuthorizationCodeResponse(string code)
{
Ensure.ArgumentNotNullOrEmptyString(code, nameof(code));
Code = code;
}
public string Code { get; set; }
public string State { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
namespace SpotifyAPI.Web.Auth
{
public class ImplictGrantResponse
{
public ImplictGrantResponse(string accessToken, string tokenType, int expiresIn)
{
Ensure.ArgumentNotNullOrEmptyString(accessToken, nameof(accessToken));
Ensure.ArgumentNotNullOrEmptyString(tokenType, nameof(tokenType));
AccessToken = accessToken;
TokenType = tokenType;
ExpiresIn = expiresIn;
}
public string AccessToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
public string State { get; set; }
/// <summary>
/// Auto-Initalized to UTC Now
/// </summary>
/// <value></value>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; }
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,77 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="css/bulma.min.css"/>
<style>
p {
font-size: 1.0rem;
}
.demo-image {
margin: 30px;
border: 20px solid #31BD5B;
}
</style>
</head>
<body>
<div class="container">
<div class="notification has-text-centered">
<h1 class="title is-1">Spotify Authentication</h1>
</div>
<div class="content">
<h2 class="title is-2">Introduction</h2>
<!-- <p>
In order to use this app, you will need to follow the steps below.
You will create a Spotify Developer App, which allows applications like this to securely access your spotify data, like playlists or your currently playing track.
<p class="notification is-warning">
If this page looks similar, you may already have a Spotify Developer App created. In this case, you can skip to the bottom and input your <code>client_id</code> and <code>client_secret</code>
</p>
</p>
<h2 class="title is-2">1. Login at your Developer Dashboard</h2>
<p>
Visit <a href="https://developer.spotify.com/dashboard/" rel="nofollow noreferer" target="_blank">https://developer.spotify.com/dashboard/</a> and login with your Spotify Account.
<img class="demo-image" src="images/1.png" width="700" alt=""/>
</p>
<h2 class="title is-2">2. Create a new ClientID</h2>
<p>
Visit <a href="https://developer.spotify.com/dashboard/" rel="nofollow noreferer" target="_blank">https://developer.spotify.com/dashboard/</a> and login with your Spotify Account.
<img class="demo-image" src="images/2.png" width="700" alt=""/>
</p>-->
<form action="/" method="post" style="margin-bottom: 20px">
<div class="field">
<label class="label" for="clientId">Client ID</label>
<div class="control">
<input class="input" type="text" placeholder="Client ID" name="clientId" id="clientId">
</div>
</div>
<div class="field">
<label class="label" for="secretId">Secret ID</label>
<div class="control">
<input class="input" type="password" placeholder="Secret ID" name="secretId" id="secretId">
</div>
</div>
<input class="input is-hidden" hidden name="state" id="state"/>
<div class="field is-grouped">
<div class="control">
<button class="button is-link">Submit</button>
</div>
</div>
</form>
</div>
</div>
<script type="text/javascript">
const hash = window.location.hash.split("#")[1];
document.addEventListener("DOMContentLoaded", () => {
const input = document.getElementById("state");
input.value = hash;
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<title>Spotify Authorization</title>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link href="/main.css" rel="stylesheet">
<script src="/main.js"></script>
</head>
<body>
<main>
<div class="flex justify-center flex-wrap logo">
<div class="w-1/8">
<img src="logo.svg" width="120" height="120" />
</div>
</div>
<h1 class="text-4xl">Success!</h1>
<p class="text-xl mx-2">
Spotify Authorization was successful. You can close this tab and go back to your app.
</p>
<div class="text-center py-4 lg:px-4 my-6">
<div class="p-2 bg-teal-800 items-center text-teal-100 leading-none lg:rounded-full flex lg:inline-flex"
role="alert">
<span class="flex rounded-full bg-teal-500 uppercase px-2 py-1 text-xs font-bold mr-3">Tip</span>
<span class="font-semibold mr-2 text-left flex-auto">
If the app does not detect the authorization, make sure you use one of the following supported Browsers:
<b>Chrome</b>, <b>Edge</b> or <b>Firefox</b>
</span>
</div>
</div>
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -0,0 +1,22 @@
html,
body {
width : 100%;
height: 100%;
}
body {
color : #f5f6fa;
background-color : #353b48;
width : 100%;
height : 100%;
background-attachment: fixed;
}
main {
text-align: center;
margin-top: 100px;
}
.logo {
margin-bottom: 50px;
}

View File

@ -0,0 +1,43 @@
function getUrlParams(hash, start) {
const hashes = hash.slice(hash.indexOf(start) + 1).split('&')
if (!hashes || hashes.length === 0 || hashes[0] === "") {
return undefined;
}
const params = {}
hashes.map(hash => {
const [key, val] = hash.split('=')
params[key] = decodeURIComponent(val)
})
return params
}
function handleImplicitGrant() {
const params = getUrlParams(window.location.hash, '#');
if (!params) {
return;
}
params.request_type = "token";
console.log("Sent request_type token to server", params);
fetch('?' + new URLSearchParams(params).toString(), {
method: 'POST',
});
}
handleImplicitGrant();
function handleAuthenticationCode() {
const params = getUrlParams(window.location.search, '?');
if (!params) {
return;
}
params.request_type = "code";
console.log("Sent request_type code to server", params);
fetch('?' + new URLSearchParams(params).toString(), {
method: 'POST',
});
}
handleAuthenticationCode();

View File

@ -1,45 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script type="text/javascript">
const serialize = (obj) => {
var str = [];
for (let p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}
document.addEventListener("DOMContentLoaded",
() => {
const hash = window.location.hash.substr(1);
let result;
if (hash === "") {
const params = (new URL(document.location)).searchParams;
result = {
error: params.get("error"),
state: params.get("state")
}
} else {
result = hash.split('&').reduce(function(res, item) {
const parts = item.split('=');
res[parts[0]] = parts[1];
return res;
},
{});
}
window.location = `/auth?${serialize(result)}`;
});
</script>
</body>
</html>

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>SpotifyAPI.Web.Auth</PackageId> <PackageId>SpotifyAPI.Web.Auth</PackageId>
<Title>SpotifyAPI.Web.Auth</Title> <Title>SpotifyAPI.Web.Auth</Title>
<Authors>Jonas Dellinger</Authors> <Authors>Jonas Dellinger</Authors>
@ -19,7 +18,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EmbedIO" Version="2.9.2"> <PackageReference Include="EmbedIO" Version="3.4.3">
<PrivateAssets>None</PrivateAssets> <PrivateAssets>None</PrivateAssets>
</PackageReference> </PackageReference>
<ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj"> <ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj">
@ -28,6 +27,10 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="..\SpotifyAPI.Web\Util\Ensure.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Resources\**\*" /> <EmbeddedResource Include="Resources\**\*" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1 @@
credentials.json

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\SpotifyAPI.Web\SpotifyAPI.Web.csproj" />
<ProjectReference Include="..\..\SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,94 @@
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System;
using SpotifyAPI.Web.Auth;
using SpotifyAPI.Web.Http;
using SpotifyAPI.Web;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace CLI
{
/// <summary>
/// This is a basic example how to get user access using the Auth package and a CLI Program
/// Your spotify app needs to have http://localhost:5000 as redirect uri whitelisted
/// </summary>
public class Program
{
private const string CredentialsPath = "credentials.json";
private static readonly string clientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID");
private static readonly string clientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET");
private static EmbedIOAuthServer _server;
public static async Task<int> Main()
{
if (File.Exists(CredentialsPath))
{
await Start();
}
else
{
await StartAuthentication();
}
Console.ReadKey();
return 0;
}
private static async Task Start()
{
var json = await File.ReadAllTextAsync(CredentialsPath);
var token = JsonConvert.DeserializeObject<AuthorizationCodeTokenResponse>(json);
var authenticator = new AuthorizationCodeAuthenticator(clientId, clientSecret, token);
authenticator.TokenRefreshed += (sender, token) => File.WriteAllText(CredentialsPath, JsonConvert.SerializeObject(token));
var config = SpotifyClientConfig.CreateDefault()
.WithAuthenticator(authenticator);
var spotify = new SpotifyClient(config);
var me = await spotify.UserProfile.Current();
Console.WriteLine($"Welcome {me.DisplayName} ({me.Id}), your authenticated!");
var playlists = await spotify.Paginate(await spotify.Playlists.CurrentUsers());
Console.WriteLine($"Total Playlists in your Account: {playlists.Count}");
Environment.Exit(0);
}
private static async Task StartAuthentication()
{
_server = new EmbedIOAuthServer(new Uri("http://localhost:5000"), 5000);
await _server.Start();
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
var request = new LoginRequest(clientId, LoginRequest.ResponseType.Code)
{
Scope = new List<string> { "user-read-email", "user-read-private" }
};
Uri url = _server.BuildLoginUri(request);
try
{
BrowserUtil.Open(url);
}
catch (Exception)
{
Console.WriteLine("Unable to open URL, manually open: {0}", url);
}
}
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.RedirectUri)
);
await File.WriteAllTextAsync(CredentialsPath, JsonConvert.SerializeObject(token));
await Start();
}
}
}

View File

@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj" /> <ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj" />
<ProjectReference Include="..\SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -4,6 +4,6 @@ namespace SpotifyAPI.Web
{ {
public interface IOAuthClient public interface IOAuthClient
{ {
Task<TokenResponse> RequestToken(ClientCredentialsRequest request); Task<CredentialsTokenResponse> RequestToken(ClientCredentialsRequest request);
} }
} }

View File

@ -15,12 +15,23 @@ namespace SpotifyAPI.Web
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")]
public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { } public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { }
public Task<TokenResponse> RequestToken(ClientCredentialsRequest request) public Task<CredentialsTokenResponse> RequestToken(ClientCredentialsRequest request)
{ {
return RequestToken(request, API); return RequestToken(request, API);
} }
public static Task<TokenResponse> RequestToken( public Task<AuthorizationCodeRefreshResponse> RequestToken(AuthorizationCodeRefreshRequest request)
{
return RequestToken(request, API);
}
public Task<AuthorizationCodeTokenResponse> RequestToken(AuthorizationCodeTokenRequest request)
{
return RequestToken(request, API);
}
public static Task<CredentialsTokenResponse> RequestToken(
ClientCredentialsRequest request, IAPIConnector apiConnector ClientCredentialsRequest request, IAPIConnector apiConnector
) )
{ {
@ -32,13 +43,59 @@ namespace SpotifyAPI.Web
new KeyValuePair<string, string>("grant_type", "client_credentials") new KeyValuePair<string, string>("grant_type", "client_credentials")
}; };
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{request.ClientId}:{request.ClientSecret}")); return SendOAuthRequest<CredentialsTokenResponse>(apiConnector, form, request.ClientId, request.ClientSecret);
var headers = new Dictionary<string, string> }
public static Task<AuthorizationCodeRefreshResponse> RequestToken(
AuthorizationCodeRefreshRequest request, IAPIConnector apiConnector
)
{
Ensure.ArgumentNotNull(request, nameof(request));
Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector));
var form = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", request.RefreshToken)
};
return SendOAuthRequest<AuthorizationCodeRefreshResponse>(apiConnector, form, request.ClientId, request.ClientSecret);
}
public static Task<AuthorizationCodeTokenResponse> RequestToken(
AuthorizationCodeTokenRequest request, IAPIConnector apiConnector
)
{
Ensure.ArgumentNotNull(request, nameof(request));
Ensure.ArgumentNotNull(apiConnector, nameof(apiConnector));
var form = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", request.Code),
new KeyValuePair<string, string>("redirect_uri", request.RedirectUri.ToString())
};
return SendOAuthRequest<AuthorizationCodeTokenResponse>(apiConnector, form, request.ClientId, request.ClientSecret);
}
private static Task<T> SendOAuthRequest<T>(
IAPIConnector apiConnector,
List<KeyValuePair<string, string>> form,
string clientId,
string clientSecret)
{
var headers = BuildAuthHeader(clientId, clientSecret);
return apiConnector.Post<T>(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers);
}
private static Dictionary<string, string> BuildAuthHeader(string clientId, string clientSecret)
{
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));
return new Dictionary<string, string>
{ {
{ "Authorization", $"Basic {base64}"} { "Authorization", $"Basic {base64}"}
}; };
return apiConnector.Post<TokenResponse>(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers);
} }
private static APIConnector ValidateConfig(SpotifyClientConfig config) private static APIConnector ValidateConfig(SpotifyClientConfig config)

View File

@ -212,7 +212,9 @@ namespace SpotifyAPI.Web.Http
private async Task ApplyAuthenticator(IRequest request) private async Task ApplyAuthenticator(IRequest request)
{ {
if (_authenticator != null && !request.Endpoint.IsAbsoluteUri) if (_authenticator != null
&& !request.Endpoint.IsAbsoluteUri
|| request.Endpoint.AbsoluteUri.Contains("https://api.spotify.com", StringComparison.InvariantCulture))
{ {
await _authenticator.Apply(request, this).ConfigureAwait(false); await _authenticator.Apply(request, this).ConfigureAwait(false);
} }

View File

@ -0,0 +1,66 @@
using System;
using System.Threading.Tasks;
namespace SpotifyAPI.Web.Http
{
/// <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 AuthorizationCodeAuthenticator : IAuthenticator
{
public event EventHandler<AuthorizationCodeTokenResponse> TokenRefreshed;
/// <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 AuthorizationCodeAuthenticator(string clientId, string clientSecret, AuthorizationCodeTokenResponse initialToken)
{
Ensure.ArgumentNotNull(clientId, nameof(clientId));
Ensure.ArgumentNotNull(clientSecret, nameof(clientSecret));
Ensure.ArgumentNotNull(initialToken, nameof(initialToken));
InitialToken = initialToken;
ClientId = clientId;
ClientSecret = clientSecret;
}
/// <summary>
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
/// </summary>
public string ClientId { get; }
/// <summary>
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
/// </summary>
public string ClientSecret { get; }
/// <summary>
/// The inital token passed to the authenticator. Fields will be updated on refresh.
/// </summary>
/// <value></value>
public AuthorizationCodeTokenResponse InitialToken { get; }
public async Task Apply(IRequest request, IAPIConnector apiConnector)
{
Ensure.ArgumentNotNull(request, nameof(request));
if (InitialToken.IsExpired)
{
var tokenRequest = new AuthorizationCodeRefreshRequest(ClientId, ClientSecret, 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;
TokenRefreshed?.Invoke(this, InitialToken);
}
request.Headers["Authorization"] = $"{InitialToken.TokenType} {InitialToken.AccessToken}";
}
}
}

View File

@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http
/// </summary> /// </summary>
public class CredentialsAuthenticator : IAuthenticator public class CredentialsAuthenticator : IAuthenticator
{ {
private TokenResponse _token; private CredentialsTokenResponse _token;
/// <summary> /// <summary>
/// Initiate a new instance. The first token will be fetched when the first API call occurs /// Initiate a new instance. The first token will be fetched when the first API call occurs
@ -36,7 +36,7 @@ namespace SpotifyAPI.Web.Http
/// <summary> /// <summary>
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard /// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
/// </summary> /// </summary>
public string ClientSecret { get; set; } public string ClientSecret { get; }
public async Task Apply(IRequest request, IAPIConnector apiConnector) public async Task Apply(IRequest request, IAPIConnector apiConnector)
{ {

View File

@ -0,0 +1,23 @@
namespace SpotifyAPI.Web
{
/// <summary>
/// Used when requesting a refreshed token from spotify oauth services (Authorization Code Auth)
/// </summary>
public class AuthorizationCodeRefreshRequest
{
public AuthorizationCodeRefreshRequest(string clientId, string clientSecret, string refreshToken)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret));
Ensure.ArgumentNotNullOrEmptyString(refreshToken, nameof(refreshToken));
ClientId = clientId;
ClientSecret = clientSecret;
RefreshToken = refreshToken;
}
public string RefreshToken { get; }
public string ClientId { get; }
public string ClientSecret { get; }
}
}

View File

@ -0,0 +1,27 @@
using System;
namespace SpotifyAPI.Web
{
/// <summary>
/// Used when requesting a token from spotify oauth services (Authorization Code Auth)
/// </summary>
public class AuthorizationCodeTokenRequest
{
public AuthorizationCodeTokenRequest(string clientId, string clientSecret, string code, Uri redirectUri)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret));
Ensure.ArgumentNotNullOrEmptyString(code, nameof(code));
Ensure.ArgumentNotNull(redirectUri, nameof(redirectUri));
ClientId = clientId;
ClientSecret = clientSecret;
Code = code;
RedirectUri = redirectUri;
}
public string ClientId { get; }
public string ClientSecret { get; }
public string Code { get; }
public Uri RedirectUri { get; }
}
}

View File

@ -1,5 +1,8 @@
namespace SpotifyAPI.Web namespace SpotifyAPI.Web
{ {
/// <summary>
/// Used when requesting a token from spotify oauth services (Client Credentials Auth)
/// </summary>
public class ClientCredentialsRequest public class ClientCredentialsRequest
{ {
public ClientCredentialsRequest(string clientId, string clientSecret) public ClientCredentialsRequest(string clientId, string clientSecret)

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace SpotifyAPI.Web
{
public class AuthorizationCodeRefreshResponse
{
public string AccessToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
public string Scope { get; set; }
/// <summary>
/// Auto-Initalized to UTC Now
/// </summary>
/// <value></value>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
namespace SpotifyAPI.Web
{
public class AuthorizationCodeTokenResponse
{
public string AccessToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
public string Scope { get; set; }
public string RefreshToken { get; set; }
/// <summary>
/// Auto-Initalized to UTC Now
/// </summary>
/// <value></value>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; }
}
}

View File

@ -1,7 +1,7 @@
using System; using System;
namespace SpotifyAPI.Web namespace SpotifyAPI.Web
{ {
public class TokenResponse public class CredentialsTokenResponse
{ {
public string AccessToken { get; set; } public string AccessToken { get; set; }
public string TokenType { get; set; } public string TokenType { get; set; }

View File

@ -0,0 +1,25 @@
namespace SpotifyAPI.Web
{
public static class Scopes
{
public const string UgcImageUpload = "ugc-image-upload";
public const string UserReadPlaybackState = "user-read-playback-state";
public const string UserModifyPlaybackState = "user-modify-playback-state";
public const string UserReadCurrentlyPlaying = "user-read-currently-playing";
public const string Streaming = "streaming";
public const string AppRemoteControl = "app-remote-control";
public const string UserReadEmail = "user-read-email";
public const string UserReadPrivate = "user-read-private";
public const string PlaylistReadCollaborative = "playlist-read-collaborative";
public const string PlaylistModifyPublic = "playlist-modify-public";
public const string PlaylistReadPrivate = "playlist-read-private";
public const string PlaylistModifyPrivate = "playlist-modify-private";
public const string UserLibraryModify = "user-library-modify";
public const string UserLibraryRead = "user-library-read";
public const string UserTopRead = "user-top-read";
public const string UserReadPlaybackPosition = "user-read-playback-position";
public const string UserReadRecentlyPlayed = "user-read-recently-played";
public const string UserFollowRead = "user-follow-read";
public const string UserFollowModify = "user-follow-modify";
}
}

View File

@ -7,6 +7,8 @@ namespace SpotifyAPI.Web
public static readonly Uri APIV1 = new Uri("https://api.spotify.com/v1/"); public static readonly Uri APIV1 = new Uri("https://api.spotify.com/v1/");
public static readonly Uri Authorize = new Uri("https://accounts.spotify.com/authorize");
public static readonly Uri OAuthToken = new Uri("https://accounts.spotify.com/api/token"); public static readonly Uri OAuthToken = new Uri("https://accounts.spotify.com/api/token");
public static Uri Me() => EUri($"me"); public static Uri Me() => EUri($"me");

View File

@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web.Tests", "Spo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyAPI.Web.Auth", "SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyAPI.Web.Auth", "SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SpotifyAPI.Web.Examples", "SpotifyAPI.Web.Examples", "{48A7DE65-29BB-409C-AC45-77F6586C0B15}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "SpotifyAPI.Web.Examples\CLI\CLI.csproj", "{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -27,6 +31,10 @@ Global
{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Release|Any CPU.Build.0 = Release|Any CPU {400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}.Release|Any CPU.Build.0 = Release|Any CPU
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -35,5 +43,6 @@ Global
SolutionGuid = {097062B8-0E87-43C8-BD98-61843A68BE6D} SolutionGuid = {097062B8-0E87-43C8-BD98-61843A68BE6D}
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A} = {48A7DE65-29BB-409C-AC45-77F6586C0B15}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal