Skip to content

Commit d0c8957

Browse files
authored
Add MtM Orgs support (#36)
* Add Orgs support to token cache * WIP * WIP * Rebase + tests * Remove .NET 8 restriction * Update docs * Test fix * nit * Add tests for org config * Guard against scope nesting * More doc fixes * Include the org in the cache key. * Doc tweaks --------- Co-authored-by: JT <Hawxy@users.noreply.github.com>
1 parent a0bb28f commit d0c8957

File tree

17 files changed

+659
-52
lines changed

17 files changed

+659
-52
lines changed

README.md

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ dotnet add package Auth0Net.DependencyInjection
3434

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

37-
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.
37+
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.
3838

3939
```csharp
40-
services.AddAuth0AuthenticationClientCore("your-auth0-domain.auth0.com");
40+
services.AddAuth0AuthenticationClient("your-auth0-domain.auth0.com");
4141
```
4242

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

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

7575
```csharp
76-
services.AddAuth0ManagementClient().AddManagementAccessToken();
76+
services.AddAuth0ManagementClient();
7777
```
7878

7979
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.
8080

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

8383
```csharp
84-
8584
public class MyAuth0Service : IAuth0Service
8685
{
8786
private readonly IManagementApiClient _managementApiClient;
@@ -93,23 +92,23 @@ public class MyAuth0Service : IAuth0Service
9392
```
9493

9594

96-
#### Handling Custom Domains
95+
#### Handling Custom Domains
9796

98-
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:
97+
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:
9998

10099
```cs
101-
services.AddAuth0ManagementClient()
102-
.AddManagementAccessToken(c =>
100+
services.AddAuth0ManagementClient(c =>
103101
{
102+
// Set the audience to your default Auth0 domain.
104103
c.Audience = "my-tenant.au.auth0.com";
105104
});
106105
```
107106

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

110109
![Auth0AuthenticationAll](https://user-images.githubusercontent.com/975824/128319653-418e0e72-2ddf-4d02-9544-1d60bd523321.png)
111110
112-
**Note:** This feature relies on `services.AddAuth0AuthenticationClient(config => ...)` being called and configured as outlined in the previous scenario.
111+
**Note:** This feature relies on `services.AddAuth0AuthenticationClient(config => ...)` being called and configured as outlined in the previous section.
113112

114113
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)
115114
@@ -140,29 +139,85 @@ services.AddHttpClient<MyHttpService>(x=> x.BaseAddress = new Uri("https://MySer
140139
.AddAccessToken(config => config.AudienceResolver = request => request.RequestUri.GetLeftPart(UriPartial.Authority));
141140
```
142141

143-
## Additional Functionality
142+
### M2M Organizations Support
143+
144+
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.
145+
This feature is important if your internal or third-party services expect a token to be scoped to a specific Auth0 organization.
146+
147+
Orgs support must be enabled for your combination of client/api/org(s) before usage.
148+
149+
#### Static Organization
150+
151+
Clients that simply require a single organization for a specific client can do so via setting the `Organization` property when configuring the access token:
152+
153+
```csharp
154+
builder.Services
155+
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
156+
.AddAccessToken(config =>
157+
{
158+
config.Audience = builder.Configuration["AspNetCore:Audience"];
159+
config.Organization = builder.Configuration["AspNetCore:Audience"];
160+
});
161+
```
144162

145-
### Enhanced Resilience
163+
#### Dynamic Organization via Request Metadata
146164

147-
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.
148-
This package includes an additional `.AddAuth0RateLimitResilience()` extension that adds improved rate limit handling to the Auth0 clients.
149-
If you're running into rate limit failures, I highly recommend adding this functionality:
165+
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`:
150166

151167
```csharp
152-
services.AddAuth0ManagementClient()
153-
.AddManagementAccessToken()
154-
.AddAuth0RateLimitResilience();
168+
builder.Services
169+
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
170+
.AddAccessToken(config =>
171+
{
172+
config.Audience = builder.Configuration["AspNetCore:Audience"];
173+
config.OrganizationResolver = x =>
174+
x.Headers.TryGetValues("org-id", out var values)
175+
? values.SingleOrDefault()
176+
: null;
177+
});
155178
```
156179

157-
When a retry occurs, you should see a warning log similar to:
180+
#### Dynamic Organization via Client Scope (Experimental)
181+
182+
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.
183+
You can achieve this by injecting your client via `OrganizationScopeFactory<TClient>` and then creating an organization scope via `.CreateScope`:
158184

159-
`Resilience event occurred. EventName: '"OnRetry"', Source: '"IManagementConnection-RateLimitRetry"/""/"Retry"', Operation Key: 'null', Result: '429'`
185+
```csharp
186+
// Inject the factory around your remote client
187+
private readonly OrganizationScopeFactory<UsersService> _scopeFactory;
188+
189+
public QueryUsersService(OrganizationScopeFactory<UsersService> scopeFactory)
190+
{
191+
_scopeFactory = scopeFactory;
192+
}
193+
194+
public async Task CreateUserAsync(User user, string orgId)
195+
{
196+
// Create the scope so the MTM token is generated with the current OrgId
197+
using var orgScope = _scopeFactory.CreateScope(orgId);
198+
await orgScope.Client.CreateUser(user, stoppingToken);
199+
}
200+
201+
```
202+
203+
ALWAYS ensure you dispose of the scope when finished.
204+
205+
There's a few limitations if you're using this functionality, as it uses `AsyncLocal` internally:
206+
207+
- Never use multiple client scopes at the same time, either with the same or different client types. This will throw an exception.
208+
- 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.
209+
210+
If you have a use-case for either of these items, please open an issue with an example.
211+
212+
This functionality is marked as experimental, and you must `#pragma warning disable AUTH0_EXPERIMENTAL` to use it.
213+
214+
## Additional Functionality
160215

161216
### Utility
162217

163218
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.
164219

165-
This is identical to `https://{Configuration["Auth0:Domain"]}/` that you usually end up writing _somewhere_ in your `Startup.cs`.
220+
This is identical to `https://{Configuration["Auth0:Domain"]}/` that you usually end up writing _somewhere_ in your `Program.cs`.
166221
167222
For example, formatting the domain for the JWT Authority:
168223

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

184239
### Samples
185240

186-
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.
241+
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.
187242
188243
### Internal Cache
189244

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

194249
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.
195250

196-
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.
251+
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.
252+
253+
If you want to use your own implementation of FusionCache, specify `FusionCacheInstance` when configurating the authentication client:
254+
255+
```csharp
256+
services.AddAuth0AuthenticationClient(x =>
257+
{
258+
//...
259+
// Use the default FusionCache instance registered via `.AddFusionCache()`
260+
x.FusionCacheInstance = FusionCacheOptions.DefaultCacheName
261+
});
262+
```
197263

198264
## Disclaimer
199265

samples/Sample.AspNetCore/Program.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Security.Claims;
12
using Auth0.ManagementApi;
23
using Auth0Net.DependencyInjection;
34
using Auth0Net.DependencyInjection.HttpClient;
@@ -9,14 +10,13 @@
910

1011
var builder = WebApplication.CreateBuilder(args);
1112

12-
// 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/)
13-
string domain = builder.Configuration["Auth0:Domain"]!.ToHttpsUrl();
13+
var domain = builder.Configuration["Auth0:Domain"];
1414

1515
// Protect your API with authentication as you normally would
1616
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
1717
.AddJwtBearer(options =>
1818
{
19-
options.Authority = domain;
19+
options.Authority = domain!.ToHttpsUrl();
2020
options.Audience = builder.Configuration["Auth0:Audience"];
2121
});
2222

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

62+
app.MapGet("/users/org-scoped", async ([FromServices] IManagementApiClient client, HttpContext context, ILogger<Program> logger) =>
63+
{
64+
var orgId = context.User.FindFirstValue("org_id");
65+
if (!string.IsNullOrEmpty(orgId)) {
66+
logger.LogInformation("Found org {org}", orgId);
67+
}
68+
var user = await client.Users.ListAsync(new ListUsersRequestParameters() { });
69+
70+
return user.CurrentPage.Select(x => new Sample.AspNetCore.User(x.UserId, x.Name, x.Email)).ToArray();
71+
});
72+
6273

6374
app.Run();

samples/Sample.ConsoleApp/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
// Works for the Grpc integration too!
2525
builder.Services
2626
.AddGrpcClient<UserService.UserServiceClient>(x => x.Address = new Uri(builder.Configuration["AspNetCore:Url"]!))
27-
.AddAccessToken(config => config.Audience = builder.Configuration["AspNetCore:Audience"]);
27+
.AddAccessToken(config =>
28+
{
29+
config.Audience = builder.Configuration["AspNetCore:Audience"];
30+
});
2831

2932

3033
builder.Services.AddHostedService<PumpingBackgroundService>();

samples/Sample.ConsoleApp/PumpingBackgroundService.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Sample.ConsoleApp.Services;
1+
using Auth0Net.DependencyInjection.HttpClient;
2+
using Auth0Net.DependencyInjection.Organizations;
3+
using Sample.ConsoleApp.Services;
24
using User;
35

46
namespace Sample.ConsoleApp;
@@ -8,13 +10,14 @@ public class PumpingBackgroundService : BackgroundService
810
{
911
private readonly UsersService _usersService;
1012
private readonly UserService.UserServiceClient _usersClient;
11-
private readonly ILogger<PumpingBackgroundService> _logger;
13+
private readonly ILogger<PumpingBackgroundService> _logger;
1214

13-
public PumpingBackgroundService(UsersService usersService, UserService.UserServiceClient usersClient, ILogger<PumpingBackgroundService> logger)
15+
public PumpingBackgroundService(UsersService usersService, UserService.UserServiceClient usersClient, ILogger<PumpingBackgroundService> logger)
1416
{
1517
_usersService = usersService;
1618
_usersClient = usersClient;
1719
_logger = logger;
20+
1821
}
1922

2023
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Auth0Net.DependencyInjection.Organizations;
2+
using Sample.ConsoleApp.Services;
3+
#pragma warning disable AUTH0_EXPERIMENTAL
4+
5+
namespace Sample.ConsoleApp;
6+
7+
// Not a realistic example, just using it to hit our API.
8+
public class PumpingBackgroundServiceOrgScoped : BackgroundService
9+
{
10+
private readonly OrganizationScopeFactory<UsersService> _scopeFactory;
11+
private readonly ILogger<PumpingBackgroundService> _logger;
12+
13+
public PumpingBackgroundServiceOrgScoped(OrganizationScopeFactory<UsersService> scopeFactory, ILogger<PumpingBackgroundService> logger)
14+
{
15+
_scopeFactory = scopeFactory;
16+
_logger = logger;
17+
}
18+
19+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
20+
{
21+
using var scopedClient = _scopeFactory.CreateScope("org_12345");
22+
23+
while (!stoppingToken.IsCancellationRequested)
24+
{
25+
var userHttpClient = await scopedClient.Client.GetUsersAsync(stoppingToken);
26+
_logger.LogInformation("HttpClient got user's email: {email}", userHttpClient?.First().Email);
27+
await Task.Delay(5000, stoppingToken);
28+
}
29+
}
30+
}

samples/Sample.ConsoleApp/Services/UsersService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
using System.Net.Http.Json;
1+
using System.Net.Http.Json;
2+
using Auth0Net.DependencyInjection.HttpClient;
23

34
namespace Sample.ConsoleApp.Services;
45

56
public class UsersService
67
{
78
private readonly HttpClient _client;
8-
99
public UsersService(HttpClient client)
1010
{
1111
_client = client;
1212
}
13-
13+
1414
public async Task<User[]?> GetUsersAsync(CancellationToken ct) => await _client.GetFromJsonAsync<User[]>("users", cancellationToken: ct);
1515

1616
public record User(string Id, string Name, string Email);

src/Auth0Net.DependencyInjection/Auth0Extensions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Auth0Net.DependencyInjection.Factory;
55
using Auth0Net.DependencyInjection.HttpClient;
66
using Auth0Net.DependencyInjection.Injectables;
7+
using Auth0Net.DependencyInjection.Organizations;
78
using Microsoft.Extensions.DependencyInjection;
89

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

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

153158
return builder.AddHttpMessageHandler(provider =>
154-
new Auth0TokenHandler(provider.GetRequiredService<IAuth0TokenCache>(), c));
159+
new Auth0TokenHandler(provider.GetRequiredService<IAuth0TokenCache>(), c, provider.GetRequiredService<HttpClientOrganizationAccessor>()));
155160
}
156161
}

0 commit comments

Comments
 (0)