mirror of
https://github.com/Sarsoo/Spotify.NET.git
synced 2024-12-23 14:46:26 +00:00
First draft of SpotifyAPI.Web.Auth
This commit is contained in:
parent
255bbd5c2f
commit
ff9d03ffb0
20
SpotifyAPI.Web.Auth/AuthException.cs
Normal file
20
SpotifyAPI.Web.Auth/AuthException.cs
Normal 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; }
|
||||
}
|
||||
}
|
26
SpotifyAPI.Web.Auth/BrowserUtil.cs
Normal file
26
SpotifyAPI.Web.Auth/BrowserUtil.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
SpotifyAPI.Web.Auth/EmbedIOAuthServer.cs
Normal file
120
SpotifyAPI.Web.Auth/EmbedIOAuthServer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
SpotifyAPI.Web.Auth/IAuthServer.cs
Normal file
19
SpotifyAPI.Web.Auth/IAuthServer.cs
Normal 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; }
|
||||
}
|
||||
}
|
26
SpotifyAPI.Web.Auth/Models/Request/LoginRequest.cs
Normal file
26
SpotifyAPI.Web.Auth/Models/Request/LoginRequest.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
30
SpotifyAPI.Web.Auth/Models/Response/ImplicitGrantResponse.cs
Normal file
30
SpotifyAPI.Web.Auth/Models/Response/ImplicitGrantResponse.cs
Normal 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 |
@ -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>
|
BIN
SpotifyAPI.Web.Auth/Resources/DefaultHTML/favicon.ico
Normal file
BIN
SpotifyAPI.Web.Auth/Resources/DefaultHTML/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
38
SpotifyAPI.Web.Auth/Resources/DefaultHTML/index.html
Normal file
38
SpotifyAPI.Web.Auth/Resources/DefaultHTML/index.html
Normal 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>
|
9
SpotifyAPI.Web.Auth/Resources/DefaultHTML/logo.svg
Normal file
9
SpotifyAPI.Web.Auth/Resources/DefaultHTML/logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 51 KiB |
22
SpotifyAPI.Web.Auth/Resources/DefaultHTML/main.css
Normal file
22
SpotifyAPI.Web.Auth/Resources/DefaultHTML/main.css
Normal 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;
|
||||
}
|
43
SpotifyAPI.Web.Auth/Resources/DefaultHTML/main.js
Normal file
43
SpotifyAPI.Web.Auth/Resources/DefaultHTML/main.js
Normal 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();
|
||||
|
@ -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>
|
@ -1,7 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.1</TargetFrameworks>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageId>SpotifyAPI.Web.Auth</PackageId>
|
||||
<Title>SpotifyAPI.Web.Auth</Title>
|
||||
<Authors>Jonas Dellinger</Authors>
|
||||
@ -19,7 +18,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="2.9.2">
|
||||
<PackageReference Include="EmbedIO" Version="3.4.3">
|
||||
<PrivateAssets>None</PrivateAssets>
|
||||
</PackageReference>
|
||||
<ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj">
|
||||
@ -28,6 +27,10 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SpotifyAPI.Web\Util\Ensure.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**\*" />
|
||||
</ItemGroup>
|
||||
|
1
SpotifyAPI.Web.Examples/CLI/.gitignore
vendored
Normal file
1
SpotifyAPI.Web.Examples/CLI/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
credentials.json
|
17
SpotifyAPI.Web.Examples/CLI/CLI.csproj
Normal file
17
SpotifyAPI.Web.Examples/CLI/CLI.csproj
Normal 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>
|
94
SpotifyAPI.Web.Examples/CLI/Program.cs
Normal file
94
SpotifyAPI.Web.Examples/CLI/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SpotifyAPI.Web\SpotifyAPI.Web.csproj" />
|
||||
<ProjectReference Include="..\SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -4,6 +4,6 @@ namespace SpotifyAPI.Web
|
||||
{
|
||||
public interface IOAuthClient
|
||||
{
|
||||
Task<TokenResponse> RequestToken(ClientCredentialsRequest request);
|
||||
Task<CredentialsTokenResponse> RequestToken(ClientCredentialsRequest request);
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,23 @@ namespace SpotifyAPI.Web
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")]
|
||||
public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { }
|
||||
|
||||
public Task<TokenResponse> RequestToken(ClientCredentialsRequest request)
|
||||
public Task<CredentialsTokenResponse> RequestToken(ClientCredentialsRequest request)
|
||||
{
|
||||
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
|
||||
)
|
||||
{
|
||||
@ -32,13 +43,59 @@ namespace SpotifyAPI.Web
|
||||
new KeyValuePair<string, string>("grant_type", "client_credentials")
|
||||
};
|
||||
|
||||
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{request.ClientId}:{request.ClientSecret}"));
|
||||
var headers = new Dictionary<string, string>
|
||||
return SendOAuthRequest<CredentialsTokenResponse>(apiConnector, form, request.ClientId, request.ClientSecret);
|
||||
}
|
||||
|
||||
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}"}
|
||||
};
|
||||
|
||||
return apiConnector.Post<TokenResponse>(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers);
|
||||
}
|
||||
|
||||
private static APIConnector ValidateConfig(SpotifyClientConfig config)
|
||||
|
@ -212,7 +212,9 @@ namespace SpotifyAPI.Web.Http
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace SpotifyAPI.Web.Http
|
||||
/// </summary>
|
||||
public class CredentialsAuthenticator : IAuthenticator
|
||||
{
|
||||
private TokenResponse _token;
|
||||
private CredentialsTokenResponse _token;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// The ClientID, defined in a spotify application in your Spotify Developer Dashboard
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
public string ClientSecret { get; }
|
||||
|
||||
public async Task Apply(IRequest request, IAPIConnector apiConnector)
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Used when requesting a token from spotify oauth services (Client Credentials Auth)
|
||||
/// </summary>
|
||||
public class ClientCredentialsRequest
|
||||
{
|
||||
public ClientCredentialsRequest(string clientId, string clientSecret)
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
namespace SpotifyAPI.Web
|
||||
{
|
||||
public class TokenResponse
|
||||
public class CredentialsTokenResponse
|
||||
{
|
||||
public string AccessToken { get; set; }
|
||||
public string TokenType { get; set; }
|
25
SpotifyAPI.Web/Models/Scopes.cs
Normal file
25
SpotifyAPI.Web/Models/Scopes.cs
Normal 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";
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ namespace SpotifyAPI.Web
|
||||
|
||||
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 Uri Me() => EUri($"me");
|
||||
|
@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpotifyAPI.Web.Tests", "Spo
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpotifyAPI.Web.Auth", "SpotifyAPI.Web.Auth\SpotifyAPI.Web.Auth.csproj", "{400A3787-FDBE-4A4C-9DDD-AAEB3DCE1DF5}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -35,5 +43,6 @@ Global
|
||||
SolutionGuid = {097062B8-0E87-43C8-BD98-61843A68BE6D}
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A} = {48A7DE65-29BB-409C-AC45-77F6586C0B15}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
Loading…
Reference in New Issue
Block a user