Skip to content
80 changes: 80 additions & 0 deletions Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
- [3.4. Challenge an already enrolled user](#34-challenge-an-already-enrolled-user)
- [3.5. Get the list of Authenticators for a user](#35-get-the-list-of-authenticators-for-a-user)
- [3.6. Delete an enrolled authenticator](#36-delete-an-enrolled-authenticator)
- [4. Monitor Client / Organization Quota when authenticating using Client Credentials (M2M)](#4-client-organization-quota-when-authenticating-using-client-credentials-m2m)
- [4.1. Monitor quota when authenticating using Client Credentials (M2M)](#41-monitor-quota-when-authenticating-using-client-credentials-m2m)
- [4.1 Rate limit exception when quota is breached while authenticating using Client Credentials (M2M)](#42-rate-limit-exception-when-quota-is-breached-while-authenticating-using-client-credentials-m2m)

## 1. Client Initialization

Expand Down Expand Up @@ -173,6 +176,83 @@ await authClient.DeleteMfaAuthenticatorAsync(
```
⬆️ [Go to Top](#)

## 4. Client / Organization Quota when authenticating using Client Credentials (M2M)
Assuming the Client and Organization Quota is configured using one of the options as shown [here](#2-update-m2m-token-quota-at-different-levels).

### 4.1 Monitor quota when authenticating using Client Credentials (M2M)
You can monitor the usage on every authentication request like below
```csharp
public async Task LoginWithClientCredentialsAndMonitorClientQuota()
{
var authClient = new AuthenticationApiClient("my.custom.domain");

// Fetch the access token using the Client Credentials.
var accessTokenResponse = await authClient.GetTokenAsync(new ClientCredentialsTokenRequest()
{
Audience = "audience",
ClientId = "clientId",
ClientSecret = "clientSecret",
});

Console.WriteLine($"Access Token : {accessTokenResponse.AccessToken}");

var clientQuota = accessTokenResponse.Headers.GetClientQuotaLimit();
Console.WriteLine($"Client Quota remaining in the hour bucket : {clientQuota.PerHour.Remaining }");
Console.WriteLine($"Client Quota configured in the hour bucket : {clientQuota.PerHour.Quota }");
Console.WriteLine($"Client Quota for the hour bucket is refreshed after (in secs): {clientQuota.PerHour.ResetAfter }");

Console.WriteLine($"Client Quota remaining in the Day bucket : {clientQuota.PerDay.Remaining }");
Console.WriteLine($"Client Quota configured in the Day bucket : {clientQuota.PerDay.Quota }");
Console.WriteLine($"Client Quota for the Day bucket is refreshed after (in secs): {clientQuota.PerDay.ResetAfter }");

var orgQuota = accessTokenResponse.Headers.GetOrganizationQuotaLimit();
Console.WriteLine($"Organization Quota remaining in the hour bucket : {orgQuota.PerHour.Remaining }");
Console.WriteLine($"Organization Quota configured in the hour bucket : {orgQuota.PerHour.Quota }");
Console.WriteLine($"Organization Quota for the hour bucket is refreshed after (in secs): {orgQuota.PerHour.ResetAfter }");

Console.WriteLine($"Organization Quota remaining in the Day bucket : {orgQuota.PerDay.Remaining }");
Console.WriteLine($"Organization Quota configured in the Day bucket : {orgQuota.PerDay.Quota }");
Console.WriteLine($"Organization Quota for the Day bucket is refreshed after (in secs): {orgQuota.PerDay.ResetAfter }");
}
```
⬆️ [Go to Top](#)

### 4.2 Rate limit exception when quota is breached while authenticating using Client Credentials (M2M)
When the token quota is breached (either at client level OR at org level), the server returns a 429 with some extra headers that we can use to extract details of the failure.
Below is an example where there is a client quota breach.
```csharp
public async Task LoginWithClientCredentialsAndMonitorClientQuota()
{
var authClient = new AuthenticationApiClient("my.custom.domain");

try
{
// Fetch the access token using the Client Credentials.
var accessTokenResponse = await authClient.GetTokenAsync(new ClientCredentialsTokenRequest()
{
Audience = "audience",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
Console.WriteLine($"Access Token : {accessTokenResponse.AccessToken}");
}
catch (RateLimitApiException ex)
{
Console.WriteLine($"Error message : {ex.ApiError.Message}");
Console.WriteLine($"x-RateLimit-Limit : {ex.RateLimit.Limit}")
Console.WriteLine($"x-RateLimit-Remaining : {ex.RateLimit.Remaining}")
Console.WriteLine($"x-RateLimit-Reset : {ex.RateLimit.Reset}")
Console.WriteLine($"Retry-After : {ex.RateLimit.RetryAfter}")
Console.WriteLine($"Time to reset the breached client quota: {ex.RateLimit.ClientQuotaLimit.PerHour.ResetAfter}");
}
catch (Exception ex)
{
Console.WriteLine("An exception occurred");
}
}
```
⬆️ [Go to Top](#)

# Management API

- [1. Management Client Initialization](#1-management-client-initialization)
Expand Down
11 changes: 7 additions & 4 deletions src/Auth0.AuthenticationApi/ExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Auth0.AuthenticationApi.Models;
using Auth0.Core.Exceptions;

namespace Auth0.AuthenticationApi
{
Expand All @@ -21,13 +23,14 @@ public static void AddIfNotEmpty(this IDictionary<string, string> dictionary, st
if (!string.IsNullOrEmpty(value))
dictionary.Add(key, value);
}

/// <summary>
/// Adds all items from the source to the target dictionary.
/// </summary>
/// <param name="targetDictionary">Dictionary to add the items to.</param>
/// <param name="sourceDictionary">Dictionary whose items you want to add to the target.</param>
public static void AddAll(this IDictionary<string, string> targetDictionary, IDictionary<string, string> sourceDictionary)
public static void AddAll(this IDictionary<string, string> targetDictionary,
IDictionary<string, string> sourceDictionary)
{
foreach (var keyValuePair in sourceDictionary)
{
Expand All @@ -41,7 +44,7 @@ public static void AddAll(this IDictionary<string, string> targetDictionary, IDi
}
}
}

/// <summary>
/// Get the string value for the corresponding <see cref="AuthorizationResponseMode"/>.
/// </summary>
Expand All @@ -56,7 +59,7 @@ public static string ToStringValue(this AuthorizationResponseMode responseMode)

return null;
}

/// <summary>
/// Get the string value for the corresponding <see cref="AuthorizationResponseType"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Auth0.AuthenticationApi.Models;

namespace Auth0.AuthenticationApi
{
Expand Down Expand Up @@ -74,7 +75,7 @@ public async Task<T> SendAsync<T>(HttpMethod method, Uri uri, object body, IDict
return await SendRequest<T>(request, cancellationToken).ConfigureAwait(false);
}
}

private async Task<T> SendRequest<T>(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
if (!ownHttpClient)
Expand All @@ -86,8 +87,11 @@ private async Task<T> SendRequest<T>(HttpRequestMessage request, CancellationTok
throw await ApiException.CreateSpecificExceptionAsync(response).ConfigureAwait(false);

var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

return DeserializeContent<T>(content);

var parsedResponse = DeserializeContent<T>(content);

AddResponseHeaders(parsedResponse, response);
return parsedResponse;
}
}

Expand Down Expand Up @@ -128,6 +132,19 @@ private static HttpContent CreateFormUrlEncodedContent(IDictionary<string, strin
{
return new FormUrlEncodedContent(parameters.Select(p => new KeyValuePair<string, string>(p.Key, p.Value ?? "")));
}

internal static void AddResponseHeaders<T>(T parsedResponse, HttpResponseMessage httpResponse)
{
if (parsedResponse == null || httpResponse == null) return;

var headers = httpResponse.Headers?.ToDictionary(h => h.Key, h => h.Value);
var headersProperty = typeof(T).GetProperty("Headers");

if (headersProperty != null && headersProperty.PropertyType == typeof(IDictionary<string, IEnumerable<string>>))
{
headersProperty.SetValue(parsedResponse, headers);
}
}

private static string CreateAgentString()
{
Expand Down
6 changes: 6 additions & 0 deletions src/Auth0.AuthenticationApi/Models/AccessTokenResponse.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Http.Headers;
using Auth0.AuthenticationApi.Models.Mfa;
using Newtonsoft.Json;

namespace Auth0.AuthenticationApi.Models
Expand All @@ -24,5 +28,7 @@ public class AccessTokenResponse : TokenBase
/// </summary>
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }

public IDictionary<string, IEnumerable<string>> Headers { get; set; }
}
}
41 changes: 41 additions & 0 deletions src/Auth0.Core/Exceptions/QuotaLimit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Auth0.Core.Exceptions
{
/// <summary>
/// Represents the Client Quota Headers returned as part of the response.
/// </summary>
public class ClientQuotaLimit
{
public QuotaLimit PerHour { get; set; }
public QuotaLimit PerDay { get; set; }
}

/// <summary>
/// Represents the Organization Quota Headers returned as part of the response.
/// </summary>
public class OrganizationQuotaLimit
{
public QuotaLimit PerHour { get; set; }
public QuotaLimit PerDay { get; set; }
}

/// <summary>
/// Represents the structure of the quota limit headers returned as part of the response.
/// </summary>
public class QuotaLimit
{
/// <summary>
/// The current configured quota
/// </summary>
public int Quota { get; set; }

/// <summary>
/// The remaining quota
/// </summary>
public int Remaining { get; set; }

/// <summary>
/// Remaining number of seconds when the quota resets.
/// </summary>
public int ResetAfter { get; set; }
}
}
33 changes: 27 additions & 6 deletions src/Auth0.Core/Exceptions/RateLimit.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;

Expand All @@ -20,31 +21,51 @@ public class RateLimit
/// The number of requests remaining in the current rate limit window.
/// </summary>
public long Remaining { get; internal set; }

/// <summary>
/// Indicates how long the user should wait before making a follow-up request
/// </summary>
public long RetryAfter { get; internal set; }

/// <summary>
/// The date and time offset at which the current rate limit window is reset.
/// </summary>
public DateTimeOffset? Reset { get; internal set; }

/// <summary>
/// Represents Client Quota Headers returned.
/// </summary>
public ClientQuotaLimit ClientQuotaLimit { get; internal set; }

/// <summary>
/// Represents Client Quota Headers returned.
/// </summary>
public OrganizationQuotaLimit OrganizationQuotaLimit { get; internal set; }

/// <summary>
/// Parse the rate limit headers into a <see cref="RateLimit"/> object.
/// </summary>
/// <param name="headers"><see cref="HttpHeaders"/> to parse.</param>
/// <returns>Instance of <see cref="RateLimit"/> containing parsed rate limit headers.</returns>
public static RateLimit Parse(HttpHeaders headers)
{
var reset = GetHeaderValue(headers, "x-ratelimit-reset");
var headersKvp =
headers?.ToDictionary(h => h.Key, h => h.Value);
var reset = GetHeaderValue(headersKvp, "x-ratelimit-reset");
return new RateLimit
{
Limit = GetHeaderValue(headers, "x-ratelimit-limit"),
Remaining = GetHeaderValue(headers, "x-ratelimit-remaining"),
Reset = reset == 0 ? null : (DateTimeOffset?)epoch.AddSeconds(reset)
Limit = GetHeaderValue(headersKvp, "x-ratelimit-limit"),
Remaining = GetHeaderValue(headersKvp, "x-ratelimit-remaining"),
Reset = reset == 0 ? null : (DateTimeOffset?)epoch.AddSeconds(reset),
RetryAfter = GetHeaderValue(headersKvp, "Retry-After"),
ClientQuotaLimit = headersKvp.GetClientQuotaLimit(),
OrganizationQuotaLimit = headersKvp.GetOrganizationQuotaLimit()
};
}

private static long GetHeaderValue(HttpHeaders headers, string name)
private static long GetHeaderValue(IDictionary<string, IEnumerable<string>> headers, string name)
{
if (headers.TryGetValues(name, out var v) && long.TryParse(v?.FirstOrDefault(), out var value))
if (headers.TryGetValue(name, out var v) && long.TryParse(v?.FirstOrDefault(), out var value))
return value;

return 0;
Expand Down
Loading
Loading