Skip to content

Commit ff9d03f

Browse files
committed
First draft of SpotifyAPI.Web.Auth
1 parent 255bbd5 commit ff9d03f

36 files changed

Lines changed: 754 additions & 136 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace SpotifyAPI.Web.Auth
2+
{
3+
[System.Serializable]
4+
public class AuthException : System.Exception
5+
{
6+
public AuthException(string error, string state)
7+
{
8+
Error = error;
9+
State = state;
10+
}
11+
public AuthException(string message) : base(message) { }
12+
public AuthException(string message, System.Exception inner) : base(message, inner) { }
13+
protected AuthException(
14+
System.Runtime.Serialization.SerializationInfo info,
15+
System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
16+
17+
public string Error { get; set; }
18+
public string State { get; set; }
19+
}
20+
}

‎SpotifyAPI.Web.Auth/BrowserUtil.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Diagnostics;
2+
using System.Runtime.InteropServices;
3+
using System;
4+
5+
namespace SpotifyAPI.Web.Auth
6+
{
7+
public static class BrowserUtil
8+
{
9+
public static void Open(Uri uri)
10+
{
11+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
12+
{
13+
var uriStr = uri.ToString().Replace("&", "^&");
14+
Process.Start(new ProcessStartInfo($"cmd", $"/c start {uriStr}"));
15+
}
16+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
17+
{
18+
Process.Start("xdg-open", uri.ToString());
19+
}
20+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
21+
{
22+
Process.Start("open", uri.ToString());
23+
}
24+
}
25+
}
26+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Reflection;
2+
using System.Threading;
3+
using System.Web;
4+
using System.Globalization;
5+
using System.Text;
6+
using System;
7+
using System.Threading.Tasks;
8+
using EmbedIO;
9+
using EmbedIO.Actions;
10+
11+
namespace SpotifyAPI.Web.Auth
12+
{
13+
public class EmbedIOAuthServer : IAuthServer
14+
{
15+
public event Func<object, AuthorizationCodeResponse, Task> AuthorizationCodeReceived;
16+
public event Func<object, ImplictGrantResponse, Task> ImplictGrantReceived;
17+
18+
private const string CallbackPath = "/";
19+
private const string DefaultResourcePath = "SpotifyAPI.Web.Auth.Resources.DefaultHTML";
20+
21+
private CancellationTokenSource _cancelTokenSource;
22+
private readonly WebServer _webServer;
23+
24+
public EmbedIOAuthServer(Uri baseUri, int port, string resourcePath = DefaultResourcePath)
25+
{
26+
Ensure.ArgumentNotNull(baseUri, nameof(baseUri));
27+
28+
BaseUri = baseUri;
29+
Port = port;
30+
31+
_webServer = new WebServer(port)
32+
.WithModule(new ActionModule("/", HttpVerbs.Post, (ctx) =>
33+
{
34+
var query = ctx.Request.QueryString;
35+
if (query["error"] != null)
36+
{
37+
throw new AuthException(query["error"], query["state"]);
38+
}
39+
40+
var requestType = query.Get("request_type");
41+
if (requestType == "token")
42+
{
43+
ImplictGrantReceived?.Invoke(this, new ImplictGrantResponse(
44+
query["access_token"], query["token_type"], int.Parse(query["expires_in"])
45+
)
46+
{
47+
State = query["state"]
48+
});
49+
}
50+
if (requestType == "code")
51+
{
52+
AuthorizationCodeReceived?.Invoke(this, new AuthorizationCodeResponse(query["code"])
53+
{
54+
State = query["state"]
55+
});
56+
}
57+
58+
return ctx.SendStringAsync("OK", "text/plain", Encoding.UTF8);
59+
}))
60+
.WithEmbeddedResources("/", Assembly.GetExecutingAssembly(), resourcePath);
61+
}
62+
63+
public Uri BaseUri { get; }
64+
public Uri RedirectUri { get => new Uri(BaseUri, CallbackPath); }
65+
public int Port { get; }
66+
67+
public Task Start()
68+
{
69+
_cancelTokenSource = new CancellationTokenSource();
70+
_webServer.Start(_cancelTokenSource.Token);
71+
return Task.CompletedTask;
72+
}
73+
74+
public Task Stop()
75+
{
76+
_cancelTokenSource.Cancel();
77+
return Task.CompletedTask;
78+
}
79+
80+
public Uri BuildLoginUri(LoginRequest request)
81+
{
82+
Ensure.ArgumentNotNull(request, nameof(request));
83+
84+
var callbackUri = new Uri(BaseUri, CallbackPath);
85+
86+
StringBuilder builder = new StringBuilder(SpotifyUrls.Authorize.ToString());
87+
builder.Append($"?client_id={request.ClientId}");
88+
builder.Append($"&response_type={request.ResponseTypeParam.ToString().ToLower()}");
89+
builder.Append($"&redirect_uri={HttpUtility.UrlEncode(callbackUri.ToString())}");
90+
if (!string.IsNullOrEmpty(request.State))
91+
{
92+
builder.Append($"&state={HttpUtility.UrlEncode(request.State)}");
93+
}
94+
if (request.Scope != null)
95+
{
96+
builder.Append($"&scope={HttpUtility.UrlEncode(string.Join(" ", request.Scope))}");
97+
}
98+
if (request.ShowDialog != null)
99+
{
100+
builder.Append($"&show_dialog={request.ShowDialog.Value}");
101+
}
102+
103+
return new Uri(builder.ToString());
104+
}
105+
106+
public void Dispose()
107+
{
108+
Dispose(true);
109+
GC.SuppressFinalize(this);
110+
}
111+
112+
protected virtual void Dispose(bool disposing)
113+
{
114+
if (disposing)
115+
{
116+
_webServer?.Dispose();
117+
}
118+
}
119+
}
120+
}

‎SpotifyAPI.Web.Auth/IAuthServer.cs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace SpotifyAPI.Web.Auth
5+
{
6+
public interface IAuthServer : IDisposable
7+
{
8+
event Func<object, AuthorizationCodeResponse, Task> AuthorizationCodeReceived;
9+
10+
event Func<object, ImplictGrantResponse, Task> ImplictGrantReceived;
11+
12+
Task Start();
13+
Task Stop();
14+
15+
Uri BuildLoginUri(LoginRequest request);
16+
17+
Uri RedirectUri { get; }
18+
}
19+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
namespace SpotifyAPI.Web.Auth
3+
{
4+
public class LoginRequest
5+
{
6+
public LoginRequest(string clientId, ResponseType responseType)
7+
{
8+
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
9+
10+
ClientId = clientId;
11+
ResponseTypeParam = responseType;
12+
}
13+
14+
public ResponseType ResponseTypeParam { get; }
15+
public string ClientId { get; }
16+
public string State { get; set; }
17+
public ICollection<string> Scope { get; set; }
18+
public bool? ShowDialog { get; set; }
19+
20+
public enum ResponseType
21+
{
22+
Code,
23+
Token
24+
}
25+
}
26+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace SpotifyAPI.Web.Auth
2+
{
3+
public class AuthorizationCodeResponse
4+
{
5+
public AuthorizationCodeResponse(string code)
6+
{
7+
Ensure.ArgumentNotNullOrEmptyString(code, nameof(code));
8+
9+
Code = code;
10+
}
11+
12+
public string Code { get; set; }
13+
public string State { get; set; }
14+
}
15+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
3+
namespace SpotifyAPI.Web.Auth
4+
{
5+
public class ImplictGrantResponse
6+
{
7+
public ImplictGrantResponse(string accessToken, string tokenType, int expiresIn)
8+
{
9+
Ensure.ArgumentNotNullOrEmptyString(accessToken, nameof(accessToken));
10+
Ensure.ArgumentNotNullOrEmptyString(tokenType, nameof(tokenType));
11+
12+
AccessToken = accessToken;
13+
TokenType = tokenType;
14+
ExpiresIn = expiresIn;
15+
}
16+
17+
public string AccessToken { get; set; }
18+
public string TokenType { get; set; }
19+
public int ExpiresIn { get; set; }
20+
public string State { get; set; }
21+
22+
/// <summary>
23+
/// Auto-Initalized to UTC Now
24+
/// </summary>
25+
/// <value></value>
26+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
27+
28+
public bool IsExpired { get => CreatedAt.AddSeconds(ExpiresIn) <= DateTime.UtcNow; }
29+
}
30+
}

‎SpotifyAPI.Web.Auth/Resources/AuthorizationCodeAuth/css/bulma.min.css‎

Lines changed: 0 additions & 1 deletion
This file was deleted.
-38.1 KB
Binary file not shown.
-15.7 KB
Binary file not shown.

0 commit comments

Comments
 (0)