Skip to content

Commit b98c34c

Browse files
authored
API changed a bit before release, readmefile, version bump
1 parent 29f2183 commit b98c34c

File tree

5 files changed

+75
-74
lines changed

5 files changed

+75
-74
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.6</Version>
3+
<Version>0.1.7</Version>
44
</PropertyGroup>
55
</Project>

README.md

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,79 @@
11
# QAToolKit Authentication library
2-
[![Build .NET Library](https://github.com/qatoolkit/qatoolkit-auth-net/workflows/.NET%20Core/badge.svg?branch=main)](https://github.com/qatoolkit/qatoolkit-auth-net/actions)
2+
[![Build .NET Library](https://github.com/qatoolkit/qatoolkit-auth-net/workflows/Build%20.NET%20Library/badge.svg)](https://github.com/qatoolkit/qatoolkit-auth-net/actions)
33
[![CodeQL](https://github.com/qatoolkit/qatoolkit-auth-net/workflows/CodeQL%20Analyze/badge.svg)](https://github.com/qatoolkit/qatoolkit-auth-net/security/code-scanning)
44
[![Sonarcloud Quality gate](https://github.com/qatoolkit/qatoolkit-auth-net/workflows/Sonarqube%20Analyze/badge.svg)](https://sonarcloud.io/dashboard?id=qatoolkit_qatoolkit-auth-net)
55
[![NuGet package](https://img.shields.io/nuget/v/QAToolKit.Auth?label=QAToolKit.Auth)](https://www.nuget.org/packages/QAToolKit.Auth/)
66

77
## Description
8-
`QAToolKit.Auth` is a .NET Standard 2.1 library, that contains core objects and functions of the toolkit. It's normally not used as a standalone library but is a dependency for other QAToolKit libraries.
8+
`QAToolKit.Auth` is a .NET Standard 2.1 library, that retrieves the JWT access tokens from different identity providers.
9+
10+
Currently it supports next Identity providers and Oauth2 flows:
11+
- `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.
912

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

12-
## 1. HttpRequest functions
13-
HttpRequest object is one of the main objects that is shared among the QA Toolkit libraries. `QAToolKit.Core` library contains `HttpRequestTools` which can manipulate the HttpRequest object.
15+
## 1. Keycloak support
1416

15-
For example URL, header and Body generators: `HttpRequestUrlGenerator`, `HttpRequestBodyGenerator` and `HttpRequestHeaderGenerator`.
17+
Keycloak support is limited to the `client credential` or `Protection API token (PAT)` flow in combination with `token exchange`.
1618

17-
### 1.1. HttpRequestUrlGenerator
18-
This is a method that will accept key/value pairs for replacement of placeholders in the `HttpRequest` object. Replacement object are stored in a dictionary, which `prevents mistakes with duplicated keys`.
19-
Also dictionary keys are case insensitive when looking for values to replace.
19+
### 1.1. Client credential flow
2020

21-
```csharp
22-
options.AddReplacementValues(new Dictionary<string, object> {
23-
{
24-
"version",
25-
"1"
26-
},
27-
{
28-
"parentId",
29-
"4"
30-
}
31-
});
21+
A mocked request below is sent to the Keycloak endpoint, and the PAT token is retrieved:
22+
23+
```bash
24+
curl -X POST \
25+
-H "Content-Type: application/x-www-form-urlencoded" \
26+
-d 'grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}' \
27+
"http://localhost:8080/auth/realms/${realm_name}/protocol/openid-connect/token"
3228
```
3329

34-
In the example above we say: "Replace `{version}` and {parentId} placeholders in Path and URL parameters and JSON body models."
30+
Read [more](https://www.keycloak.org/docs/latest/authorization_services/#_service_protection_whatis_obtain_pat) here in the Keycloak documentation.
3531

36-
In other words, if you have a test API endpoint like this: https://api.demo.com/v{version}/categories?parent={parentId} that will be set to https://api.demo.com/v1/categories?parent=4.
32+
Now let's retrive a PAT token with QAToolKit Auth libraray:
3733

38-
That, does not stop there, you can also populate JSON request bodies.
34+
```csharp
35+
var auth = new KeycloakAuthenticator(options =>
36+
{
37+
options.AddClientCredentialFlowParameters(
38+
new Uri("https://my.keycloakserver.com/auth/realms/realmX/protocol/openid-connect/token"),
39+
"my_client",
40+
"client_secret");
41+
});
3942

40-
### 1.2. HttpRequestBodyGenerator
43+
var token = await auth.GetAccessToken();
44+
```
4145

42-
For example if you set the replacement value to stringified json:
46+
### 1.2. Client credential flow with token exchange
4347

44-
```csharp
45-
options.AddReplacementValues(new Dictionary<string, object> {
46-
{
47-
"id",
48-
"100"
49-
},
50-
{
51-
"category",
52-
"{\"id\":1,\"name\":\"dog\"}"
53-
}
54-
});
48+
If you want to replace the PAT token with user token, you can additionally specify a username. A mocked request looks like this:
49+
50+
```bash
51+
curl -X POST \
52+
-H "Content-Type: application/x-www-form-urlencoded" \
53+
-d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id=${client_id}&client_secret=${client_secret}&subject_token=eyJhbGciOiJI...&requested_subject=myuser@users.com' \
54+
"http://localhost:8080/auth/realms/${realm_name}/protocol/openid-connect/token"
5555
```
56-
than the parent model object will be replaced with the stringified json above.
5756

58-
What happend behind the curtains, the model proxy class is generated, which is then used to Deserialized the JSON into the object.
59-
`HttpRequest` list is then scanned and values are properly replaced.
57+
As you see it has a different `grant_type` and additionally 2 more properties in the URL; PAT token (`subject_token`) and userName for which we want to replace the token (`requested_subject`).
6058

61-
### 1.3. HttpRequestHeaderGenerator
59+
```csharp
60+
var auth = new KeycloakAuthenticator(options =>
61+
{
62+
options.AddClientCredentialFlowParameters(
63+
new Uri("https://my.keycloakserver.com/auth/realms/realmX/protocol/openid-connect/token"),
64+
"my_client",
65+
"client_secret");
66+
options.AddUserNameForImpersonation("myuser@users.com");
67+
});
6268

63-
To-do
69+
var token = await auth.GetAccessToken();
70+
```
6471

6572
## To-do
6673

6774
- **This library is an early alpha version**
68-
- `HttpRequestHeaderGenerator` is missing implementation.
69-
- `HttpRequestBodyGenerator` need to cover the whole spectrum of object tipes. Currently it's missing arrays, and nested objects. It's on the priority list.
75+
- Add more providers identity providers.
76+
- Add more OAuth2 flows.
7077

7178
## License
7279

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ public void KeycloakOptionsTest_Successful()
2727
Assert.Equal("12345", options.ClientId);
2828
Assert.Equal("12345", options.Secret);
2929
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
30+
Assert.True(options.UseImpersonation);
31+
}
32+
33+
[Fact]
34+
public void KeycloakOptionsNoImpersonationTest_Successful()
35+
{
36+
var options = new KeycloakOptions();
37+
options.AddClientCredentialFlowParameters(new Uri("https://api.com/token"), "12345", "12345");
38+
39+
Assert.Equal("12345", options.ClientId);
40+
Assert.Equal("12345", options.Secret);
41+
Assert.Equal(new Uri("https://api.com/token"), options.TokenEndpoint);
42+
Assert.False(options.UseImpersonation);
3043
}
3144

3245
[Theory]

src/QAToolKit.Auth/Keycloak/KeycloakOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public class KeycloakOptions
2323
/// Username / email of the user for which you want to retrieve the access token
2424
/// </summary>
2525
public string UserName { get; set; }
26+
/// <summary>
27+
/// If username is set use impersonation
28+
/// </summary>
29+
public bool UseImpersonation { get; private set; } = false;
2630

2731
/// <summary>
2832
/// Add client credential flow parameters
@@ -57,6 +61,7 @@ public KeycloakOptions AddUserNameForImpersonation(string userName)
5761
throw new ArgumentNullException($"{nameof(userName)} is null.");
5862

5963
UserName = userName;
64+
UseImpersonation = true;
6065
return this;
6166
}
6267
}

src/QAToolKit.Auth/Keycloak/KeycloakTokenService.cs

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ internal class KeycloakTokenService : IDisposable
1515
private readonly string _clientId;
1616
private readonly string _secret;
1717
private string _accessToken = null;
18-
private DateTimeOffset? _accessTokenValidity = null;
1918
private readonly string _assemblyName;
2019
private readonly string _assemblyVersion;
2120
private readonly string _impersonatedUsername;
21+
private readonly bool _useImpersonation;
2222

2323
public KeycloakTokenService(KeycloakOptions keycloakOptions)
2424
{
@@ -27,19 +27,16 @@ public KeycloakTokenService(KeycloakOptions keycloakOptions)
2727
_tokenEndpoint = keycloakOptions.TokenEndpoint;
2828
_clientId = keycloakOptions.ClientId;
2929
_secret = keycloakOptions.Secret;
30+
_impersonatedUsername = keycloakOptions.UserName;
31+
_useImpersonation = keycloakOptions.UseImpersonation;
3032

3133
_assemblyName = typeof(KeycloakTokenService).Assembly.GetName().Name;
3234
_assemblyVersion = typeof(KeycloakTokenService).Assembly.GetName().Version.ToString();
33-
_impersonatedUsername = keycloakOptions.UserName;
35+
3436
}
3537

3638
public async Task<string> GetAccessTokenAsync()
3739
{
38-
if (IsAccessTokenValid())
39-
{
40-
return _accessToken;
41-
}
42-
4340
await PostTokenClientCredentials();
4441

4542
return _accessToken;
@@ -59,21 +56,15 @@ private async Task PostTokenClientCredentials()
5956
new KeyValuePair<string, string>("client_secret", _secret)
6057
});
6158

62-
var now = DateTimeOffset.Now;
63-
6459
var response = await _client.SendAsync(request);
6560

6661
if (response.IsSuccessStatusCode)
6762
{
6863
dynamic body = JObject.Parse(await response.Content.ReadAsStringAsync());
6964

70-
if (string.IsNullOrEmpty(_impersonatedUsername))
65+
if (!_useImpersonation)
7166
{
72-
string at = body.access_token;
73-
int ei = body.expires_in;
74-
75-
_accessToken = at;
76-
_accessTokenValidity = now.AddSeconds(ei);
67+
_accessToken = body.access_token;
7768
}
7869
else
7970
{
@@ -101,11 +92,7 @@ private async Task PostTokenClientCredentials()
10192
{
10293
dynamic content = JObject.Parse(contentStr);
10394

104-
string at = content.access_token;
105-
int ei = content.expires_in;
106-
107-
_accessToken = at;
108-
_accessTokenValidity = now.AddSeconds(ei);
95+
_accessToken = content.access_token;
10996
}
11097
else
11198
{
@@ -124,17 +111,6 @@ private async Task PostTokenClientCredentials()
124111
}
125112
}
126113

127-
private bool IsAccessTokenValid(DateTimeOffset? now = null)
128-
{
129-
if (_accessToken == null || !_accessTokenValidity.HasValue)
130-
return false;
131-
132-
if (!now.HasValue)
133-
now = DateTimeOffset.Now;
134-
135-
return _accessTokenValidity.Value.AddSeconds(-TokenValidityOffsetSeconds) >= now;
136-
}
137-
138114
private HttpRequestMessage CreateBasicTokenEndpointRequest()
139115
{
140116
var request = new HttpRequestMessage(HttpMethod.Post, _tokenEndpoint);

0 commit comments

Comments
 (0)