Skip to content

Commit 4840637

Browse files
authored
Implementation of ROPC Oauth2 flow
1 parent 9720297 commit 4840637

12 files changed

+274
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,4 @@ MigrationBackup/
361361
# Fody - auto-generated XML schema
362362
FodyWeavers.xsd
363363
/src/QAToolKit.Core.Test/global.json
364+
/.idea

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.2.3</Version>
3+
<Version>0.2.6</Version>
44
</PropertyGroup>
55
</Project>

README.md

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
[![Discord](https://img.shields.io/discord/787220825127780354?color=%23267CB9&label=Discord%20chat)](https://discord.gg/hYs6ayYQC5)
77

88
## Description
9-
`QAToolKit.Auth` is a .NET Standard 2.1 library, that retrieves the JWT access tokens from different identity providers.
9+
`QAToolKit.Auth` is a .NET Standard 2.1 library, that retrieves the JWT access tokens from different identity providers. This library should only be used for testing software and not in applications to retrieve the token.
1010

1111
Currently it supports next Identity providers and Oauth2 flows:
12-
- `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.
13-
- `Azure B2C`: Library supports [AzureB2C](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/) client credentials flow.
14-
- `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)
12+
- `Keycloak`: Library supports:
13+
- [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.
14+
- [resource owner password credentials grant](https://www.appsdeveloperblog.com/keycloak-requesting-token-with-password-grant/)
15+
- `Azure B2C`: Library supports:
16+
- [client credentials flow](https://azure.microsoft.com/en-us/services/active-directory/external-identities/b2c/)
17+
- [resource owner password credentials flow](https://docs.microsoft.com/en-us/azure/active-directory-b2c/add-ropc-policy?tabs=app-reg-ga&pivots=b2c-user-flow)
18+
- `Identity Server 4`: Library supports:
19+
- [client credentials flow](https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html)
20+
- [resource owner password]()
1521

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

@@ -23,7 +29,7 @@ Get in touch with me on:
2329

2430
## 1. Keycloak support
2531

26-
Keycloak support is limited to the `client credential` or `Protection API token (PAT)` flow in combination with `token exchange`.
32+
Keycloak support is limited to the `client credential` or `Protection API token (PAT)` flow in combination with `token exchange`. Additionally you can also get the token by using `resource owner password credentials grant`.
2733

2834
### 1.1. Client credential flow
2935

@@ -38,7 +44,7 @@ curl -X POST \
3844

3945
Read [more](https://www.keycloak.org/docs/latest/authorization_services/#_service_protection_whatis_obtain_pat) here in the Keycloak documentation.
4046

41-
Now let's retrive a PAT token with QAToolKit Auth libraray:
47+
Now let's retrive a PAT token with QAToolKit Auth library:
4248

4349
```csharp
4450
var auth = new KeycloakAuthenticator(options =>
@@ -80,9 +86,25 @@ var token = await auth.GetAccessToken();
8086
var userToken = await auth.ExchangeForUserToken("[email protected]");
8187
```
8288

89+
### 1.3. Resource owner password credentials grant
90+
91+
```csharp
92+
var auth = new KeycloakAuthenticator(options =>
93+
{
94+
options.AddResourceOwnerPasswordCredentialFlowParameters(
95+
new Uri("https://my.keycloakserver.com/auth/realms/realmX/protocol/openid-connect/token"),
96+
"my_client",
97+
"client_secret",
98+
"user",
99+
"pass");
100+
});
101+
102+
var token = await auth.GetAccessToken();
103+
```
104+
83105
## 2. Identity Server 4 support
84106

85-
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Identity Server 4.
107+
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Identity Server 4. Additionally you can also get the token by using `resource owner password`.
86108

87109
```csharp
88110
var auth = new IdentityServer4Authenticator(options =>
@@ -96,9 +118,25 @@ var auth = new IdentityServer4Authenticator(options =>
96118
var token = await auth.GetAccessToken();
97119
```
98120

121+
#### Resource owner password
122+
123+
```csharp
124+
var auth = new IdentityServer4Authenticator(options =>
125+
{
126+
options.AddResourceOwnerPasswordCredentialFlowParameters(
127+
new Uri("https://<myserver>/token"),
128+
"my_client"
129+
"<client_secret>",
130+
"user",
131+
"pass");
132+
});
133+
134+
var token = await auth.GetAccessToken();
135+
```
136+
99137
## 3. Azure B2C support
100138

101-
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Azure B2C.
139+
Under the hood it's the same code that retrieves the `client credentials flow` access token, but authenticator is explicit for Azure B2C. Additionally you can also get the token by using `resource owner password credentials flow`.
102140

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

@@ -115,10 +153,26 @@ var auth = new AzureB2CAuthenticator(options =>
115153
var token = await auth.GetAccessToken();
116154
```
117155

156+
#### Resource owner password credentials flow
157+
158+
```csharp
159+
var auth = new AzureB2CAuthenticator(options =>
160+
{
161+
options.AddResourceOwnerPasswordCredentialFlowParameters(
162+
new Uri("https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token"),
163+
"<clientId>"
164+
"<clientSecret>"
165+
new string[] { "https://graph.windows.net/.default" },
166+
"user",
167+
"pass");
168+
});
169+
170+
var token = await auth.GetAccessToken();
171+
```
172+
118173
## To-do
119174

120175
- **This library is an early alpha version**
121-
- Add Password flows to AzurteB2C and IdentityServer4.
122176

123177
## License
124178

src/QAToolKit.Auth.Test/AzureB2C/AzureB2CAuthenticatorTests.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ namespace QAToolKit.Auth.Test.AzureB2C
1010
public class AzureB2CAuthenticatorTests
1111
{
1212
[Fact]
13-
public async Task CreateAuthenticatonServiceTest_Success()
13+
public async Task CreateAuthenticatorServiceTest_Success()
1414
{
1515
var authenticator = Substitute.For<IAuthenticationService>();
1616
await authenticator.GetAccessToken();
1717
Assert.Single(authenticator.ReceivedCalls());
1818
}
1919

2020
[Fact]
21-
public async Task CreateAuthenticatonServiceWithReturnsTest_Success()
21+
public async Task CreateAuthenticatorServiceWithReturnsTest_Success()
2222
{
2323
var authenticator = Substitute.For<IAuthenticationService>();
2424
authenticator.GetAccessToken().Returns(args => "12345");
@@ -37,5 +37,17 @@ public void CreateAzureB2COptionsTest_Success()
3737
azureB2COptions.Invoke(options);
3838
Assert.Single(azureB2COptions.ReceivedCalls());
3939
}
40+
41+
[Fact]
42+
public void CreateAzureB2CROPCOptionsTest_Success()
43+
{
44+
var options = new AzureB2COptions();
45+
options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345",
46+
"user", "pass");
47+
48+
var azureB2COptions = Substitute.For<Action<AzureB2COptions>>();
49+
azureB2COptions.Invoke(options);
50+
Assert.Single(azureB2COptions.ReceivedCalls());
51+
}
4052
}
4153
}

src/QAToolKit.Auth.Test/IdentityServer4/IdentityServer4AuthenticatorTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,17 @@ public void CreateIdentityServer4OptionsTest_Success()
3737
id4Options.Invoke(options);
3838
Assert.Single(id4Options.ReceivedCalls());
3939
}
40+
41+
[Fact]
42+
public void CreateIdentityServer4ROPCOptionsTest_Success()
43+
{
44+
var options = new IdentityServer4Options();
45+
options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345",
46+
"user", "pass");
47+
48+
var id4Options = Substitute.For<Action<IdentityServer4Options>>();
49+
id4Options.Invoke(options);
50+
Assert.Single(id4Options.ReceivedCalls());
51+
}
4052
}
4153
}

src/QAToolKit.Auth.Test/IdentityServer4/IdentityServer4OptionsTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ public void KeycloakOptionsTest_Successful()
2626
Assert.Equal("12345", options.ClientId);
2727
Assert.Equal("12345", options.Secret);
2828
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
29+
Assert.True(options.FlowType == FlowType.ClientCredentialFlow);
30+
Assert.Null(options.UserName);
31+
Assert.Null(options.Password);
32+
}
33+
34+
[Fact]
35+
public void KeycloakOptionsROPCTest_Successful()
36+
{
37+
var options = new IdentityServer4Options();
38+
options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345",
39+
"user","pass");
40+
41+
Assert.Equal("12345", options.ClientId);
42+
Assert.Equal("12345", options.Secret);
43+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
44+
Assert.True(options.FlowType == FlowType.ResourceOwnerPasswordCredentialFlow);
45+
Assert.Equal("12345", options.ClientId);
46+
Assert.Equal("12345", options.Secret);
2947
}
3048

3149
[Fact]
@@ -71,5 +89,38 @@ public void KeycloakOptionsCorrectUriTest_Fails(string clientId, string clientSe
7189
var options = new IdentityServer4Options();
7290
Assert.Throws<ArgumentNullException>(() => options.AddClientCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret));
7391
}
92+
93+
[Theory]
94+
[InlineData("", "", "", "")]
95+
[InlineData(null, null, null, null)]
96+
[InlineData(null, "test", null, "geslo")]
97+
[InlineData("test", null, null, "")]
98+
public void KeycloakOptionsROPCUriNullTest_Fails(string clientId, string clientSecret, string username, string password)
99+
{
100+
var options = new IdentityServer4Options();
101+
Assert.Throws<ArgumentNullException>(() => options.AddResourceOwnerPasswordCredentialFlowParameters(null, clientId, clientSecret, username, password));
102+
}
103+
104+
[Theory]
105+
[InlineData("", "", "", "")]
106+
[InlineData(null, null, null, null)]
107+
[InlineData(null, "test", null, "geslo")]
108+
[InlineData("test", null, null, "geslo")]
109+
public void KeycloakOptionsROPCWrongUriTest_Fails(string clientId, string clientSecret, string username, string password)
110+
{
111+
var options = new IdentityServer4Options();
112+
Assert.Throws<UriFormatException>(() => options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https"), clientId, clientSecret, username, password));
113+
}
114+
115+
[Theory]
116+
[InlineData("", "", "", "")]
117+
[InlineData(null, null, null, null)]
118+
[InlineData(null, "test", null, "geslo")]
119+
[InlineData("test", null, "user", null)]
120+
public void KeycloakOptionsROPCCorrectUriTest_Fails(string clientId, string clientSecret, string username, string password)
121+
{
122+
var options = new IdentityServer4Options();
123+
Assert.Throws<ArgumentNullException>(() => options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https://localhost/token"), clientId, clientSecret, username, password));
124+
}
74125
}
75126
}

src/QAToolKit.Auth.Test/Keycloak/KeycloakAuthenticatorTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,17 @@ public void CreateKeycloakOptionsTest_Success()
3838
keycloakOptions.Invoke(options);
3939
Assert.Single(keycloakOptions.ReceivedCalls());
4040
}
41+
42+
[Fact]
43+
public void CreateKeycloakROPCOptionsTest_Success()
44+
{
45+
var options = new KeycloakOptions();
46+
options.AddResourceOwnerPasswordCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345",
47+
"user","pass");
48+
49+
var keycloakOptions = Substitute.For<Action<KeycloakOptions>>();
50+
keycloakOptions.Invoke(options);
51+
Assert.Single(keycloakOptions.ReceivedCalls());
52+
}
4153
}
4254
}

src/QAToolKit.Auth.Test/QAToolKit.Auth.Test.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="coverlet.msbuild" Version="2.9.0">
10+
<PackageReference Include="coverlet.msbuild" Version="3.0.3">
1111
<PrivateAssets>all</PrivateAssets>
1212
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1313
</PackageReference>
1414
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
1515
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
16-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
16+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
1717
<PackageReference Include="NSubstitute" Version="4.2.2" />
1818
<PackageReference Include="xunit" Version="2.4.1" />
1919
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
2020
<PrivateAssets>all</PrivateAssets>
2121
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2222
</PackageReference>
23-
<PackageReference Include="coverlet.collector" Version="1.3.0">
23+
<PackageReference Include="coverlet.collector" Version="3.0.3">
2424
<PrivateAssets>all</PrivateAssets>
2525
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2626
</PackageReference>

src/QAToolKit.Auth/DefaultOptions.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@ public abstract class DefaultOptions
1010
/// <summary>
1111
/// Keycloak token endpoint you want to call
1212
/// </summary>
13-
public Uri TokenEndpoint { get; set; }
13+
public Uri TokenEndpoint { get; private set; }
1414
/// <summary>
1515
/// Keycloak client ID
1616
/// </summary>
17-
public string ClientId { get; set; }
17+
public string ClientId { get; private set; }
1818
/// <summary>
1919
/// Keycloak client secret
2020
/// </summary>
21-
public string Secret { get; set; }
21+
public string Secret { get; private set; }
2222
/// <summary>
2323
/// Scopes that client has access to
2424
/// </summary>
25-
public string[] Scopes { get; set; } = null;
25+
public string[] Scopes { get; private set; } = null;
26+
/// <summary>
27+
/// Username for ROPC flow
28+
/// </summary>
29+
public string UserName { get; private set; }
30+
/// <summary>
31+
/// User password for ROPC flow
32+
/// </summary>
33+
public string Password { get; private set; }
34+
/// <summary>
35+
/// Oauth2 flow type
36+
/// </summary>
37+
public FlowType FlowType { get; private set; }
2638

2739
/// <summary>
2840
/// Add client credential flow parameters
@@ -45,6 +57,44 @@ public virtual DefaultOptions AddClientCredentialFlowParameters(Uri tokenEndpoin
4557
ClientId = clientId;
4658
Secret = clientSecret;
4759
Scopes = scopes;
60+
UserName = null;
61+
Password = null;
62+
FlowType = FlowType.ClientCredentialFlow;
63+
return this;
64+
}
65+
66+
/// <summary>
67+
/// Add resource owner password credential flow parameters
68+
/// </summary>
69+
/// <param name="tokenEndpoint">Keycloak token endpoint</param>
70+
/// <param name="clientId">Keycloak client ID</param>
71+
/// <param name="clientSecret">Keycloak client secret</param>
72+
/// <param name="userName">Username</param>
73+
/// <param name="password">Password</param>
74+
/// <param name="scopes">Scopes that client has access to</param>
75+
/// <returns></returns>
76+
/// <exception cref="ArgumentNullException"></exception>
77+
public virtual DefaultOptions AddResourceOwnerPasswordCredentialFlowParameters(Uri tokenEndpoint, string clientId, string clientSecret,
78+
string userName, string password, string[] scopes = null)
79+
{
80+
if (tokenEndpoint == null)
81+
throw new ArgumentNullException($"{nameof(tokenEndpoint)} is null.");
82+
if (string.IsNullOrEmpty(clientId))
83+
throw new ArgumentNullException($"{nameof(clientId)} is null.");
84+
if (string.IsNullOrEmpty(clientSecret))
85+
throw new ArgumentNullException($"{nameof(clientSecret)} is null.");
86+
if (string.IsNullOrEmpty(userName))
87+
throw new ArgumentNullException($"{nameof(userName)} is null.");
88+
if (string.IsNullOrEmpty(password))
89+
throw new ArgumentNullException($"{nameof(password)} is null.");
90+
91+
TokenEndpoint = tokenEndpoint;
92+
ClientId = clientId;
93+
Secret = clientSecret;
94+
Scopes = scopes;
95+
UserName = userName;
96+
Password = password;
97+
FlowType = FlowType.ResourceOwnerPasswordCredentialFlow;
4898
return this;
4999
}
50100
}

0 commit comments

Comments
 (0)