Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 90 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ dotnet add package Auth0Net.DependencyInjection

![Auth0Authentication](https://user-images.githubusercontent.com/975824/128319560-4b859296-44f5-4219-a1b3-8255bf29f1b3.png)

If you're simply using the `AuthenticationApiClient` and nothing else, you can call `AddAuth0AuthenticationClientCore` and pass in your Auth0 Domain. This integration is lightweight and does not support any other features of this library.
If you're simply using the `AuthenticationApiClient` and nothing else, you can call `AddAuth0AuthenticationClient` and pass in your Auth0 Domain. This integration is lightweight and does not support any other features of this library.

```csharp
services.AddAuth0AuthenticationClientCore("your-auth0-domain.auth0.com");
services.AddAuth0AuthenticationClient("your-auth0-domain.auth0.com");
```

You can then request the `IAuthenticationApiClient` within your class:
Expand Down Expand Up @@ -70,18 +70,17 @@ services.AddAuth0AuthenticationClient(config =>
});
```

Add the `ManagementApiClient` with `AddAuth0ManagementClient()` and add the `DelegatingHandler` with `AddManagementAccessToken()` that will attach the Access Token automatically:
Add the `ManagementApiClient` with `AddAuth0ManagementClient()`. The client will attach the Access Token automatically:

```csharp
services.AddAuth0ManagementClient().AddManagementAccessToken();
services.AddAuth0ManagementClient();
```

Ensure your Machine-to-Machine application is authorized to request tokens from the Managment API and it has the correct scopes for the features you wish to use.

You can then request the `IManagementApiClient` (or `IAuthenticationApiClient`) within your services:

```csharp

