Skip to content

Commit

Permalink
feat: Add OpenTelemetry support; Improve logging (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored May 10, 2024
1 parent 018aaea commit 71d77d5
Show file tree
Hide file tree
Showing 48 changed files with 1,014 additions and 319 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ jobs:
- name: "Dotnet Cake Build"
run: dotnet cake --target=Build
shell: pwsh
# - name: "Dotnet Cake Test"
# run: dotnet cake --target=Test
# shell: pwsh
- name: "Dotnet Cake Test"
run: dotnet cake --target=Test
shell: pwsh
- name: "Dotnet Cake Pack"
run: dotnet cake --target=Pack
shell: pwsh
Expand Down
7 changes: 7 additions & 0 deletions KeycloakAuthorizationServicesDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebApiWithControllers",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResourceAuthorization", "samples\ResourceAuthorization\ResourceAuthorization.csproj", "{B060EE8C-C76D-48A4-B209-4646070A7E0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keycloak.AuthServices.OpenTelemetry", "src\Keycloak.AuthServices.OpenTelemetry\Keycloak.AuthServices.OpenTelemetry.csproj", "{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -160,6 +162,10 @@ Global
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B060EE8C-C76D-48A4-B209-4646070A7E0D}.Release|Any CPU.Build.0 = Release|Any CPU
{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -186,6 +192,7 @@ Global
{8C43A1C1-0069-4B21-ADDE-5268EB214820} = {F9D5C5B8-9933-4AE0-ADAC-6B8C15F7552A}
{BF2DCACD-E7C4-4B92-909F-CC535B70F94D} = {96857509-627A-4FD2-AC82-34387619A7B1}
{B060EE8C-C76D-48A4-B209-4646070A7E0D} = {AEBE10B1-96B1-4060-B8C1-1F9BFA7A586C}
{3FE98A91-BA4E-4D4F-A6A5-A43123644ACD} = {F9D5C5B8-9933-4AE0-ADAC-6B8C15F7552A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E1907BFD-C144-4B48-AA40-972F499D4E08}
Expand Down
2 changes: 1 addition & 1 deletion build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Task("Build")

Task("Test")
.Description("Runs unit tests and outputs test results to the artefacts directory.")
.DoesForEach(GetFiles("./tests/**/*.csproj"), project =>
.DoesForEach(GetFiles("./tests/**/*.csproj").Where(file => !file.ToString().Contains("Integration")), project =>
{
DotNetTest(
project.ToString(),
Expand Down
8 changes: 5 additions & 3 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default withMermaid({
},
{
text: 'Authorization',
collapsed: false,
collapsed: true,
items: [
{
text: 'Authorization Server', link: '/authorization/authorization-server'
Expand Down Expand Up @@ -104,9 +104,11 @@ export default withMermaid({
]
},
{
text: 'Q&A',
text: 'Maintenance👨‍🔬',
items: [
{ text: 'Recipes', link: '/qa/recipes' },
{ text: 'Q&A', link: '/qa/recipes' },
{ text: 'Troubleshooting', link: '/qa/troubleshooting' },
{ text: 'OpenTelemetry🔭', link: '/opentelemetry' }
]
},
{
Expand Down
4 changes: 3 additions & 1 deletion docs/authorization/resources-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

ASP.NET Core allows you to built policies based on [Microsoft.AspNetCore.Authorization.AuthorizationBuilder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationbuilder). `Keycloak.AuthServices.Authorization` adds [Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authorization.authorizationpolicybuilder) extension methods to work with protected resources and configure your polices.

::: details AuthorizationPolicyBuilderExtensions

<<< @/../src/Keycloak.AuthServices.Authorization/PoliciesBuilderExtensions.cs#RequireProtectedResource

<!-- <<< @/../src/Keycloak.AuthServices.Authorization/PoliciesBuilderExtensions.cs#RequireProtectedResourceScopes -->
:::

## Add to your code

Expand Down
6 changes: 3 additions & 3 deletions docs/configuration/configuration-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ Not everything you want to do can be configured with `KeycloakAuthenticationOpti

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides

> [!NOTE]
> `KeycloakAuthenticationOptions` ("Keycloak") takes precedence over `Authentication:Schemes:{SchemeName}` ("Bearer" - `JwtBearerOptions`) in the case of default configuration
Here is a trick to bind options from configuration an override directly in the same code:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides2{3}
Expand Down Expand Up @@ -87,9 +90,6 @@ Typically, ASP.NET Core expects to find these (default) options under the `Authe
}
```

> [!NOTE]
> `KeycloakAuthenticationOptions` ("Keycloak") takes precedence over `Authentication:Schemes:{SchemeName}` ("Bearer") in the case of default configuration
### AuthenticationBuilder Extensions

For situations when you want to override *Authentication Scheme* or you just prefer more verbose way of defining your project's *Authentication* you can use `AuthenticationBuilder` extension methods:
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/configuration-keycloak.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configure Keycloak

This section contains a general instruction of how to configure Keyclaok to be used for .NET applications.
This section contains a general instruction of how to configure Keycloak to be used for .NET applications.

*Table of Contents*:
[[toc]]
Expand Down
64 changes: 64 additions & 0 deletions docs/opentelemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Keycloak.AuthServices.OpenTelemetry

`Keycloak.AuthServices` can be instrumented via [OpenTelemetry](https://opentelemetry.io/docs/languages/net/getting-started/).

You may ask, **"Why do I even need it?"** and you would be right. In most cases, logging is enough. However, since `Keycloak.AuthServices.Authorization` makes multiple outgoing requests to the Authorization Server, it was decided to add OpenTelemetry support to gain better insights into how the authorization process works.

## Add to your code

```bash
dotnet add package Keycloak.AuthServices.OpenTelemetry
```

Here is how to use it:

```csharp
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});

services
.AddOpenTelemetry()
.WithMetrics(metrics =>
metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddKeycloakAuthServicesInstrumentation() // [!code highlight]
)
.WithTracing(tracing =>
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddKeycloakAuthServicesInstrumentation() // [!code highlight]
)
.UseOtlpExporter();
```

## Metrics example

```bash
dotnet counters monitor \
--name ResourceAuthorization \
--counters Keycloak.AuthServices.Authorization
```

Play around with an application and see the results:

```text
Press p to pause, r to resume, q to quit.
Status: Running
Name Current Value
[Keycloak.AuthServices.Authorization]
keycloak.authservices.requirements.fail (Count)
requirement=ParameterizedProtectedResourceRequirement 3
keycloak.authservices.requirements.succeed (Count)
requirement=ParameterizedProtectedResourceRequirement 5
requirement=RealmAccessRequirement 16
```
97 changes: 69 additions & 28 deletions docs/qa/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,54 @@ Welcome to the Recipes section! Here you will find a collection of instructions

[[toc]]

## How to get an access token from Swagger UI?
## How to debug an application?

Adjust logging level:

```json
{
"Logging": {
"Keycloak.AuthServices": "Debug",
"Keycloak.AuthServices.Authorization": "Trace"
}
}
```

> [!NOTE]
> ☝️`Keycloak.AuthServices` supports OpenTelemetry. See [Keycloak.AuthServices.OpenTelemetry](/opentelemetry).
## How to get Options from DI?

```csharp
var keycloakAuthenticationOptions = serviceProvider
.GetRequiredService<IOptionsMonitor<KeycloakAuthenticationOptions>>()
.Get(JwtBearerDefaults.AuthenticationScheme);

var keycloakAuthenticationOptions = serviceProvider
.GetRequiredService<IOptionsMonitor<KeycloakAuthorizationOptions>>()
.CurrentValue;
```

> [!NOTE]
> To retrieve `KeycloakAuthenticationOptions` you need to use `IOptionsMonitor.Get(string name)` because this options are registered per Scheme.
## How to get Options outside of `IServiceProvider`?

Sometimes you need to resolve options before the DI container is built. E.g: application startup.

```csharp
var keycloakOptions = configuration.GetKeycloakOptions<KeycloakAuthenticationOptions>()!;
// OR
KeycloakAuthorizationOptions options = new();
configuration.BindKeycloakOptions(options);
```

## How to get an access token via Swagger UI?

Here is an example of how to use [NSwag](https://github.com/RicoSuter/NSwag/wiki/AspNetCore-Middleware#add-oauth2-authentication-openapi-3):

::: details Code

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -55,42 +99,39 @@ app.UseSwaggerUi(ui =>
app.Run();
```

## How to get Options from DI?
:::

```csharp
var keycloakAuthenticationOptions = serviceProvider
.GetRequiredService<IOptionsMonitor<KeycloakAuthenticationOptions>>()
.Get(JwtBearerDefaults.AuthenticationScheme);
## How to setup resiliency to HTTP Clients?

var keycloakAuthenticationOptions = serviceProvider
.GetRequiredService<IOptionsMonitor<KeycloakAuthorizationOptions>>()
.CurrentValue;
```
Every HTTP Client provided by `Keycloak.AuthServices` expose `IHttpClientBuilder`. It a standard way to extend behavior of `HttpClient`. We can use it to our advantage!

> [!NOTE]
> To retrieve `KeycloakAuthenticationOptions` you need to use `IOptionsMonitor.Get(string name)` because this options are registered per Scheme.
Install [Microsoft.Extensions.Http.Resilience](https://www.nuget.org/packages/Microsoft.Extensions.Http.Resilience)

## How to get Options outside of `IServiceProvider`?
```bash
dotnet add package Microsoft.Extensions.Http.Resilience
```

Sometimes you need to resolve options before the DI container is built. E.g: application startup.
Add resilience handler globally (for all `HttpClient`s including ones provided by `Keycloak.AuthServices`)

Add globally:

```csharp
var keycloakOptions = configuration.GetKeycloakOptions<KeycloakAuthenticationOptions>()!;
// OR
KeycloakAuthorizationOptions options = new();
configuration.BindKeycloakOptions(options);
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services

builder.Services.ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler());
```

## How to debug an application?
Add per-client:

Adjust logging level:
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services

```json
{
"Logging": {
"Keycloak.AuthServices": "Debug",
"Keycloak.AuthServices.Authorization": "Trace"
}
}
}
services
.AddKeycloakAuthorization()
.AddAuthorizationServer(builder.Configuration)
.AddStandardResilienceHandler();
```
27 changes: 27 additions & 0 deletions docs/qa/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Troubleshooting

*Common issues:*

[[toc]]

## I receive 401 Unauthorized status code

* [Turn on](/qa/recipes.html#how-to-debug-an-application) Debug or Trace logging level and see the logs output
* Make sure access token is provided in Authorization Header.
* Make sure the audience is mapped to a token via [audience mapper](/configuration/configuration-keycloak#add-audience-mapper). You can try to disable audience validation temporarily.
* Make sure the HTTPs requirement is turned off in Development Mode. `KeycloakAuthenticationOptions.SslRequired="none"`

## I receive 403 Forbidden

* [Turn on](/qa/recipes.html#how-to-debug-an-application) Debug or Trace logging level and see the logs output
* In case of RBAC Authorization make sure the `ClaimsPrincipal` has "realm_access" and "resource_access" claims mapped from token issued by Keycloak.
* If you use Keycloak as Authorization Server, make sure it is properly configured and that the Keycloak installation is accessible.

## Keycloak is slow to respond

Keycloak is a central part of the system used by many components. Especially, in Authorization Server scenario where authorization requests are sent to centralized place. Essentially, Keycloak becomes a bottleneck of the system. Consider [Cluster Setup](https://www.keycloak.org/2019/05/keycloak-cluster-setup) to tackle this problem.

Also, you can handle transient HTTP errors by adding resiliency, see [How to setup resiliency to HTTP Clients](/qa/recipes.html#how-to-setup-resiliency-to-http-clients)

> [!NOTE]
> ☝️`Keycloak.AuthServices` supports OpenTelemetry. See [Keycloak.AuthServices.OpenTelemetry](/opentelemetry).
5 changes: 5 additions & 0 deletions samples/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.29" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.29" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.0.7" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.SpectreConsole" Version="0.3.3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@
}
}, {
"id" : "64782bb2-79bd-4904-8299-5012f66c2c85",
"name" : "Admin Can Manage Workspaces",
"name" : "Can Manage Workspaces",
"description" : "",
"type" : "scope",
"logic" : "POSITIVE",
Expand All @@ -832,17 +832,6 @@
"applyPolicies" : "[\"Is Admin\"]",
"scopes" : "[\"workspace:delete\",\"workspace:read\",\"workspace:add-user\",\"workspace:remove-user\",\"workspace:list-users\"]"
}
}, {
"id" : "a97f05a6-2164-4310-924e-5bdde44692a3",
"name" : "Workspaces Global Access",
"description" : "",
"type" : "resource",
"logic" : "POSITIVE",
"decisionStrategy" : "AFFIRMATIVE",
"config" : {
"resources" : "[\"workspaces\"]",
"applyPolicies" : "[\"Is Admin\",\"Is Reader\"]"
}
} ],
"scopes" : [ {
"id" : "5e5817b9-23ec-4422-aa1c-521aedee90e2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
},
{
"id": "64782bb2-79bd-4904-8299-5012f66c2c85",
"name": "Admin Can Manage Workspaces",
"name": "Can Manage Workspaces",
"description": "",
"type": "scope",
"logic": "POSITIVE",
Expand All @@ -144,18 +144,6 @@
"applyPolicies": "[\"Is Admin\"]",
"scopes": "[\"workspace:delete\",\"workspace:read\",\"workspace:add-user\",\"workspace:remove-user\",\"workspace:list-users\"]"
}
},
{
"id": "a97f05a6-2164-4310-924e-5bdde44692a3",
"name": "Workspaces Global Access",
"description": "",
"type": "resource",
"logic": "POSITIVE",
"decisionStrategy": "AFFIRMATIVE",
"config": {
"resources": "[\"workspaces\"]",
"applyPolicies": "[\"Is Admin\",\"Is Reader\"]"
}
}
],
"scopes": [
Expand Down
Loading

0 comments on commit 71d77d5

Please sign in to comment.