App Authentication Token Refresh
This page provides sample code for setting up your ASP.NET Core server application to refresh App tokens for GeoBlazor Pro. Once you have retrieved the ArcGIS token, call await authenticationManager.RegisterToken(tokenResponse.AccessToken, tokenResponse.Expires)
to register the token with the AuthenticationManager
class. See App Authentication for more details on how to set up your GeoBlazor App credentials.
Authentication Service
An authentication service class can be created to handle the retrieval and caching of ArcGIS tokens. Caching is important to avoid unnecessary requests and to ensure that the token is valid when needed. There are many ways to cache the token, but for simplicity, this example uses a file-based cache.
/// <summary>
/// Service for managing ArcGIS authentication tokens.
/// </summary>
public class ArcGisAuthService(HttpClient httpClient, IConfiguration config)
{
/// <summary>
/// Requests a new ArcGIS token or retrieves a cached one if available and not expired.
/// </summary>
public async Task<TokenResponse> GetTokenAsync(bool forceRefresh)
{
if (!forceRefresh)
{
TokenResponse? cachedToken = await GetCachedTokenAsync();
if (cachedToken != null)
{
return cachedToken;
}
}
return await RequestTokenAsync();
}
/// <summary>
/// Retrieves a cached ArcGIS token if it exists and is not expired.
/// </summary>
private async Task<TokenResponse?> GetCachedTokenAsync()
{
await _semaphore.WaitAsync();
try
{
string? cacheFilePath = config["ArcGISAppTokenCacheFile"];
if (!File.Exists(cacheFilePath))
{
return null;
}
string json = await File.ReadAllTextAsync(cacheFilePath);
TokenResponse? token = JsonSerializer.Deserialize<TokenResponse>(json);
if (token is null || token.Expires <= DateTimeOffset.UtcNow)
{
return null;
}
return token;
}
finally
{
_semaphore.Release();
}
}
/// <summary>
/// Caches the provided ArcGIS token to a file.
/// </summary>
private async Task CacheTokenAsync(TokenResponse token)
{
string json = JsonSerializer.Serialize(token);
await _semaphore.WaitAsync();
try
{
await File.WriteAllTextAsync(config["ArcGISAppTokenCacheFile"]!, json);
}
finally
{
_semaphore.Release();
}
}
/// <summary>
/// Requests a new ArcGIS token using client credentials.
/// </summary>
private async Task<TokenResponse> RequestTokenAsync()
{
var tokenUrl = "https://www.arcgis.com/sharing/rest/oauth2/token";
var parameters = new Dictionary<string, string>
{
{ "f", "json" },
{ "client_id", config["ArcGISAppId"] ?? "" },
{ "client_secret", config["ArcGISClientSecret"] ?? "" },
{ "grant_type", "client_credentials" },
{ "expiration", config["ArcGISTokenExpirationMinutes"] ?? "1440" }
};
var request = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
{
Content = new FormUrlEncodedContent(parameters)
};
HttpResponseMessage response = await httpClient.SendAsync(request);
string content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return new TokenResponse(false, null, null,
"Request failed with status code: " + response.StatusCode);
}
ArcGisError? errorCheck = JsonSerializer.Deserialize<ArcGisError>(content);
if (errorCheck?.Error != null)
{
return new TokenResponse(false, null, null,
$"Error {errorCheck.Error.Code}: {errorCheck.Error.Message}");
}
ArcGISTokenResponse? token = JsonSerializer.Deserialize<ArcGISTokenResponse>(content);
if (token?.AccessToken == null)
{
return new TokenResponse(false, null, null, "Access token is null in response");
}
TokenResponse tokenResponse = new TokenResponse(true, token.AccessToken,
DateTimeOffset.UtcNow.AddSeconds(token.ExpiresIn));
await CacheTokenAsync(tokenResponse);
return tokenResponse;
}
private static readonly SemaphoreSlim _semaphore = new(1, 1);
}
/// <summary>
/// The response from the request for an ArcGIS token, including success notification and error messages.
/// </summary>
public record TokenResponse(bool Success, string? AccessToken, DateTimeOffset? Expires, string? ErrorMessage = null);
/// <summary>
/// JSON representation of the ArcGIS token response.
/// </summary>
public record ArcGISTokenResponse
{
[JsonPropertyName("access_token")]
public string? AccessToken { get; init; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; init; }
}
/// <summary>
/// JSON representation of an error response from the ArcGIS API.
/// </summary>
public record ArcGisError(ErrorDetails? Error);
public record ErrorDetails(int Code, string? Message);
Background Service
A background service can be set up to periodically refresh the ArcGIS token. This is useful for ensuring that the token remains valid without requiring user intervention.
public class BackgroundTokenRefreshService(IServiceProvider serviceProvider, IConfiguration config) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = serviceProvider.CreateScope())
{
var authService = scope.ServiceProvider.GetRequiredService<ArcGisAuthService>();
TokenResponse tokenResponse = await authService.GetTokenAsync(true);
if (!tokenResponse.Success)
{
// Handle error (e.g., log it)
}
}
int refreshInterval = int.Parse(config["ArcGISTokenRefreshIntervalMinutes"] ?? "1440");
await Task.Delay(TimeSpan.FromMinutes(refreshInterval), stoppingToken);
}
}
}
User Page
This is an example of how to use the ArcGisAuthService
in a Blazor page to display a map with the authenticated token. Be aware, however, you would not want to use this from a Blazor WebAssembly page. Instead, you would use an HttpClient
call to a WebAPI endpoint on your server, that in turn calls the ArcGisAuthService
to get the token.
@page "/map"
@if (!_isInitialized)
{
<p>Loading...</p>
}
else
{
<MapView Class="map-view">
<Map>
<Basemap>
<PortalItem PortalItemId="your-portal-item-id" />
</Basemap>
</Map>
</MapView>
}
@code {
[Inject]
protected required ArcGisAuthService AuthService { get; set; }
[Inject]
protected required AuthenticationManager AuthenticationManager { get; set; }
// GeoBlazor AuthenticationManager cannot initialize until after the first render, when JavaScript interop is available.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Get the token from the authentication service
TokenResponse tokenResponse = await AuthService.GetTokenAsync(false);
if (tokenResponse.Success && tokenResponse.AccessToken != null)
{
// Register the token with the GeoBlazor AuthenticationManager
// This will allow the GeoBlazor maps to use the token for authentication
await AuthenticationManager.RegisterToken(tokenResponse.AccessToken, tokenResponse.Expires);
_isInitialized = true;
StateHasChanged(); // Trigger a re-render to show the map
}
else
{
// Handle error (e.g., show a message to the user)
Console.WriteLine($"Error retrieving token: {tokenResponse.ErrorMessage}");
}
}
}
private bool _isInitialized = false;
}
WebAssembly Startup
Here is a Program.cs
example for a Blazor WebAssembly application that calls to a server API to retrieve the token on startup, and registers it with the AuthenticationManager
.
using dymaptic.GeoBlazor.Core;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Configuration.AddInMemoryCollection();
builder.Services.AddGeoBlazor(builder.Configuration);
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
string token = await httpClient.GetStringAsync("api/auth/token");
if (!string.IsNullOrEmpty(token))
{
using (IServiceScope scope = builder.Services.BuildServiceProvider().CreateScope())
{
var authenticationManager = scope.ServiceProvider.GetRequiredService<AuthenticationManager>();
// Register the token with the AuthenticationManager
await authenticationManager.RegisterToken(token, DateTimeOffset.UtcNow.AddMinutes(60));
}
httpClient.Dispose();
}
else
{
throw new InvalidOperationException("Failed to retrieve authentication token.");
}
await builder.Build().RunAsync();