public class MyAuth0Service : IAuth0Service
{
private readonly IManagementApiClient _managementApiClient;
Expand All @@ -93,23 +92,23 @@ public class MyAuth0Service : IAuth0Service
```


#### Handling Custom Domains
#### Handling Custom Domains

If you're using a custom domain with your Auth0 tenant, you may run into a problem whereby the `audience` of the Management API is being incorrectly set. You can override this via the `Audience` property:
If you're using a custom domain with your Auth0 tenant, and it's being specified when calling `AddAuth0AuthenticationClient`, you will run into a problem whereby the `audience` of the Management API is being incorrectly set. You can override this via the `Audience` property:

```cs
services.AddAuth0ManagementClient()
.AddManagementAccessToken(c =>
services.AddAuth0ManagementClient(c =>
{
// Set the audience to your default Auth0 domain.
c.Audience = "my-tenant.au.auth0.com";
});
```

### With HttpClient and/or Grpc Services (Machine-To-Machine tokens)
### External HttpClient & Grpc Services (Machine-To-Machine Tokens)

![Auth0AuthenticationAll](https://user-images.githubusercontent.com/975824/128319653-418e0e72-2ddf-4d02-9544-1d60bd523321.png)

**Note:** This feature relies on `services.AddAuth0AuthenticationClient(config => ...)` being called and configured as outlined in the previous scenario.
**Note:** This feature relies on `services.AddAuth0AuthenticationClient(config => ...)` being called and configured as outlined in the previous section.

This library includes a delegating handler - effectively middleware for your HttpClient - that will append an access token to all outbound requests. This is useful for calling other services that are protected by Auth0. This integration requires your service implementation to use `IHttpClientFactory` as part of its registration. You can read more about it [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests)

Expand Down Expand Up @@ -140,29 +139,85 @@ services.AddHttpClient<MyHttpService>(x=> x.BaseAddress = new Uri("https://MySer
.AddAccessToken(config => config.AudienceResolver = request => request.RequestUri.GetLeftPart(UriPartial.Authority));
```

## Additional Functionality
### M2M Organizations Support

This library includes support for [Machine-to-Machine (M2M) Access for Organizations](https://auth0.com/docs/manage-users/organizations/organizations-for-m2m-applications), including static and dynamic scenarios.
This feature is important if your internal or third-party services expect a token to be scoped to a specific Auth0 organization.

Orgs support must be enabled for your combination of client/api/org(s) before usage.

#### Static Organization

Clients that simply require a single organization for a specific client can do so via setting the `Organization` property when configuring the access token:

```csharp
builder.Services
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
.AddAccessToken(config =>
{
config.Audience = builder.Configuration["AspNetCore:Audience"];
config.Organization = builder.Configuration["AspNetCore:Audience"];
});
```

### Enhanced Resilience
#### Dynamic Organization via Request Metadata

The default rate-limit behaviour in Auth0.NET is suboptimal, as it uses random backoff rather than reading the rate limit headers returned by Auth0.
This package includes an additional `.AddAuth0RateLimitResilience()` extension that adds improved rate limit handling to the Auth0 clients.
If you're running into rate limit failures, I highly recommend adding this functionality:
If you already include org metadata as part of your network request or via the request options and would like to easily migrate to Org-scoped tokens, you can choose to resolve the organization at runtime via the `OrganizationResolver`:

```csharp
services.AddAuth0ManagementClient()
.AddManagementAccessToken()
.AddAuth0RateLimitResilience();
builder.Services
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
.AddAccessToken(config =>
{
config.Audience = builder.Configuration["AspNetCore:Audience"];
config.OrganizationResolver = x =>
x.Headers.TryGetValues("org-id", out var values)
? values.SingleOrDefault()
: null;
});
```

When a retry occurs, you should see a warning log similar to:
#### Dynamic Organization via Client Scope (Experimental)

If your organization source is scoped to the usage of your service, such as an ASP.NET Core request, then you'll want the ability to freely set the Organization.
You can achieve this by injecting your client via `OrganizationScopeFactory<TClient>` and then creating an organization scope via `.CreateScope`:

`Resilience event occurred. EventName: '"OnRetry"', Source: '"IManagementConnection-RateLimitRetry"/""/"Retry"', Operation Key: 'null', Result: '429'`
```csharp
// Inject the factory around your remote client
private readonly OrganizationScopeFactory<UsersService> _scopeFactory;

public QueryUsersService(OrganizationScopeFactory<UsersService> scopeFactory)
{
_scopeFactory = scopeFactory;
}

public async Task CreateUserAsync(User user, string orgId)
{
// Create the scope so the MTM token is generated with the current OrgId
using var orgScope = _scopeFactory.CreateScope(orgId);
await orgScope.Client.CreateUser(user, stoppingToken);
}

```

ALWAYS ensure you dispose of the scope when finished.

There's a few limitations if you're using this functionality, as it uses `AsyncLocal` internally:

- Never use multiple client scopes at the same time, either with the same or different client types. This will throw an exception.
- Never call any other client that utilizes `.AddAccessToken` within a client scope. This may cause the wrong Organization ID/Name being used for a given request.

If you have a use-case for either of these items, please open an issue with an example.

This functionality is marked as experimental, and you must `#pragma warning disable AUTH0_EXPERIMENTAL` to use it.

## Additional Functionality

### Utility

This library exposes a simple string extension, `ToHttpsUrl()`, that can be used to format the naked Auth0 domain sitting in your configuration into a proper URL.

This is identical to `https://{Configuration["Auth0:Domain"]}/` that you usually end up writing _somewhere_ in your `Startup.cs`.
This is identical to `https://{Configuration["Auth0:Domain"]}/` that you usually end up writing _somewhere_ in your `Program.cs`.

For example, formatting the domain for the JWT Authority:

Expand All @@ -183,7 +238,7 @@ Both the authentication and authorization clients are registered as singletons a

### Samples

Both a .NET Generic Host and ASP.NET Core example are available in the [samples](https://github.com/Hawxy/Auth0Net.DependencyInjection/tree/main/samples) directory.
Both a .NET Generic Host and ASP.NET Core examples are available in the [samples](https://github.com/Hawxy/Auth0Net.DependencyInjection/tree/main/samples) directory.

### Internal Cache

Expand All @@ -193,7 +248,18 @@ An additional 1% of lifetime is removed to protect against clock drift between d

In some situations you might want to request an access token from Auth0 manually. You can achieve this by injecting `IAuth0TokenCache` into a class and calling `GetTokenAsync` with the audience of the API you're requesting the token for.

An in-memory-only instance of [FusionCache](https://github.com/ZiggyCreatures/FusionCache) is used as the caching implementation. This instance is _named_ and will not impact other usages of FusionCache.
An in-memory-only instance of [FusionCache](https://github.com/ZiggyCreatures/FusionCache) is used as the caching implementation. This instance is _named_ and will not impact other usages of FusionCache.

If you want to use your own implementation of FusionCache, specify `FusionCacheInstance` when configurating the authentication client:

```csharp
services.AddAuth0AuthenticationClient(x =>
{
//...
// Use the default FusionCache instance registered via `.AddFusionCache()`
x.FusionCacheInstance = FusionCacheOptions.DefaultCacheName
});
```

## Disclaimer

Expand Down
19 changes: 15 additions & 4 deletions samples/Sample.AspNetCore/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Security.Claims;
using Auth0.ManagementApi;
using Auth0Net.DependencyInjection;
using Auth0Net.DependencyInjection.HttpClient;
Expand All @@ -9,14 +10,13 @@

var builder = WebApplication.CreateBuilder(args);

// An extension method is included to convert a naked auth0 domain (my-tenant.auth0.au.com) to the correct format (https://my-tenant-auth0.au.com/)
string domain = builder.Configuration["Auth0:Domain"]!.ToHttpsUrl();
var domain = builder.Configuration["Auth0:Domain"];

// Protect your API with authentication as you normally would
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = domain;
options.Authority = domain!.ToHttpsUrl();
options.Audience = builder.Configuration["Auth0:Audience"];
});

Expand All @@ -35,7 +35,7 @@
// Adds the AuthenticationApiClient client and provides configuration to be consumed by the management client, token cache, and IHttpClientBuilder integrations
builder.Services.AddAuth0AuthenticationClient(config =>
{
config.Domain = domain;
config.Domain = domain!;
config.ClientId = builder.Configuration["Auth0:ClientId"];
config.ClientSecret = builder.Configuration["Auth0:ClientSecret"];
});
Expand All @@ -59,5 +59,16 @@
return user.CurrentPage.Select(x => new Sample.AspNetCore.User(x.UserId, x.Name, x.Email)).ToArray();
});

app.MapGet("/users/org-scoped", async ([FromServices] IManagementApiClient client, HttpContext context, ILogger<Program> logger) =>
{
var orgId = context.User.FindFirstValue("org_id");
if (!string.IsNullOrEmpty(orgId)) {
logger.LogInformation("Found org {org}", orgId);
}
var user = await client.Users.ListAsync(new ListUsersRequestParameters() { });

return user.CurrentPage.Select(x => new Sample.AspNetCore.User(x.UserId, x.Name, x.Email)).ToArray();
});


app.Run();
5 changes: 4 additions & 1 deletion samples/Sample.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
// Works for the Grpc integration too!
builder.Services
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
.AddAccessToken(config => config.Audience = builder.Configuration["AspNetCore:Audience"]);
.AddAccessToken(config =>
{
config.Audience = builder.Configuration["AspNetCore:Audience"];
});


builder.Services.AddHostedService<PumpingBackgroundService>();
Expand Down
9 changes: 6 additions & 3 deletions samples/Sample.ConsoleApp/PumpingBackgroundService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Sample.ConsoleApp.Services;
using Auth0Net.DependencyInjection.HttpClient;
using Auth0Net.DependencyInjection.Organizations;
using Sample.ConsoleApp.Services;
using User;

namespace Sample.ConsoleApp;
Expand All @@ -8,13 +10,14 @@ public class PumpingBackgroundService : BackgroundService
{
private readonly UsersService _usersService;
private readonly UserService.UserServiceClient _usersClient;
private readonly ILogger<PumpingBackgroundService> _logger;
private readonly ILogger<PumpingBackgroundService> _logger;

public PumpingBackgroundService(UsersService usersService, UserService.UserServiceClient usersClient, ILogger<PumpingBackgroundService> logger)
public PumpingBackgroundService(UsersService usersService, UserService.UserServiceClient usersClient, ILogger<PumpingBackgroundService> logger)
{
_usersService = usersService;
_usersClient = usersClient;
_logger = logger;

}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down
30 changes: 30 additions & 0 deletions samples/Sample.ConsoleApp/PumpingBackgroundServiceOrgScoped.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Auth0Net.DependencyInjection.Organizations;
using Sample.ConsoleApp.Services;
#pragma warning disable AUTH0_EXPERIMENTAL

namespace Sample.ConsoleApp;

// Not a realistic example, just using it to hit our API.
public class PumpingBackgroundServiceOrgScoped : BackgroundService
{
private readonly OrganizationScopeFactory<UsersService> _scopeFactory;
private readonly ILogger<PumpingBackgroundService> _logger;

public PumpingBackgroundServiceOrgScoped(OrganizationScopeFactory<UsersService> scopeFactory, ILogger<PumpingBackgroundService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scopedClient = _scopeFactory.CreateScope("org_12345");

while (!stoppingToken.IsCancellationRequested)
{
var userHttpClient = await scopedClient.Client.GetUsersAsync(stoppingToken);
_logger.LogInformation("HttpClient got user's email: {email}", userHttpClient?.First().Email);
await Task.Delay(5000, stoppingToken);
}
}
}
6 changes: 3 additions & 3 deletions samples/Sample.ConsoleApp/Services/UsersService.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using System.Net.Http.Json;
using System.Net.Http.Json;
using Auth0Net.DependencyInjection.HttpClient;

namespace Sample.ConsoleApp.Services;

public class UsersService
{
private readonly HttpClient _client;

public UsersService(HttpClient client)
{
_client = client;
}

public async Task<User[]?> GetUsersAsync(CancellationToken ct) => await _client.GetFromJsonAsync<User[]>("users", cancellationToken: ct);

public record User(string Id, string Name, string Email);
Expand Down
7 changes: 6 additions & 1 deletion src/Auth0Net.DependencyInjection/Auth0Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Auth0Net.DependencyInjection.Factory;
using Auth0Net.DependencyInjection.HttpClient;
using Auth0Net.DependencyInjection.Injectables;
using Auth0Net.DependencyInjection.Organizations;
using Microsoft.Extensions.DependencyInjection;

namespace Auth0Net.DependencyInjection;
Expand Down Expand Up @@ -89,6 +90,10 @@ private static IHttpClientBuilder AddAuth0AuthenticationClientInternal(this ISer
services.AddSingleton<IAuth0TokenCache, Auth0TokenCache>();
}

services.AddSingleton<HttpClientOrganizationAccessor>();
#pragma warning disable AUTH0_EXPERIMENTAL
services.AddTransient(typeof(OrganizationScopeFactory<>));
#pragma warning restore AUTH0_EXPERIMENTAL
services.AddSingleton<IAuthenticationApiClient, InjectableAuthenticationApiClient>();
return services.AddHttpClient<IAuthenticationConnection, HttpClientAuthenticationConnection>()
#if NET8_0
Expand Down Expand Up @@ -151,6 +156,6 @@ public static IHttpClientBuilder AddAccessToken(this IHttpClientBuilder builder,
throw new ArgumentException("Audience or AudienceResolver must be set");

return builder.AddHttpMessageHandler(provider =>
new Auth0TokenHandler(provider.GetRequiredService<IAuth0TokenCache>(), c));
new Auth0TokenHandler(provider.GetRequiredService<IAuth0TokenCache>(), c, provider.GetRequiredService<HttpClientOrganizationAccessor>()));
}
}
Loading
Loading