diff --git a/Src/Notion.Client/Api/Authentication/CreateToken/AuthenticationClient.cs b/Src/Notion.Client/Api/Authentication/CreateToken/AuthenticationClient.cs index 23632632..12da19fb 100644 --- a/Src/Notion.Client/Api/Authentication/CreateToken/AuthenticationClient.cs +++ b/Src/Notion.Client/Api/Authentication/CreateToken/AuthenticationClient.cs @@ -9,11 +9,13 @@ public async Task CreateTokenAsync( CreateTokenRequest createTokenRequest, CancellationToken cancellationToken = default) { - var body = (ICreateTokenBodyParameters)createTokenRequest; + ICreateTokenBodyParameters body = createTokenRequest; + IBasicAuthenticationParameters basicAuth = createTokenRequest; return await _client.PostAsync( ApiEndpoints.AuthenticationUrls.CreateToken(), body, + basicAuthenticationParameters: basicAuth, cancellationToken: cancellationToken ); } diff --git a/Src/Notion.Client/Api/Authentication/CreateToken/Request/CreateTokenRequest.cs b/Src/Notion.Client/Api/Authentication/CreateToken/Request/CreateTokenRequest.cs index 3fe8ffde..38a377b6 100644 --- a/Src/Notion.Client/Api/Authentication/CreateToken/Request/CreateTokenRequest.cs +++ b/Src/Notion.Client/Api/Authentication/CreateToken/Request/CreateTokenRequest.cs @@ -1,6 +1,6 @@ namespace Notion.Client { - public class CreateTokenRequest : ICreateTokenBodyParameters + public class CreateTokenRequest : ICreateTokenBodyParameters, IBasicAuthenticationParameters { public string GrantType => "authorization_code"; @@ -9,5 +9,9 @@ public class CreateTokenRequest : ICreateTokenBodyParameters public string RedirectUri { get; set; } public ExternalAccount ExternalAccount { get; set; } + + public string ClientId { get; set; } + + public string ClientSecret { get; set; } } } diff --git a/Src/Notion.Client/Api/Authentication/HeaderHelpers.cs b/Src/Notion.Client/Api/Authentication/HeaderHelpers.cs new file mode 100644 index 00000000..bf9eb115 --- /dev/null +++ b/Src/Notion.Client/Api/Authentication/HeaderHelpers.cs @@ -0,0 +1,21 @@ +using System; +using System.Text; + +namespace Notion.Client +{ + internal static class HeaderHelpers + { + public static string GetBasicAuthHeaderValue(IBasicAuthenticationParameters basicAuth) + { + if (basicAuth == null) + { + return null; + } + + var basicAuthString = $"{basicAuth.ClientId}:{basicAuth.ClientSecret}"; + var basicAuthHeaderValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(basicAuthString)); + + return basicAuthHeaderValue; + } + } +} diff --git a/Src/Notion.Client/Api/Authentication/IBasicAuthenticationParameters.cs b/Src/Notion.Client/Api/Authentication/IBasicAuthenticationParameters.cs new file mode 100644 index 00000000..dc41e502 --- /dev/null +++ b/Src/Notion.Client/Api/Authentication/IBasicAuthenticationParameters.cs @@ -0,0 +1,8 @@ +namespace Notion.Client +{ + public interface IBasicAuthenticationParameters + { + string ClientId { get; } + string ClientSecret { get; } + } +} diff --git a/Src/Notion.Client/Api/Authentication/RevokeToken/AuthenticationClient.cs b/Src/Notion.Client/Api/Authentication/RevokeToken/AuthenticationClient.cs index c1f3ab9d..babbd135 100644 --- a/Src/Notion.Client/Api/Authentication/RevokeToken/AuthenticationClient.cs +++ b/Src/Notion.Client/Api/Authentication/RevokeToken/AuthenticationClient.cs @@ -9,11 +9,13 @@ public async Task RevokeTokenAsync( RevokeTokenRequest revokeTokenRequest, CancellationToken cancellationToken = default) { - var body = (IRevokeTokenBodyParameters)revokeTokenRequest; + IRevokeTokenBodyParameters body = revokeTokenRequest; + IBasicAuthenticationParameters basicAuth = revokeTokenRequest; await _client.PostAsync( ApiEndpoints.AuthenticationUrls.RevokeToken(), body, + basicAuthenticationParameters: basicAuth, cancellationToken: cancellationToken ); } diff --git a/Src/Notion.Client/Api/Authentication/RevokeToken/Request/RevokeTokenRequest.cs b/Src/Notion.Client/Api/Authentication/RevokeToken/Request/RevokeTokenRequest.cs index 7e92ca01..73494b97 100644 --- a/Src/Notion.Client/Api/Authentication/RevokeToken/Request/RevokeTokenRequest.cs +++ b/Src/Notion.Client/Api/Authentication/RevokeToken/Request/RevokeTokenRequest.cs @@ -1,7 +1,11 @@ namespace Notion.Client { - public class RevokeTokenRequest : IRevokeTokenBodyParameters + public class RevokeTokenRequest : IRevokeTokenBodyParameters, IBasicAuthenticationParameters { public string Token { get; set; } + + public string ClientId { get; set; } + + public string ClientSecret { get; set; } } } diff --git a/Src/Notion.Client/RestClient/IRestClient.cs b/Src/Notion.Client/RestClient/IRestClient.cs index b8ffe5b3..2e6fe960 100644 --- a/Src/Notion.Client/RestClient/IRestClient.cs +++ b/Src/Notion.Client/RestClient/IRestClient.cs @@ -20,6 +20,7 @@ Task PostAsync( IEnumerable> queryParams = null, IDictionary headers = null, JsonSerializerSettings serializerSettings = null, + IBasicAuthenticationParameters basicAuthenticationParameters = null, CancellationToken cancellationToken = default); Task PatchAsync( diff --git a/Src/Notion.Client/RestClient/RestClient.cs b/Src/Notion.Client/RestClient/RestClient.cs index 59a848f7..fc15f38d 100644 --- a/Src/Notion.Client/RestClient/RestClient.cs +++ b/Src/Notion.Client/RestClient/RestClient.cs @@ -48,6 +48,7 @@ public async Task PostAsync( IEnumerable> queryParams = null, IDictionary headers = null, JsonSerializerSettings serializerSettings = null, + IBasicAuthenticationParameters basicAuthenticationParameters = null, CancellationToken cancellationToken = default) { void AttachContent(HttpRequestMessage httpRequest) @@ -56,8 +57,15 @@ void AttachContent(HttpRequestMessage httpRequest) Encoding.UTF8, "application/json"); } - var response = await SendAsync(uri, HttpMethod.Post, queryParams, headers, AttachContent, - cancellationToken); + var response = await SendAsync( + uri, + HttpMethod.Post, + queryParams, + headers, + AttachContent, + basicAuthenticationParameters, + cancellationToken + ); return await response.ParseStreamAsync(serializerSettings); } @@ -77,7 +85,7 @@ void AttachContent(HttpRequestMessage httpRequest) } var response = await SendAsync(uri, new HttpMethod("PATCH"), queryParams, headers, AttachContent, - cancellationToken); + basicAuthenticationParameters: null, cancellationToken); return await response.ParseStreamAsync(serializerSettings); } @@ -88,7 +96,8 @@ public async Task DeleteAsync( IDictionary headers = null, CancellationToken cancellationToken = default) { - await SendAsync(uri, HttpMethod.Delete, queryParams, headers, null, cancellationToken); + await SendAsync(uri, HttpMethod.Delete, queryParams, headers, null, + basicAuthenticationParameters: null, cancellationToken); } private static ClientOptions MergeOptions(ClientOptions options) @@ -116,6 +125,7 @@ private static async Task BuildException(HttpResponseMessage response if (errorResponse.ErrorCode == NotionAPIErrorCode.RateLimited) { var retryAfter = response.Headers.RetryAfter.Delta; + return new NotionApiRateLimitException( response.StatusCode, errorResponse.ErrorCode, @@ -139,6 +149,7 @@ private async Task SendAsync( IEnumerable> queryParams = null, IDictionary headers = null, Action attachContent = null, + IBasicAuthenticationParameters basicAuthenticationParameters = null, CancellationToken cancellationToken = default) { EnsureHttpClient(); @@ -146,7 +157,8 @@ private async Task SendAsync( requestUri = AddQueryString(requestUri, queryParams); using var httpRequest = new HttpRequestMessage(httpMethod, requestUri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.AuthToken); + + httpRequest.Headers.Authorization = CreateAuthenticationHeader(basicAuthenticationParameters); httpRequest.Headers.Add("Notion-Version", _options.NotionVersion); if (headers != null) @@ -166,6 +178,13 @@ private async Task SendAsync( return response; } + private AuthenticationHeaderValue CreateAuthenticationHeader(IBasicAuthenticationParameters basicAuth) + { + return basicAuth != null + ? new AuthenticationHeaderValue("Basic", HeaderHelpers.GetBasicAuthHeaderValue(basicAuth)) + : new AuthenticationHeaderValue("Bearer", _options.AuthToken); + } + private static void AddHeaders(HttpRequestMessage request, IDictionary headers) { foreach (var header in headers) diff --git a/Test/Notion.IntegrationTests/AuthenticationClientTests.cs b/Test/Notion.IntegrationTests/AuthenticationClientTests.cs new file mode 100644 index 00000000..9e07c8b5 --- /dev/null +++ b/Test/Notion.IntegrationTests/AuthenticationClientTests.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Notion.Client; +using Xunit; + +namespace Notion.IntegrationTests; + +public class AuthenticationClientTests : IntegrationTestBase +{ + private readonly string _clientId = GetEnvironmentVariableRequired("NOTION_CLIENT_ID"); + private readonly string _clientSecret = GetEnvironmentVariableRequired("NOTION_CLIENT_SECRET"); + + [Fact] + public async Task Create_and_revoke_token() + { + // Arrange + var createRequest = new CreateTokenRequest + { + Code = "03b3bd2d-6b96-4104-a9f4-ee04d881532c", + ClientId = _clientId, + ClientSecret = _clientSecret, + RedirectUri = "https://localhost:5001", + }; + + // Act + var response = await Client.AuthenticationClient.CreateTokenAsync(createRequest); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.AccessToken); + + // revoke token + await Client.AuthenticationClient.RevokeTokenAsync(new RevokeTokenRequest + { + Token = response.AccessToken, + ClientId = _clientId, + ClientSecret = _clientSecret + }); + } +} diff --git a/Test/Notion.IntegrationTests/IntegrationTestBase.cs b/Test/Notion.IntegrationTests/IntegrationTestBase.cs index 31836c32..34707ff1 100644 --- a/Test/Notion.IntegrationTests/IntegrationTestBase.cs +++ b/Test/Notion.IntegrationTests/IntegrationTestBase.cs @@ -15,10 +15,13 @@ protected IntegrationTestBase() Client = NotionClientFactory.Create(options); - ParentPageId = Environment.GetEnvironmentVariable("NOTION_PARENT_PAGE_ID") - ?? throw new InvalidOperationException("Parent page id is required."); - - ParentDatabaseId = Environment.GetEnvironmentVariable("NOTION_PARENT_DATABASE_ID") - ?? throw new InvalidOperationException("Parent database id is required."); + ParentPageId = GetEnvironmentVariableRequired("NOTION_PARENT_PAGE_ID"); + ParentDatabaseId = GetEnvironmentVariableRequired("NOTION_PARENT_DATABASE_ID"); + } + + protected static string GetEnvironmentVariableRequired(string envName) + { + return Environment.GetEnvironmentVariable(envName) ?? + throw new InvalidOperationException($"Environment variable '{envName}' is required."); } }