From 9b8a4cd2c9f45d3aafa605a207a88ce183f710e5 Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Wed, 13 May 2020 23:49:54 +0200 Subject: [PATCH] Added first OAuth request: client credentials --- .vscode/csharp.code-snippets | 8 +-- .../Clients/SpotifyClientConfigTest.cs | 6 +-- SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs | 4 +- ...catorTest.cs => TokenAuthenticatorTest.cs} | 8 +-- SpotifyAPI.Web/Clients/APIClient.cs | 2 +- .../Clients/Interfaces/IOAuthClient.cs | 9 ++++ SpotifyAPI.Web/Clients/OAuthClient.cs | 49 +++++++++++++++++++ SpotifyAPI.Web/Clients/SpotifyClientConfig.cs | 4 +- SpotifyAPI.Web/Http/APIConnector.cs | 37 ++++++++++---- .../TokenAuthenticator.cs} | 6 +-- .../Http/Interfaces/IAPIConnector.cs | 7 ++- .../Http/Interfaces/IAuthenticator.cs | 2 +- .../Models/Request/CredentialsRequest.cs | 16 ++++++ SpotifyAPI.Web/Models/Response/Token.cs | 9 ++++ SpotifyAPI.Web/SpotifyAPI.Web.csproj | 2 +- SpotifyAPI.Web/SpotifyUrls.cs | 2 + 16 files changed, 140 insertions(+), 31 deletions(-) rename SpotifyAPI.Web.Tests/Http/{TokenHeaderAuthenticatorTest.cs => TokenAuthenticatorTest.cs} (65%) create mode 100644 SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs create mode 100644 SpotifyAPI.Web/Clients/OAuthClient.cs rename SpotifyAPI.Web/Http/{TokenHeaderAuthenticator.cs => Authenticators/TokenAuthenticator.cs} (68%) create mode 100644 SpotifyAPI.Web/Models/Request/CredentialsRequest.cs create mode 100644 SpotifyAPI.Web/Models/Response/Token.cs diff --git a/.vscode/csharp.code-snippets b/.vscode/csharp.code-snippets index 58f4b322..7eeb64f1 100644 --- a/.vscode/csharp.code-snippets +++ b/.vscode/csharp.code-snippets @@ -1,7 +1,7 @@ { - "model-class": { + "class-model": { "scope": "csharp", - "prefix": "model-class", + "prefix": "class-model", "body": [ "namespace SpotifyAPI.Web", "{", @@ -15,9 +15,9 @@ ], "description": "Creates a new model" }, - "request-class": { + "class-request": { "scope": "csharp", - "prefix": "request-class", + "prefix": "class-request", "body": [ "namespace SpotifyAPI.Web", "{", diff --git a/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs b/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs index 9a8f1233..8560ae31 100644 --- a/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs +++ b/SpotifyAPI.Web.Tests/Clients/SpotifyClientConfigTest.cs @@ -37,9 +37,9 @@ namespace SpotifyAPI.Web Assert.AreEqual(null, defaultConfig.HTTPLogger); Assert.AreEqual(null, defaultConfig.RetryHandler); - Assert.IsInstanceOf(typeof(TokenHeaderAuthenticator), defaultConfig.Authenticator); + Assert.IsInstanceOf(typeof(TokenAuthenticator), defaultConfig.Authenticator); - var tokenHeaderAuth = defaultConfig.Authenticator as TokenHeaderAuthenticator; + var tokenHeaderAuth = defaultConfig.Authenticator as TokenAuthenticator; Assert.AreEqual(token, tokenHeaderAuth.Token); Assert.AreEqual(tokenType, tokenHeaderAuth.TokenType); } @@ -51,7 +51,7 @@ namespace SpotifyAPI.Web var defaultConfig = SpotifyClientConfig.CreateDefault(); var tokenConfig = defaultConfig.WithToken(token); - Assert.AreEqual(token, (tokenConfig.Authenticator as TokenHeaderAuthenticator).Token); + Assert.AreEqual(token, (tokenConfig.Authenticator as TokenAuthenticator).Token); Assert.AreNotEqual(defaultConfig, tokenConfig); Assert.AreEqual(null, defaultConfig.Authenticator); } diff --git a/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs b/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs index 7eb7df11..befb6bf6 100644 --- a/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs +++ b/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs @@ -46,7 +46,7 @@ namespace SpotifyAPI.Web.Tests ); await apiConnector.SendAPIRequest(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false); - authenticator.Verify(a => a.Apply(It.IsAny()), Times.Once); + authenticator.Verify(a => a.Apply(It.IsAny(), It.IsAny()), Times.Once); httpClient.Verify(h => h.DoRequest(It.IsAny()), Times.Once); serializer.Verify(s => s.DeserializeResponse(response.Object), Times.Once); } @@ -89,7 +89,7 @@ namespace SpotifyAPI.Web.Tests await apiConnector.SendAPIRequest(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false); serializer.Verify(s => s.SerializeRequest(It.IsAny()), Times.Once); - authenticator.Verify(a => a.Apply(It.IsAny()), Times.Exactly(2)); + authenticator.Verify(a => a.Apply(It.IsAny(), It.IsAny()), Times.Exactly(2)); httpClient.Verify(h => h.DoRequest(It.IsAny()), Times.Exactly(2)); serializer.Verify(s => s.DeserializeResponse(response.Object), Times.Once); } diff --git a/SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs b/SpotifyAPI.Web.Tests/Http/TokenAuthenticatorTest.cs similarity index 65% rename from SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs rename to SpotifyAPI.Web.Tests/Http/TokenAuthenticatorTest.cs index bcbc58fd..44b21cb4 100644 --- a/SpotifyAPI.Web.Tests/Http/TokenHeaderAuthenticatorTest.cs +++ b/SpotifyAPI.Web.Tests/Http/TokenAuthenticatorTest.cs @@ -6,16 +6,18 @@ using SpotifyAPI.Web.Http; namespace SpotifyAPI.Web.Tests { [TestFixture] - public class TokenHeaderAuthenticatorTest + public class TokenAuthenticatorTest { [Test] public void Apply_AddsCorrectHeader() { - var authenticator = new TokenHeaderAuthenticator("MyToken", "Bearer"); + var authenticator = new TokenAuthenticator("MyToken", "Bearer"); var request = new Mock(); + var apiConnector = new Mock(); + request.SetupGet(r => r.Headers).Returns(new Dictionary()); - authenticator.Apply(request.Object); + authenticator.Apply(request.Object, apiConnector.Object); Assert.AreEqual(request.Object.Headers["Authorization"], "Bearer MyToken"); } } diff --git a/SpotifyAPI.Web/Clients/APIClient.cs b/SpotifyAPI.Web/Clients/APIClient.cs index 7008b13f..a778e1ae 100644 --- a/SpotifyAPI.Web/Clients/APIClient.cs +++ b/SpotifyAPI.Web/Clients/APIClient.cs @@ -11,6 +11,6 @@ namespace SpotifyAPI.Web API = apiConnector; } - public IAPIConnector API { get; set; } + protected IAPIConnector API { get; set; } } } diff --git a/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs b/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs new file mode 100644 index 00000000..e1538537 --- /dev/null +++ b/SpotifyAPI.Web/Clients/Interfaces/IOAuthClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace SpotifyAPI.Web +{ + public interface IOAuthClient + { + Task RequestToken(ClientCredentialsRequest request); + } +} diff --git a/SpotifyAPI.Web/Clients/OAuthClient.cs b/SpotifyAPI.Web/Clients/OAuthClient.cs new file mode 100644 index 00000000..75e42f70 --- /dev/null +++ b/SpotifyAPI.Web/Clients/OAuthClient.cs @@ -0,0 +1,49 @@ +using System.Text; +using System; +using System.Net.Http; +using System.Collections.Generic; +using System.Threading.Tasks; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web +{ + public class OAuthClient : APIClient, IOAuthClient + { + public OAuthClient(IAPIConnector apiConnector) : base(apiConnector) { } + public OAuthClient() : this(SpotifyClientConfig.CreateDefault()) { } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")] + public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { } + + public Task RequestToken(ClientCredentialsRequest request) + { + Ensure.ArgumentNotNull(request, nameof(request)); + + var form = new List> + { + new KeyValuePair("grant_type", "client_credentials") + }; + + var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{request.ClientId}:{request.ClientSecret}")); + var headers = new Dictionary + { + { "Authorization", $"Basic {base64}"} + }; + + return API.Post(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers); + } + + private static APIConnector ValidateConfig(SpotifyClientConfig config) + { + Ensure.ArgumentNotNull(config, nameof(config)); + return new APIConnector( + config.BaseAddress, + config.Authenticator, + config.JSONSerializer, + config.HTTPClient, + config.RetryHandler, + config.HTTPLogger + ); + } + } +} diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs index 1458c1d7..6223b180 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -49,7 +49,7 @@ namespace SpotifyAPI.Web return new SpotifyClientConfig( BaseAddress, - new TokenHeaderAuthenticator(token, tokenType), + new TokenAuthenticator(token, tokenType), JSONSerializer, HTTPClient, RetryHandler, @@ -147,7 +147,7 @@ namespace SpotifyAPI.Web public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") { - return CreateDefault().WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType)); + return CreateDefault().WithAuthenticator(new TokenAuthenticator(token, tokenType)); } public static SpotifyClientConfig CreateDefault() diff --git a/SpotifyAPI.Web/Http/APIConnector.cs b/SpotifyAPI.Web/Http/APIConnector.cs index e48e2760..0a540d45 100644 --- a/SpotifyAPI.Web/Http/APIConnector.cs +++ b/SpotifyAPI.Web/Http/APIConnector.cs @@ -106,6 +106,13 @@ namespace SpotifyAPI.Web.Http return SendAPIRequest(uri, HttpMethod.Post, parameters, body); } + public Task Post(Uri uri, IDictionary parameters, object body, Dictionary headers) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return SendAPIRequest(uri, HttpMethod.Post, parameters, body, headers); + } + public async Task Post(Uri uri, IDictionary parameters, object body) { Ensure.ArgumentNotNull(uri, nameof(uri)); @@ -160,13 +167,14 @@ namespace SpotifyAPI.Web.Http Uri uri, HttpMethod method, IDictionary parameters, - object body + object body, + IDictionary headers ) { Ensure.ArgumentNotNull(uri, nameof(uri)); Ensure.ArgumentNotNull(method, nameof(method)); - return new Request(new Dictionary(), parameters) + return new Request(headers ?? new Dictionary(), parameters ?? new Dictionary()) { BaseAddress = _baseAddress, Endpoint = uri, @@ -184,7 +192,10 @@ namespace SpotifyAPI.Web.Http private async Task DoRequest(IRequest request) { - await _authenticator.Apply(request).ConfigureAwait(false); + if (_authenticator != null) + { + await _authenticator.Apply(request, this).ConfigureAwait(false); + } _httpLogger?.OnRequest(request); IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); _httpLogger?.OnResponse(response); @@ -192,7 +203,10 @@ namespace SpotifyAPI.Web.Http { response = await _retryHandler.HandleRetry(request, response, async (newRequest) => { - await _authenticator.Apply(newRequest).ConfigureAwait(false); + if (_authenticator != null) + { + await _authenticator.Apply(request, this).ConfigureAwait(false); + } var newResponse = await _httpClient.DoRequest(request).ConfigureAwait(false); _httpLogger?.OnResponse(newResponse); return newResponse; @@ -206,10 +220,11 @@ namespace SpotifyAPI.Web.Http Uri uri, HttpMethod method, IDictionary parameters = null, - object body = null + object body = null, + IDictionary headers = null ) { - var request = CreateRequest(uri, method, parameters, body); + var request = CreateRequest(uri, method, parameters, body, headers); return DoRequest(request); } @@ -217,10 +232,11 @@ namespace SpotifyAPI.Web.Http Uri uri, HttpMethod method, IDictionary parameters = null, - object body = null + object body = null, + IDictionary headers = null ) { - var request = CreateRequest(uri, method, parameters, body); + var request = CreateRequest(uri, method, parameters, body, headers); IAPIResponse apiResponse = await DoSerializedRequest(request).ConfigureAwait(false); return apiResponse.Body; } @@ -229,10 +245,11 @@ namespace SpotifyAPI.Web.Http Uri uri, HttpMethod method, IDictionary parameters = null, - object body = null + object body = null, + IDictionary headers = null ) { - var request = CreateRequest(uri, method, parameters, body); + var request = CreateRequest(uri, method, parameters, body, headers); var response = await DoSerializedRequest(request).ConfigureAwait(false); return response.Response; } diff --git a/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs b/SpotifyAPI.Web/Http/Authenticators/TokenAuthenticator.cs similarity index 68% rename from SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs rename to SpotifyAPI.Web/Http/Authenticators/TokenAuthenticator.cs index 00c7d865..02388a16 100644 --- a/SpotifyAPI.Web/Http/TokenHeaderAuthenticator.cs +++ b/SpotifyAPI.Web/Http/Authenticators/TokenAuthenticator.cs @@ -2,9 +2,9 @@ using System.Threading.Tasks; namespace SpotifyAPI.Web.Http { - public class TokenHeaderAuthenticator : IAuthenticator + public class TokenAuthenticator : IAuthenticator { - public TokenHeaderAuthenticator(string token, string tokenType) + public TokenAuthenticator(string token, string tokenType) { Token = token; TokenType = tokenType; @@ -14,7 +14,7 @@ namespace SpotifyAPI.Web.Http public string TokenType { get; set; } - public Task Apply(IRequest request) + public Task Apply(IRequest request, IAPIConnector apiConnector) { Ensure.ArgumentNotNull(request, nameof(request)); diff --git a/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs b/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs index d7c402e7..8182b372 100644 --- a/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs +++ b/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs @@ -24,6 +24,7 @@ namespace SpotifyAPI.Web.Http Task Post(Uri uri); Task Post(Uri uri, IDictionary parameters); Task Post(Uri uri, IDictionary parameters, object body); + Task Post(Uri uri, IDictionary parameters, object body, Dictionary headers); Task Post(Uri uri, IDictionary parameters, object body); Task Put(Uri uri); @@ -37,7 +38,11 @@ namespace SpotifyAPI.Web.Http Task Delete(Uri uri, IDictionary parameters, object body); Task Delete(Uri uri, IDictionary parameters, object body); - Task SendAPIRequest(Uri uri, HttpMethod method, IDictionary parameters = null, object body = null); + Task SendAPIRequest( + Uri uri, HttpMethod method, + IDictionary parameters = null, + object body = null, + IDictionary headers = null); void SetRequestTimeout(TimeSpan timeout); } diff --git a/SpotifyAPI.Web/Http/Interfaces/IAuthenticator.cs b/SpotifyAPI.Web/Http/Interfaces/IAuthenticator.cs index b6ac2d8c..84f31f42 100644 --- a/SpotifyAPI.Web/Http/Interfaces/IAuthenticator.cs +++ b/SpotifyAPI.Web/Http/Interfaces/IAuthenticator.cs @@ -4,6 +4,6 @@ namespace SpotifyAPI.Web.Http { public interface IAuthenticator { - Task Apply(IRequest request); + Task Apply(IRequest request, IAPIConnector apiConnector); } } diff --git a/SpotifyAPI.Web/Models/Request/CredentialsRequest.cs b/SpotifyAPI.Web/Models/Request/CredentialsRequest.cs new file mode 100644 index 00000000..c5364557 --- /dev/null +++ b/SpotifyAPI.Web/Models/Request/CredentialsRequest.cs @@ -0,0 +1,16 @@ +namespace SpotifyAPI.Web +{ + public class ClientCredentialsRequest + { + public ClientCredentialsRequest(string clientId, string clientSecret) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId)); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret)); + + ClientId = clientId; + ClientSecret = clientSecret; + } + public string ClientId { get; } + public string ClientSecret { get; } + } +} diff --git a/SpotifyAPI.Web/Models/Response/Token.cs b/SpotifyAPI.Web/Models/Response/Token.cs new file mode 100644 index 00000000..79461ed0 --- /dev/null +++ b/SpotifyAPI.Web/Models/Response/Token.cs @@ -0,0 +1,9 @@ +namespace SpotifyAPI.Web +{ + public class TokenResponse + { + public string AccessToken { get; set; } + public string TokenType { get; set; } + public int ExpiresIn { get; set; } + } +} diff --git a/SpotifyAPI.Web/SpotifyAPI.Web.csproj b/SpotifyAPI.Web/SpotifyAPI.Web.csproj index 0379ab22..9a597a39 100644 --- a/SpotifyAPI.Web/SpotifyAPI.Web.csproj +++ b/SpotifyAPI.Web/SpotifyAPI.Web.csproj @@ -2,7 +2,7 @@ netstandard2.1 - true + SpotifyAPI.Web SpotifyAPI.Web Jonas Dellinger diff --git a/SpotifyAPI.Web/SpotifyUrls.cs b/SpotifyAPI.Web/SpotifyUrls.cs index 403dd2c2..19ea572a 100644 --- a/SpotifyAPI.Web/SpotifyUrls.cs +++ b/SpotifyAPI.Web/SpotifyUrls.cs @@ -7,6 +7,8 @@ namespace SpotifyAPI.Web public static readonly Uri APIV1 = new Uri("https://api.spotify.com/v1/"); + public static readonly Uri OAuthToken = new Uri("https://accounts.spotify.com/api/token"); + public static Uri Me() => EUri($"me"); public static Uri User(string userId) => EUri($"users/{userId}");