Skip to content

Commit 83e59c7

Browse files
authored
AzureB2C, IdentityServer 4 client credential flow support
* AzureB2C, IdentityServer 4 client credential flow support, refactored foundation, new tests
1 parent 6ac166e commit 83e59c7

26 files changed

+604
-215
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>0.1.7</Version>
3+
<Version>0.2.2</Version>
44
</PropertyGroup>
55
</Project>

README.md

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
Currently it supports next Identity providers and Oauth2 flows:
1111
- `Keycloak`: Library supports Keycloak [client credentials flow](https://tools.ietf.org/html/rfc6749#section-4.4) or `Protection API token (PAT)` flow. Additionally you can replace the PAT with user token by exchanging the token.
12+
- `Azure B2C`: Library supports [AzureB2C](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/) client credentials flow.
13+
- `Identity Server 4`: Library supports [Identity Server 4](https://identityserver.io/) client credentials [flow](https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html)
1214

1315
Supported .NET frameworks and standards: `netstandard2.0`, `netstandard2.1`, `netcoreapp3.1`, `net5.0`
1416

@@ -43,7 +45,7 @@ var auth = new KeycloakAuthenticator(options =>
4345
var token = await auth.GetAccessToken();
4446
```
4547

46-
### 1.2. Client credential flow with token exchange
48+
### 1.2. Exchange token for user token
4749

4850
If you want to replace the PAT token with user token, you can additionally specify a username. A mocked request looks like this:
4951

@@ -63,17 +65,53 @@ var auth = new KeycloakAuthenticator(options =>
6365
new Uri("https://my.keycloakserver.com/auth/realms/realmX/protocol/openid-connect/token"),
6466
"my_client",
6567
"client_secret");
66-
options.AddUserNameForImpersonation("myuser@users.com");
6768
});
6869

70+
//Get client credentials flow access token
71+
var token = await auth.GetAccessToken();
72+
//Replace client credentials flow token for user access token
73+
var userToken = await auth.ExchangeForUserToken("myuser@email.com");
74+
```
75+
76+
## 2. Identity Server 4 support
77+
78+
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Identity Server 4.
79+
80+
```csharp
81+
var auth = new IdentityServer4Authenticator(options =>
82+
{
83+
options.AddClientCredentialFlowParameters(
84+
new Uri("https://<myserver>/token"),
85+
"my_client"
86+
"<client_secret>");
87+
});
88+
89+
var token = await auth.GetAccessToken();
90+
```
91+
92+
## 3. Azure B2C support
93+
94+
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Azure B2C.
95+
96+
Azure B2C client credentials flow needs a defined scope which is usually `https://graph.windows.net/.default`.
97+
98+
```csharp
99+
var auth = new AzureB2CAuthenticator(options =>
100+
{
101+
options.AddClientCredentialFlowParameters(
102+
new Uri("https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token"),
103+
"<clientId>"
104+
"<clientSecret>"
105+
new string[] { "https://graph.windows.net/.default" });
106+
});
107+
69108
var token = await auth.GetAccessToken();
70109
```
71110

72111
## To-do
73112

74113
- **This library is an early alpha version**
75-
- Add more providers identity providers.
76-
- Add more OAuth2 flows.
114+
- Add Password flows to AzurteB2C and IdentityServer4.
77115

78116
## License
79117

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using NSubstitute;
2+
using QAToolKit.Auth.AzureB2C;
3+
using QAToolKit.Core.Interfaces;
4+
using System;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace QAToolKit.Auth.Test.AzureB2C
9+
{
10+
public class AzureB2CAuthenticatorTests
11+
{
12+
[Fact]
13+
public async Task CreateAuthenticatonServiceTest_Success()
14+
{
15+
var authenticator = Substitute.For<IAuthenticationService>();
16+
await authenticator.GetAccessToken();
17+
Assert.Single(authenticator.ReceivedCalls());
18+
}
19+
20+
[Fact]
21+
public async Task CreateAuthenticatonServiceWithReturnsTest_Success()
22+
{
23+
var authenticator = Substitute.For<IAuthenticationService>();
24+
authenticator.GetAccessToken().Returns(args => "12345");
25+
26+
Assert.Equal("12345", await authenticator.GetAccessToken());
27+
Assert.Single(authenticator.ReceivedCalls());
28+
}
29+
30+
[Fact]
31+
public void CreateAzureB2COptionsTest_Success()
32+
{
33+
var options = new AzureB2COptions();
34+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
35+
36+
var azureB2COptions = Substitute.For<Action<AzureB2COptions>>();
37+
azureB2COptions.Invoke(options);
38+
Assert.Single(azureB2COptions.ReceivedCalls());
39+
}
40+
}
41+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Microsoft.Extensions.Logging;
2+
using QAToolKit.Auth.AzureB2C;
3+
using System;
4+
using Xunit;
5+
using Xunit.Abstractions;
6+
7+
namespace QAToolKit.Auth.Test.AzureB2C
8+
{
9+
public class AzureB2COptionsTests
10+
{
11+
private readonly ILogger<AzureB2COptionsTests> _logger;
12+
13+
public AzureB2COptionsTests(ITestOutputHelper testOutputHelper)
14+
{
15+
var loggerFactory = new LoggerFactory();
16+
loggerFactory.AddProvider(new XunitLoggerProvider(testOutputHelper));
17+
_logger = loggerFactory.CreateLogger<AzureB2COptionsTests>();
18+
}
19+
20+
[Fact]
21+
public void KeycloakOptionsTest_Successful()
22+
{
23+
var options = new AzureB2COptions();
24+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
25+
26+
Assert.Equal("12345", options.ClientId);
27+
Assert.Equal("12345", options.Secret);
28+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
29+
}
30+
31+
[Fact]
32+
public void KeycloakOptionsNoImpersonationTest_Successful()
33+
{
34+
var options = new AzureB2COptions();
35+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
36+
37+
Assert.Equal("12345", options.ClientId);
38+
Assert.Equal("12345", options.Secret);
39+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
40+
}
41+
42+
[Theory]
43+
[InlineData("", "")]
44+
[InlineData(null, null)]
45+
[InlineData(null, "test")]
46+
[InlineData("test", null)]
47+
public void KeycloakOptionsUriNullTest_Fails(string clientId, string clientSecret)
48+
{
49+
var options = new AzureB2COptions();
50+
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(null, clientId, clientSecret));
51+
}
52+
53+
[Theory]
54+
[InlineData("", "")]
55+
[InlineData(null, null)]
56+
[InlineData(null, "test")]
57+
[InlineData("test", null)]
58+
public void KeycloakOptionsWrongUriTest_Fails(string clientId, string clientSecret)
59+
{
60+
var options = new AzureB2COptions();
61+
Assert.Throws<UriFormatException>(() => options.AddClientCredentialFlowParameters(new Uri("https"), clientId, clientSecret));
62+
}
63+
64+
[Theory]
65+
[InlineData("", "")]
66+
[InlineData(null, null)]
67+
[InlineData(null, "test")]
68+
[InlineData("test", null)]
69+
public void KeycloakOptionsCorrectUriTest_Fails(string clientId, string clientSecret)
70+
{
71+
var options = new AzureB2COptions();
72+
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret));
73+
}
74+
}
75+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
using System;
33
using Xunit;
44

5-
namespace QAToolKit.Auth.Test.Keycloak.Exceptions
5+
namespace QAToolKit.Auth.Test.Exceptions
66
{
77
public class KeycloakAccessDeniedExceptionTests : Exception
88
{
99
[Fact]
1010
public void CreateExceptionTest_Successful()
1111
{
12-
var exception = new KeycloakAccessDeniedException("my error");
12+
var exception = new AccessDeniedException("my error");
1313

1414
Assert.Equal("my error", exception.Message);
1515
}
@@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
1818
public void CreateExceptionWithInnerExceptionTest_Successful()
1919
{
2020
var innerException = new Exception("Inner");
21-
var exception = new KeycloakAccessDeniedException("my error", innerException);
21+
var exception = new AccessDeniedException("my error", innerException);
2222

2323
Assert.Equal("my error", exception.Message);
2424
Assert.Equal("Inner", innerException.Message);

src/QAToolKit.Auth.Test/Keycloak/Exceptions/KeycloakExceptionTests.cs renamed to src/QAToolKit.Auth.Test/Exceptions/AuthenticationExceptionTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
using System;
33
using Xunit;
44

5-
namespace QAToolKit.Auth.Test.Keycloak.Exceptions
5+
namespace QAToolKit.Auth.Test.Exceptions
66
{
77
public class KeycloakExceptionTests : Exception
88
{
99
[Fact]
1010
public void CreateExceptionTest_Successful()
1111
{
12-
var exception = new KeycloakException("my error");
12+
var exception = new AuthenticationException("my error");
1313

1414
Assert.Equal("my error", exception.Message);
1515
}
@@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
1818
public void CreateExceptionWithInnerExceptionTest_Successful()
1919
{
2020
var innerException = new Exception("Inner");
21-
var exception = new KeycloakException("my error", innerException);
21+
var exception = new AuthenticationException("my error", innerException);
2222

2323
Assert.Equal("my error", exception.Message);
2424
Assert.Equal("Inner", innerException.Message);
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
using System;
33
using Xunit;
44

5-
namespace QAToolKit.Auth.Test.Keycloak.Exceptions
5+
namespace QAToolKit.Auth.Test.Exceptions
66
{
7-
public class KeycloakUnauthorizedClientExceptionTests : Exception
7+
public class UnauthorizedClientExceptionTests : Exception
88
{
99
[Fact]
1010
public void CreateExceptionTest_Successful()
1111
{
12-
var exception = new KeycloakUnauthorizedClientException("my error");
12+
var exception = new UnauthorizedClientException("my error");
1313

1414
Assert.Equal("my error", exception.Message);
1515
}
@@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
1818
public void CreateExceptionWithInnerExceptionTest_Successful()
1919
{
2020
var innerException = new Exception("Inner");
21-
var exception = new KeycloakUnauthorizedClientException("my error", innerException);
21+
var exception = new UnauthorizedClientException("my error", innerException);
2222

2323
Assert.Equal("my error", exception.Message);
2424
Assert.Equal("Inner", innerException.Message);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using NSubstitute;
2+
using QAToolKit.Auth.IdentityServer4;
3+
using QAToolKit.Core.Interfaces;
4+
using System;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace QAToolKit.Auth.Test.IdentityServer4
9+
{
10+
public class IdentityServer4AuthenticatorTests
11+
{
12+
[Fact]
13+
public async Task CreateAuthenticatonServiceTest_Success()
14+
{
15+
var authenticator = Substitute.For<IAuthenticationService>();
16+
await authenticator.GetAccessToken();
17+
Assert.Single(authenticator.ReceivedCalls());
18+
}
19+
20+
[Fact]
21+
public async Task CreateAuthenticatonServiceWithReturnsTest_Success()
22+
{
23+
var authenticator = Substitute.For<IAuthenticationService>();
24+
authenticator.GetAccessToken().Returns(args => "12345");
25+
26+
Assert.Equal("12345", await authenticator.GetAccessToken());
27+
Assert.Single(authenticator.ReceivedCalls());
28+
}
29+
30+
[Fact]
31+
public void CreateIdentityServer4OptionsTest_Success()
32+
{
33+
var options = new IdentityServer4Options();
34+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
35+
36+
var id4Options = Substitute.For<Action<IdentityServer4Options>>();
37+
id4Options.Invoke(options);
38+
Assert.Single(id4Options.ReceivedCalls());
39+
}
40+
}
41+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Microsoft.Extensions.Logging;
2+
using QAToolKit.Auth.IdentityServer4;
3+
using System;
4+
using Xunit;
5+
using Xunit.Abstractions;
6+
7+
namespace QAToolKit.Auth.Test.IdentityServer4
8+
{
9+
public class IdentityServer4OptionsTests
10+
{
11+
private readonly ILogger<IdentityServer4OptionsTests> _logger;
12+
13+
public IdentityServer4OptionsTests(ITestOutputHelper testOutputHelper)
14+
{
15+
var loggerFactory = new LoggerFactory();
16+
loggerFactory.AddProvider(new XunitLoggerProvider(testOutputHelper));
17+
_logger = loggerFactory.CreateLogger<IdentityServer4OptionsTests>();
18+
}
19+
20+
[Fact]
21+
public void KeycloakOptionsTest_Successful()
22+
{
23+
var options = new IdentityServer4Options();
24+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
25+
26+
Assert.Equal("12345", options.ClientId);
27+
Assert.Equal("12345", options.Secret);
28+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
29+
}
30+
31+
[Fact]
32+
public void KeycloakOptionsNoImpersonationTest_Successful()
33+
{
34+
var options = new IdentityServer4Options();
35+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
36+
37+
Assert.Equal("12345", options.ClientId);
38+
Assert.Equal("12345", options.Secret);
39+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
40+
}
41+
42+
[Theory]
43+
[InlineData("", "")]
44+
[InlineData(null, null)]
45+
[InlineData(null, "test")]
46+
[InlineData("test", null)]
47+
public void KeycloakOptionsUriNullTest_Fails(string clientId, string clientSecret)
48+
{
49+
var options = new IdentityServer4Options();
50+
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(null, clientId, clientSecret));
51+
}
52+
53+
[Theory]
54+
[InlineData("", "")]
55+
[InlineData(null, null)]
56+
[InlineData(null, "test")]
57+
[InlineData("test", null)]
58+
public void KeycloakOptionsWrongUriTest_Fails(string clientId, string clientSecret)
59+
{
60+
var options = new IdentityServer4Options();
61+
Assert.Throws<UriFormatException>(() => options.AddClientCredentialFlowParameters(new Uri("https"), clientId, clientSecret));
62+
}
63+
64+
[Theory]
65+
[InlineData("", "")]
66+
[InlineData(null, null)]
67+
[InlineData(null, "test")]
68+
[InlineData("test", null)]
69+
public void KeycloakOptionsCorrectUriTest_Fails(string clientId, string clientSecret)
70+
{
71+
var options = new IdentityServer4Options();
72+
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret));
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)