Skip to content

Commit 115829b

Browse files
Merge pull request #474 from notion-dotnet/453-add-support-for-introspect-token-endpoint
Add support for Introspect token API
2 parents 1a55497 + ae89c33 commit 115829b

File tree

10 files changed

+287
-1
lines changed

10 files changed

+287
-1
lines changed

Src/Notion.Client/Api/ApiEndpoints.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public static class AuthenticationUrls
136136
{
137137
public static string CreateToken() => "/v1/oauth/token";
138138
public static string RevokeToken() => "/v1/oauth/revoke";
139+
public static string IntrospectToken() => "/v1/oauth/introspect";
139140
}
140141
}
141142
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
3+
namespace Notion.Client
4+
{
5+
public static class BasicAuthParamValidator
6+
{
7+
public static void Validate(IBasicAuthenticationParameters basicAuthParams)
8+
{
9+
if (basicAuthParams == null)
10+
{
11+
throw new ArgumentNullException(nameof(basicAuthParams), "Basic authentication parameters must be provided.");
12+
}
13+
14+
if (string.IsNullOrWhiteSpace(basicAuthParams.ClientId))
15+
{
16+
throw new ArgumentException("ClientId must be provided.", nameof(basicAuthParams.ClientId));
17+
}
18+
19+
if (string.IsNullOrWhiteSpace(basicAuthParams.ClientSecret))
20+
{
21+
throw new ArgumentException("ClientSecret must be provided.", nameof(basicAuthParams.ClientSecret));
22+
}
23+
}
24+
}
25+
}

