Skip to content

Commit

Permalink
AzureB2C, IdentityServer 4 client credential flow support
Browse files Browse the repository at this point in the history
* AzureB2C, IdentityServer 4 client credential flow support, refactored foundation, new tests
  • Loading branch information
mihaj authored Dec 9, 2020
1 parent 6ac166e commit 83e59c7
Show file tree
Hide file tree
Showing 26 changed files with 604 additions and 215 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>0.1.7</Version>
<Version>0.2.2</Version>
</PropertyGroup>
</Project>
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

Currently it supports next Identity providers and Oauth2 flows:
- `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.
- `Azure B2C`: Library supports [AzureB2C](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/) client credentials flow.
- `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)

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

Expand Down Expand Up @@ -43,7 +45,7 @@ var auth = new KeycloakAuthenticator(options =>
var token = await auth.GetAccessToken();
```

### 1.2. Client credential flow with token exchange
### 1.2. Exchange token for user token

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

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

//Get client credentials flow access token
var token = await auth.GetAccessToken();
//Replace client credentials flow token for user access token
var userToken = await auth.ExchangeForUserToken("myuser@email.com");
```

## 2. Identity Server 4 support

Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Identity Server 4.

```csharp
var auth = new IdentityServer4Authenticator(options =>
{
options.AddClientCredentialFlowParameters(
new Uri("https://<myserver>/token"),
"my_client"
"<client_secret>");
});

var token = await auth.GetAccessToken();
```

## 3. Azure B2C support

Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Azure B2C.

Azure B2C client credentials flow needs a defined scope which is usually `https://graph.windows.net/.default`.

```csharp
var auth = new AzureB2CAuthenticator(options =>
{
options.AddClientCredentialFlowParameters(
new Uri("https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token"),
"<clientId>"
"<clientSecret>"
new string[] { "https://graph.windows.net/.default" });
});

var token = await auth.GetAccessToken();
```

## To-do

- **This library is an early alpha version**
- Add more providers identity providers.
- Add more OAuth2 flows.
- Add Password flows to AzurteB2C and IdentityServer4.

## License

Expand Down
41 changes: 41 additions & 0 deletions src/QAToolKit.Auth.Test/AzureB2C/AzureB2CAuthenticatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using NSubstitute;
using QAToolKit.Auth.AzureB2C;
using QAToolKit.Core.Interfaces;
using System;
using System.Threading.Tasks;
using Xunit;

namespace QAToolKit.Auth.Test.AzureB2C
{
public class AzureB2CAuthenticatorTests
{
[Fact]
public async Task CreateAuthenticatonServiceTest_Success()
{
var authenticator = Substitute.For<IAuthenticationService>();
await authenticator.GetAccessToken();
Assert.Single(authenticator.ReceivedCalls());
}

[Fact]
public async Task CreateAuthenticatonServiceWithReturnsTest_Success()
{
var authenticator = Substitute.For<IAuthenticationService>();
authenticator.GetAccessToken().Returns(args => "12345");

Assert.Equal("12345", await authenticator.GetAccessToken());
Assert.Single(authenticator.ReceivedCalls());
}

[Fact]
public void CreateAzureB2COptionsTest_Success()
{
var options = new AzureB2COptions();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

var azureB2COptions = Substitute.For<Action<AzureB2COptions>>();
azureB2COptions.Invoke(options);
Assert.Single(azureB2COptions.ReceivedCalls());
}
}
}
75 changes: 75 additions & 0 deletions src/QAToolKit.Auth.Test/AzureB2C/AzureB2COptionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Microsoft.Extensions.Logging;
using QAToolKit.Auth.AzureB2C;
using System;
using Xunit;
using Xunit.Abstractions;

namespace QAToolKit.Auth.Test.AzureB2C
{
public class AzureB2COptionsTests
{
private readonly ILogger<AzureB2COptionsTests> _logger;

public AzureB2COptionsTests(ITestOutputHelper testOutputHelper)
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(new XunitLoggerProvider(testOutputHelper));
_logger = loggerFactory.CreateLogger<AzureB2COptionsTests>();
}

[Fact]
public void KeycloakOptionsTest_Successful()
{
var options = new AzureB2COptions();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

Assert.Equal("12345", options.ClientId);
Assert.Equal("12345", options.Secret);
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
}

[Fact]
public void KeycloakOptionsNoImpersonationTest_Successful()
{
var options = new AzureB2COptions();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

Assert.Equal("12345", options.ClientId);
Assert.Equal("12345", options.Secret);
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsUriNullTest_Fails(string clientId, string clientSecret)
{
var options = new AzureB2COptions();
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(null, clientId, clientSecret));
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsWrongUriTest_Fails(string clientId, string clientSecret)
{
var options = new AzureB2COptions();
Assert.Throws<UriFormatException>(() => options.AddClientCredentialFlowParameters(new Uri("https"), clientId, clientSecret));
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsCorrectUriTest_Fails(string clientId, string clientSecret)
{
var options = new AzureB2COptions();
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
using System;
using Xunit;

namespace QAToolKit.Auth.Test.Keycloak.Exceptions
namespace QAToolKit.Auth.Test.Exceptions
{
public class KeycloakAccessDeniedExceptionTests : Exception
{
[Fact]
public void CreateExceptionTest_Successful()
{
var exception = new KeycloakAccessDeniedException("my error");
var exception = new AccessDeniedException("my error");

Assert.Equal("my error", exception.Message);
}
Expand All @@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
public void CreateExceptionWithInnerExceptionTest_Successful()
{
var innerException = new Exception("Inner");
var exception = new KeycloakAccessDeniedException("my error", innerException);
var exception = new AccessDeniedException("my error", innerException);

Assert.Equal("my error", exception.Message);
Assert.Equal("Inner", innerException.Message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
using System;
using Xunit;

namespace QAToolKit.Auth.Test.Keycloak.Exceptions
namespace QAToolKit.Auth.Test.Exceptions
{
public class KeycloakExceptionTests : Exception
{
[Fact]
public void CreateExceptionTest_Successful()
{
var exception = new KeycloakException("my error");
var exception = new AuthenticationException("my error");

Assert.Equal("my error", exception.Message);
}
Expand All @@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
public void CreateExceptionWithInnerExceptionTest_Successful()
{
var innerException = new Exception("Inner");
var exception = new KeycloakException("my error", innerException);
var exception = new AuthenticationException("my error", innerException);

Assert.Equal("my error", exception.Message);
Assert.Equal("Inner", innerException.Message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
using System;
using Xunit;

namespace QAToolKit.Auth.Test.Keycloak.Exceptions
namespace QAToolKit.Auth.Test.Exceptions
{
public class KeycloakUnauthorizedClientExceptionTests : Exception
public class UnauthorizedClientExceptionTests : Exception
{
[Fact]
public void CreateExceptionTest_Successful()
{
var exception = new KeycloakUnauthorizedClientException("my error");
var exception = new UnauthorizedClientException("my error");

Assert.Equal("my error", exception.Message);
}
Expand All @@ -18,7 +18,7 @@ public void CreateExceptionTest_Successful()
public void CreateExceptionWithInnerExceptionTest_Successful()
{
var innerException = new Exception("Inner");
var exception = new KeycloakUnauthorizedClientException("my error", innerException);
var exception = new UnauthorizedClientException("my error", innerException);

Assert.Equal("my error", exception.Message);
Assert.Equal("Inner", innerException.Message);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using NSubstitute;
using QAToolKit.Auth.IdentityServer4;
using QAToolKit.Core.Interfaces;
using System;
using System.Threading.Tasks;
using Xunit;

namespace QAToolKit.Auth.Test.IdentityServer4
{
public class IdentityServer4AuthenticatorTests
{
[Fact]
public async Task CreateAuthenticatonServiceTest_Success()
{
var authenticator = Substitute.For<IAuthenticationService>();
await authenticator.GetAccessToken();
Assert.Single(authenticator.ReceivedCalls());
}

[Fact]
public async Task CreateAuthenticatonServiceWithReturnsTest_Success()
{
var authenticator = Substitute.For<IAuthenticationService>();
authenticator.GetAccessToken().Returns(args => "12345");

Assert.Equal("12345", await authenticator.GetAccessToken());
Assert.Single(authenticator.ReceivedCalls());
}

[Fact]
public void CreateIdentityServer4OptionsTest_Success()
{
var options = new IdentityServer4Options();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

var id4Options = Substitute.For<Action<IdentityServer4Options>>();
id4Options.Invoke(options);
Assert.Single(id4Options.ReceivedCalls());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Microsoft.Extensions.Logging;
using QAToolKit.Auth.IdentityServer4;
using System;
using Xunit;
using Xunit.Abstractions;

namespace QAToolKit.Auth.Test.IdentityServer4
{
public class IdentityServer4OptionsTests
{
private readonly ILogger<IdentityServer4OptionsTests> _logger;

public IdentityServer4OptionsTests(ITestOutputHelper testOutputHelper)
{
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(new XunitLoggerProvider(testOutputHelper));
_logger = loggerFactory.CreateLogger<IdentityServer4OptionsTests>();
}

[Fact]
public void KeycloakOptionsTest_Successful()
{
var options = new IdentityServer4Options();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

Assert.Equal("12345", options.ClientId);
Assert.Equal("12345", options.Secret);
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
}

[Fact]
public void KeycloakOptionsNoImpersonationTest_Successful()
{
var options = new IdentityServer4Options();
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");

Assert.Equal("12345", options.ClientId);
Assert.Equal("12345", options.Secret);
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsUriNullTest_Fails(string clientId, string clientSecret)
{
var options = new IdentityServer4Options();
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(null, clientId, clientSecret));
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsWrongUriTest_Fails(string clientId, string clientSecret)
{
var options = new IdentityServer4Options();
Assert.Throws<UriFormatException>(() => options.AddClientCredentialFlowParameters(new Uri("https"), clientId, clientSecret));
}

[Theory]
[InlineData("", "")]
[InlineData(null, null)]
[InlineData(null, "test")]
[InlineData("test", null)]
public void KeycloakOptionsCorrectUriTest_Fails(string clientId, string clientSecret)
{
var options = new IdentityServer4Options();
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret));
}
}
}
Loading

0 comments on commit 83e59c7

Please sign in to comment.