I have a multi tenant application. And I am using open id connect provider (Auth0) for each tenant. I want to fetch the auth0 access token in the api. Below is the implementation I have done, could you let me know is this correct, secure and feasible or is there any other better approach. Step 1 - Create a class - Auth0TokenService
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CDP.MultiTenancy
{
public class Auth0TokenService : IAuth0TokenService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHttpClientFactory _httpClientFactory;
//private readonly IConfiguration _configuration;
public Auth0TokenService(IHttpContextAccessor httpContextAccessor,
IHttpClientFactory httpClientFactory)
//IConfiguration configuration)
{
_httpContextAccessor = httpContextAccessor;
_httpClientFactory = httpClientFactory;
//_configuration = configuration;
}
public async Task<string> GetValidAccessTokenAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null) return null;
var accessToken = httpContext.Session.GetString("Auth0AccessToken");
var refreshToken = httpContext.Session.GetString("Auth0RefreshToken");
var expiryString = httpContext.Session.GetString("Auth0TokenExpiry");
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(expiryString))
return null;
var expiry = DateTime.Parse(expiryString, null, System.Globalization.DateTimeStyles.RoundtripKind);
if (DateTime.UtcNow >= expiry.AddMinutes(-1)) // Expiring soon
{
accessToken = await RefreshAccessTokenAsync(refreshToken);
}
return accessToken;
}
private async Task<string> RefreshAccessTokenAsync(string refreshToken)
{
if (string.IsNullOrEmpty(refreshToken))
return null;
var httpContext = _httpContextAccessor.HttpContext;
var client = _httpClientFactory.CreateClient();
var tenantDomain = "fynauth0.uk.auth0.com";//httpContext.Items["TenantAuth0Domain"]?.ToString() ?? _configuration["Auth0:Domain"];
var request = new HttpRequestMessage(HttpMethod.Post, $"{tenantDomain}/oauth/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"grant_type", "refresh_token"},
{"client_id", "{ClientId}"},
{"client_secret", "{Secret}"},
{"refresh_token", refreshToken}
});
var response = await client.SendAsync(request);
if (!response.IsSuccessStatusCode) return null;
var result = await response.Content.ReadFromJsonAsync<Auth0TokenResponse>();
if (!string.IsNullOrEmpty(result?.AccessToken))
{
httpContext.Session.SetString("Auth0AccessToken", result.AccessToken);
httpContext.Session.SetString("Auth0TokenExpiry", DateTime.UtcNow.AddSeconds(result.ExpiresIn).ToString("O"));
}
return result?.AccessToken;
}
}
public class Auth0TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
}
}
Step 2 - Add the below in startup
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(60);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddHttpContextAccessor();
services.AddHttpClient();
services.AddScoped<IAuth0TokenService, Auth0TokenService>();
Step 3 - Set the token in session in options
options.Events.OnTokenValidated = context =>
{
var accessToken = context.TokenEndpointResponse?.AccessToken;
var refreshToken = context.TokenEndpointResponse?.RefreshToken;
var expiresInStr = context.TokenEndpointResponse?.ExpiresIn;
var httpContext = context.HttpContext;
if (!string.IsNullOrEmpty(accessToken))
httpContext.Session.SetString("Auth0AccessToken", accessToken);
if (!string.IsNullOrEmpty(refreshToken))
httpContext.Session.SetString("Auth0RefreshToken", refreshToken);
if (!string.IsNullOrEmpty(expiresInStr) && double.TryParse(expiresInStr, out var expiresIn))
{
httpContext.Session.SetString("Auth0TokenExpiry",
DateTime.UtcNow.AddSeconds(expiresIn).ToString("O"));
}
return Task.FromResult(0);
};
Step 4 - Fetch in the controller var token = _auth0TokenService.GetValidAccessTokenAsync().Result;
5 Answer(s)
-
0
Hi @kansoftware
Your current implementation for fetching the Auth0 access token on the API side can work in principle, but in ASP.NET Zero (and in modern OIDC best practices in general) this approach has some security and scalability concerns.
Storing Access/Refresh Tokens in Session
- While ASP.NET Core Session data is stored server side, managing it in a distributed cache setup can become tricky. Also, storing long lived and high privilege credentials like refresh tokens in session is not recommended. Instead, consider secure storage (e.g., a secure DB, Azure Key Vault) or have the SPA/mobile app store and manage them using the Auth0 SDK.
Managing refresh tokens on the backend
- In OIDC best practices, refresh tokens are usually kept for backend to backend flows. For user specific flows, it’s better for the SPA/mobile app to handle token renewal and send a new access token to the API.
Tenant based Auth0 domain
- You currently have a hard coded
tenantDomain. In a multi tenant scenario, this should come from tenant configuration. In ASP.NET Zero, you could use something like a customITenantAppConfigurationServiceto store Auth0 details per tenant.
OpenIdConnect Middleware
- In ASP.NET Zero, you can override
OnTokenValidatedorOnAuthorizationCodeReceivedinOpenIdConnectEventsto capture tokens. However, integrating this into theExternalAuthManagerlayer will make it easier to maintain long term.
Server side Access Token usage
- If the API itself needs to call Auth0 protected APIs, it’s safer to use the Client Credentials Flow with tenant specific
client_idandclient_secret. This removes the need to store refresh tokens entirely.
Token renewal
- For user specific requests, let the SPA/mobile app renew the token via MSAL/Auth0 SDK and send it in the
Authorizationheader. The API should only validate the incoming token, not manage refresh tokens.
Store tenant specific Auth0 settings in appsettings or the database. Use Client Credentials Flow for the API to get its own access token from Auth0. Keep user access tokens on the SPA side, renewed via Auth0 SDK, and pass them in the request headers.
Storing refresh tokens in session is not secure or scalable long term. Avoid hard coded domains use tenant configuration. Prefer Client Credentials Flow or SPA side token renewal. Integrate with ASP.NET Zero’s
ExternalAuthManagerfor better maintainability.Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
[oguzhanagir] said: tokens
More than calling it for mobile app, I want to use it in web application. I am integrating a third party where I will be sending this token. Could you please guide me by steps to achieve this, keeping in mind the scalability and security.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @kansoftware
Decide whether you must send the user’s access token (representing the user) or a server to server token (your API’s token) to the third party. Best practice: if the third party can accept a token issued to the same Auth0 that the user logged in with, you can forward the user token only if the third party trusts that token. Otherwise, do a token exchange Client Credentials or On Behalf Of (OBO) pattern from your backend and send that token instead. This keeps refresh tokens out of user sessions and centralizes trust.
Decide which token to send
- Option A User token (delegated): send the user’s access token if the third party trusts Auth0 tokens and scopes match what they expect. Use Authorization Code flow for login.
- Option B Server token (recommended in many cases): your backend obtains a token for the third party API via Client Credentials (or token exchange/OBO if the call must be on behalf of a user). Then your server calls the third party with that server token client credentials are easier to secure and scale.
If uncertain, prefer Option B. It’s easier to secure and avoids passing high value tokens to clients.
Authentication flows (what to implement)
User login (web app): use Authorization Code (with PKCE if you have SPA parts). In ASP.NET Core use
OpenIdConnectmiddlewareAddOpenIdConnect.Backend calling third party:
- If Client Credentials fits (no user context required): call Auth0
/oauth/tokenwith tenantclient_id/client_secretandaudience=third_party_apito get a token for the third party. - If you need actions on behalf of the user and the third party supports OBO/token exchange: perform a token exchange (Auth0 supports some delegation token exchange options via extensions or custom rules) or use an OBO flow if supported.
- If you must forward the user token, verify the third party accepts it and the token has the required
scope.
- If Client Credentials fits (no user context required): call Auth0
Tenant configuration
- Store per tenant Auth0 settings in a secure config store (database or config service):
{Auth0Domain, ClientId, ClientSecret, Audience}for each tenant. - On each request, resolve tenant context (ASP.NET Zero has tenant resolution). Use that to pick the right client id/secret/issuer.
Token caching & storage (scalability + security)
Do not store refresh tokens in unencrypted session. Avoid session for long term secrets.
Use a server side token cache (Redis or DB) keyed by tenant+user (or tenant only for client credentials).
- Encrypt secrets at rest (use a data encryption key).
- Prefer short TTLs and refresh on expiry.
Use
IHttpClientFactoryto call token endpoints and third parties (connection reuse, resilience).Optionally use a token library like IdentityModel for token caching refresh logic.
Secure secret management
- Store
client_secretin a secrets store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault). Never check into source or put in plainappsettings.json. - Use managed identities for your API where possible to access the secrets store.
Implement token retrieval logic (high level)
Resolve tenant from incoming request.
If server token (client credentials):
- Check cache for tenant specific token.
- If missing/expired, POST to
https://{tenantDomain}/oauth/tokenwithgrant_type=client_credentials,client_id,client_secret,audience. - Cache returned
access_tokenwith its expiry.
If user token must be forwarded:
- Validate the user token on the server.
- Optionally exchange it for a token the third party expects (token exchange OBO), then send exchanged token.
Use token in
Authorization: Bearer <token>header when calling third party.
- Keep tokens short lived. Use refresh only server side and protect refresh tokens strongly.
- Protect cookies:
HttpOnly,Secure,SameSite=Strictwhere possible. - Validate tokens on every call (issuer, audience, signature, expiry).
- Implement least privilege scopes for tokens you request.
- Log token errors only (don’t log tokens themselves).
- Rotate client secrets periodically and support secret rollover.
Example code (server token client credentials)
// Resolve tenantConfig (domain, clientId, clientSecret, audience) var cacheKey = $"thirdparty_token:{tenantId}:{audience}"; if (!TryGetFromCache(cacheKey, out TokenCacheEntry token) || token.IsExpired) { var client = _httpClientFactory.CreateClient(); var req = new HttpRequestMessage(HttpMethod.Post, $"https://{tenantDomain}/oauth/token") { Content = new FormUrlEncodedContent(new Dictionary<string,string>{ ["grant_type"] = "client_credentials", ["client_id"] = tenantConfig.ClientId, ["client_secret"] = tenantConfig.ClientSecret, ["audience"] = tenantConfig.Audience }) }; var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync<Auth0TokenResponse>(); token = new TokenCacheEntry { AccessToken = body.AccessToken, ExpiresAt = DateTime.UtcNow.AddSeconds(body.ExpiresIn) }; SaveToDistributedCache(cacheKey, token, token.Ttl); } // Use token.AccessToken in Authorization header var apiClient = _httpClientFactory.CreateClient("thirdparty"); apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); var result = await apiClient.GetAsync("/the/endpoint");- Keep token logic in a scoped service (e.g.
IThirdPartyTokenProvider) and inject it where needed. - Tenant resolution: use your existing tenant context services to fetch tenant config.
- Do not put refresh token logic in
HttpContext.Session. Use a secure server side store if you must persist refresh tokens.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
[oguzhanagir] said: Hi @kansoftware
Decide whether you must send the user’s access token (representing the user) or a server to server token (your API’s token) to the third party. Best practice: if the third party can accept a token issued to the same Auth0 that the user logged in with, you can forward the user token only if the third party trusts that token. Otherwise, do a token exchange Client Credentials or On Behalf Of (OBO) pattern from your backend and send that token instead. This keeps refresh tokens out of user sessions and centralizes trust.
Decide which token to send
- Option A User token (delegated): send the user’s access token if the third party trusts Auth0 tokens and scopes match what they expect. Use Authorization Code flow for login.
- Option B Server token (recommended in many cases): your backend obtains a token for the third party API via Client Credentials (or token exchange/OBO if the call must be on behalf of a user). Then your server calls the third party with that server token client credentials are easier to secure and scale.
If uncertain, prefer Option B. It’s easier to secure and avoids passing high value tokens to clients.
Authentication flows (what to implement)
User login (web app): use Authorization Code (with PKCE if you have SPA parts). In ASP.NET Core use
OpenIdConnectmiddlewareAddOpenIdConnect.Backend calling third party:
- If Client Credentials fits (no user context required): call Auth0
/oauth/tokenwith tenantclient_id/client_secretandaudience=third_party_apito get a token for the third party. - If you need actions on behalf of the user and the third party supports OBO/token exchange: perform a token exchange (Auth0 supports some delegation token exchange options via extensions or custom rules) or use an OBO flow if supported.
- If you must forward the user token, verify the third party accepts it and the token has the required
scope.
- If Client Credentials fits (no user context required): call Auth0
Tenant configuration
- Store per tenant Auth0 settings in a secure config store (database or config service):
{Auth0Domain, ClientId, ClientSecret, Audience}for each tenant. - On each request, resolve tenant context (ASP.NET Zero has tenant resolution). Use that to pick the right client id/secret/issuer.
Token caching & storage (scalability + security)
Do not store refresh tokens in unencrypted session. Avoid session for long term secrets.
Use a server side token cache (Redis or DB) keyed by tenant+user (or tenant only for client credentials).
- Encrypt secrets at rest (use a data encryption key).
- Prefer short TTLs and refresh on expiry.
Use
IHttpClientFactoryto call token endpoints and third parties (connection reuse, resilience).Optionally use a token library like IdentityModel for token caching refresh logic.
Secure secret management
- Store
client_secretin a secrets store (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault). Never check into source or put in plainappsettings.json. - Use managed identities for your API where possible to access the secrets store.
Implement token retrieval logic (high level)
Resolve tenant from incoming request.
If server token (client credentials):
- Check cache for tenant specific token.
- If missing/expired, POST to
https://{tenantDomain}/oauth/tokenwithgrant_type=client_credentials,client_id,client_secret,audience. - Cache returned
access_tokenwith its expiry.
If user token must be forwarded:
- Validate the user token on the server.
- Optionally exchange it for a token the third party expects (token exchange OBO), then send exchanged token.
Use token in
Authorization: Bearer <token>header when calling third party.
- Keep tokens short lived. Use refresh only server side and protect refresh tokens strongly.
- Protect cookies:
HttpOnly,Secure,SameSite=Strictwhere possible. - Validate tokens on every call (issuer, audience, signature, expiry).
- Implement least privilege scopes for tokens you request.
- Log token errors only (don’t log tokens themselves).
- Rotate client secrets periodically and support secret rollover.
Example code (server token client credentials)
// Resolve tenantConfig (domain, clientId, clientSecret, audience) var cacheKey = $"thirdparty_token:{tenantId}:{audience}"; if (!TryGetFromCache(cacheKey, out TokenCacheEntry token) || token.IsExpired) { var client = _httpClientFactory.CreateClient(); var req = new HttpRequestMessage(HttpMethod.Post, $"https://{tenantDomain}/oauth/token") { Content = new FormUrlEncodedContent(new Dictionary<string,string>{ ["grant_type"] = "client_credentials", ["client_id"] = tenantConfig.ClientId, ["client_secret"] = tenantConfig.ClientSecret, ["audience"] = tenantConfig.Audience }) }; var resp = await client.SendAsync(req); resp.EnsureSuccessStatusCode(); var body = await resp.Content.ReadFromJsonAsync<Auth0TokenResponse>(); token = new TokenCacheEntry { AccessToken = body.AccessToken, ExpiresAt = DateTime.UtcNow.AddSeconds(body.ExpiresIn) }; SaveToDistributedCache(cacheKey, token, token.Ttl); } // Use token.AccessToken in Authorization header var apiClient = _httpClientFactory.CreateClient("thirdparty"); apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); var result = await apiClient.GetAsync("/the/endpoint");- Keep token logic in a scoped service (e.g.
IThirdPartyTokenProvider) and inject it where needed. - Tenant resolution: use your existing tenant context services to fetch tenant config.
- Do not put refresh token logic in
HttpContext.Session. Use a secure server side store if you must persist refresh tokens.
Below are the steps I am thinking to follow, kindly let me know where I can improve to achieve the above
- On options.Events.OnTokenValidated will fetch the access token, expiry and refresh token and store in the AWS Redis cache with appropriate time
- Will create a Auth0TokenService class, which will fetch the tokens from Redis and validate. If access token is expired then will fetch new using the refresh token.
- In my API will call the Auth0TokenService class and fetch the valid auth0 access token and use.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @kansoftware
The steps you’re planning can work, but in ASP.NET Zero ABP architecture and according to OIDC security best practices, there are a few important improvements I’d recommend.
Storing the refresh token in Redis
- Redis is a good choice for distributed caching in ASP.NET Zero, but since a refresh token is a high value credential, you should always store it encrypted (e.g., using the Data Protection API or KMS / Key Vault).
- Set the TTL in Redis carefully base it on the access token’s lifetime for the cache key, not the refresh token lifetime.
Prefer Client Credentials if possible
- If the third party API calls are on behalf of your API (not a specific user), you can skip refresh tokens and use Client Credentials flow instead.
- In a multi tenant scenario, store each tenant’s
{Auth0Domain, ClientId, ClientSecret, Audience}in the database and cache them. - This avoids storing refresh tokens entirely and removes user session dependency.
ASP.NET Zero integration
- Capturing the token in
OnTokenValidatedis fine, but handle token renewal in a scoped service (e.g.,IThirdPartyTokenProvider) instead of middleware. - This ensures all API endpoints share the same centralized token management logic for a tenant.
- In ABP/Zero, you can use
ITenantResolveContributorto gettenantIdfrom the request, and name your Redis keys liketenant:{id}:auth0token.
Token refresh logic
In your service, follow this flow:
- Read the access token from Redis.
- If it’s about to expire, refresh it using the refresh token.
- Save the new token back to Redis.
- Use it in your API request as
Authorization: Bearer <token>.
Never store refresh tokens in plaintext.
Keep
client_secretvalues out of appsettings; use a Secret Manager Key Vault instead.Ensure
audiencematches what the third party API expects.If possible, use Client Credentials flow no refresh tokens needed.
If you must use refresh tokens, store them encrypted in Redis and manage renewal in a centralized service.
In a multi tenant setup, use tenantId scoped cache keys.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)