Src/Notion.Client/Api/Authentication/IAuthenticationClient.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,16 @@ Task RevokeTokenAsync(
2929
RevokeTokenRequest revokeTokenRequest,
3030
CancellationToken cancellationToken = default
3131
);
32+
33+
/// <summary>
34+
/// Get a token's active status, scope, and issued time.
35+
/// </summary>
36+
/// <param name="introspectTokenRequest"></param>
37+
/// <param name="cancellationToken"></param>
38+
/// <returns></returns>
39+
Task<IntrospectTokenResponse> IntrospectTokenAsync(
40+
IntrospectTokenRequest introspectTokenRequest,
41+
CancellationToken cancellationToken = default
42+
);
3243
}
3344
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace Notion.Client
6+
{
7+
public sealed partial class AuthenticationClient
8+
{
9+
public async Task<IntrospectTokenResponse> IntrospectTokenAsync(
10+
IntrospectTokenRequest introspectTokenRequest,
11+
CancellationToken cancellationToken = default)
12+
{
13+
if (introspectTokenRequest is null)
14+
{
15+
throw new ArgumentNullException(nameof(introspectTokenRequest));
16+
}
17+
18+
IIntrospectTokenBodyParameters body = introspectTokenRequest;
19+
IBasicAuthenticationParameters basicAuth = introspectTokenRequest;
20+
21+
if (string.IsNullOrWhiteSpace(body.Token))
22+
{
23+
throw new ArgumentException("Token must be provided.", nameof(body.Token));
24+
}
25+
26+
BasicAuthParamValidator.Validate(basicAuth);
27+
28+
return await _client.PostAsync<IntrospectTokenResponse>(
29+
ApiEndpoints.AuthenticationUrls.IntrospectToken(),
30+
body,
31+
basicAuthenticationParameters: basicAuth,
32+
cancellationToken: cancellationToken
33+
);
34+
}
35+
}
36+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Notion.Client
4+
{
5+
public interface IIntrospectTokenBodyParameters
6+
{
7+
/// <summary>
8+
/// The access token
9+
/// </summary>
10+
[JsonProperty(PropertyName = "token")]
11+
public string Token { get; }
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Notion.Client
2+
{
3+
public class IntrospectTokenRequest : IIntrospectTokenBodyParameters, IBasicAuthenticationParameters
4+
{
5+
public string Token { get; set; }
6+
7+
public string ClientId { get; set; }
8+
9+
public string ClientSecret { get; set; }
10+
}
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Notion.Client
4+
{
5+
public class IntrospectTokenResponse
6+
{
7+
[JsonProperty("active")]
8+
public bool IsActive { get; set; }
9+
10+
[JsonProperty("scope")]
11+
public string Scope { get; set; }
12+
13+
[JsonProperty("iat")]
14+
public long Iat { get; set; }
15+
}
16+
}

Test/Notion.IntegrationTests/AuthenticationClientTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,46 @@ public async Task Create_and_revoke_token()
2727
// Assert
2828
Assert.NotNull(response);
2929
Assert.NotNull(response.AccessToken);
30-
30+
31+
// revoke token
32+
await Client.AuthenticationClient.RevokeTokenAsync(new RevokeTokenRequest
33+
{
34+
Token = response.AccessToken,
35+
ClientId = _clientId,
36+
ClientSecret = _clientSecret
37+
});
38+
}
39+
40+
[Fact]
41+
public async Task Introspect_token()
42+
{
43+
// Arrange
44+
var createRequest = new CreateTokenRequest
45+
{
46+
Code = "036822b2-62c1-42f4-95ea-0153e69cc20e",
47+
ClientId = _clientId,
48+
ClientSecret = _clientSecret,
49+
RedirectUri = "https://localhost:5001",
50+
};
51+
52+
// Act
53+
var response = await Client.AuthenticationClient.CreateTokenAsync(createRequest);
54+
55+
// Assert
56+
Assert.NotNull(response);
57+
Assert.NotNull(response.AccessToken);
58+
59+
// introspect token
60+
var introspectResponse = await Client.AuthenticationClient.IntrospectTokenAsync(new IntrospectTokenRequest
61+
{
62+
Token = response.AccessToken,
63+
ClientId = _clientId,
64+
ClientSecret = _clientSecret
65+
});
66+
67+
Assert.NotNull(introspectResponse);
68+
Assert.True(introspectResponse.IsActive);
69+
3170
// revoke token
3271
await Client.AuthenticationClient.RevokeTokenAsync(new RevokeTokenRequest
3372
{
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Moq;
6+
using Moq.AutoMock;
7+
using Newtonsoft.Json;
8+
using Notion.Client;
9+
using Xunit;
10+
11+
namespace Notion.UnitTests;
12+
13+
public class AuthenticationClientTests
14+
{
15+
private readonly AutoMocker _mocker = new();
16+
private readonly Mock<IRestClient> _restClientMock;
17+
private readonly AuthenticationClient _authenticationClient;
18+
19+
public AuthenticationClientTests()
20+
{
21+
_restClientMock = _mocker.GetMock<IRestClient>();
22+
_authenticationClient = _mocker.CreateInstance<AuthenticationClient>();
23+
}
24+
25+
[Fact]
26+
public async Task IntrospectTokenAsync_ThrowsArgumentNullException_WhenRequestIsNull()
27+
{
28+
// Act & Assert
29+
var exception = await Assert.ThrowsAsync<ArgumentNullException>(() => _authenticationClient.IntrospectTokenAsync(null));
30+
Assert.Equal("introspectTokenRequest", exception.ParamName);
31+
Assert.Equal("Value cannot be null. (Parameter 'introspectTokenRequest')", exception.Message);
32+
}
33+
34+
[Theory]
35+
[InlineData(null)]
36+
[InlineData("")]
37+
[InlineData(" ")]
38+
public async Task IntrospectTokenAsync_ThrowsArgumentException_WhenTokenIsNullOrEmpty(string token)
39+
{
40+
// Arrange
41+
var request = new IntrospectTokenRequest
42+
{
43+
Token = token,
44+
ClientId = "validClientId",
45+
ClientSecret = "validClientSecret"
46+
};
47+
48+
// Act & Assert
49+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => _authenticationClient.IntrospectTokenAsync(request));
50+
Assert.Equal("Token", exception.ParamName);
51+
Assert.Equal("Token must be provided. (Parameter 'Token')", exception.Message);
52+
}
53+
54+
[Theory]
55+
[InlineData(null)]
56+
[InlineData("")]
57+
[InlineData(" ")]
58+
public async Task IntrospectTokenAsync_ThrowsArgumentException_WhenClientIdIsNullOrEmpty(string clientId)
59+
{
60+
// Arrange
61+
var request = new IntrospectTokenRequest
62+
{
63+
Token = "validToken",
64+
ClientId = clientId,
65+
ClientSecret = "validClientSecret"
66+
};
67+
68+
// Act & Assert
69+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => _authenticationClient.IntrospectTokenAsync(request));
70+
Assert.Equal("ClientId", exception.ParamName);
71+
Assert.Equal("ClientId must be provided. (Parameter 'ClientId')", exception.Message);
72+
}
73+
74+
[Theory]
75+
[InlineData(null)]
76+
[InlineData("")]
77+
[InlineData(" ")]
78+
public async Task IntrospectTokenAsync_ThrowsArgumentException_WhenClientSecretIsNullOrEmpty(string clientSecret)
79+
{
80+
// Arrange
81+
var request = new IntrospectTokenRequest
82+
{
83+
Token = "validToken",
84+
ClientId = "validClientId",
85+
ClientSecret = clientSecret
86+
};
87+
88+
// Act & Assert
89+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => _authenticationClient.IntrospectTokenAsync(request));
90+
Assert.Equal("ClientSecret", exception.ParamName);
91+
Assert.Equal("ClientSecret must be provided. (Parameter 'ClientSecret')", exception.Message);
92+
}
93+
94+
[Fact]
95+
public async Task IntrospectTokenAsync_CallsPostAsync_WithCorrectParameters()
96+
{
97+
// Arrange
98+
var introspectTokenRequest = new IntrospectTokenRequest
99+
{
100+
Token = "validToken",
101+
ClientId = "validClientId",
102+
ClientSecret = "validClientSecret"
103+
};
104+
105+
var expectedResponse = new IntrospectTokenResponse
106+
{
107+
IsActive = true,
108+
Scope = "read write",
109+
Iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
110+
};
111+
112+
_restClientMock
113+
.Setup(client => client.PostAsync<IntrospectTokenResponse>(
114+
It.Is<string>(url => url == ApiEndpoints.AuthenticationUrls.IntrospectToken()),
115+
It.IsAny<IIntrospectTokenBodyParameters>(),
116+
It.IsAny<IEnumerable<KeyValuePair<string, string>>>(),
117+
It.IsAny<IDictionary<string, string>>(),
118+
It.IsAny<JsonSerializerSettings>(),
119+
It.IsAny<IBasicAuthenticationParameters>(),
120+
It.IsAny<CancellationToken>()))
121+
.ReturnsAsync(expectedResponse);
122+
123+
// Act
124+
var response = await _authenticationClient.IntrospectTokenAsync(introspectTokenRequest);
125+
126+
// Assert
127+
Assert.NotNull(response);
128+
Assert.Equal(expectedResponse.IsActive, response.IsActive);
129+
Assert.Equal(expectedResponse.Scope, response.Scope);
130+
Assert.Equal(expectedResponse.Iat, response.Iat);
131+
_restClientMock.VerifyAll();
132+
}
133+
}

Test/Notion.UnitTests/Notion.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageReference Include="FluentAssertions" Version="5.10.3" />
1111
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
1212
<PackageReference Include="Microsoft.VisualStudio.VsixColorCompiler" Version="17.11.35325.10" />
13+
<PackageReference Include="Moq.AutoMock" Version="3.5.0" />
1314
<PackageReference Include="WireMock.Net" Version="1.4.19" />
1415
<PackageReference Include="xunit" Version="2.4.1" />
1516
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">

0 commit comments

Comments
 (0)