How to fetch protected content from the Delivery API with a server-to-server approach.
If protected content is consumed from the Delivery API in a server-to-server context, the interactive authorization flow won't work. Instead, we have to utilize the OpenId Connect Client Credentials flow, which is configured in the application settings.
Configuration
In the Delivery API, Client Credentials map known Members to client IDs and secrets. These Members are known as API Members. When an API consumer uses the Client Credentials of an API Member, the consumer efficiently assumes the identity of this API Member.
An API Member works the same as a regular Member, with the added option of authorizing with Client Credentials.
In the following configuration example, the Member "member@local" is mapped to a set of Client Credentials:
After restarting the site, the backoffice will list "member@local" as an API Member:
Authorizing and consuming the Delivery API
The configured Client Credentials can be exchanged for an access token using the Delivery API token endpoint. Subsequently, the access token can be used as a Bearer token to retrieve protected content from the Delivery API.
The following code sample illustrates how this can be done.
You should always reuse access tokens for the duration of their lifetime. This will increase performance both for your Delivery API consumer and for the Delivery API itself.
The code sample handles token reuse in the ApiAccessTokenService service. It must be registered as a singleton service to work.
In the code sample, the token endpoint is hardcoded in the token exchange request. The Delivery API also supports OpenId Connect Discovery for API Members, if you prefer that.
Program.cs
usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Hosting;usingSystem.Net.Http.Json;usingIdentityModel.Client;var builder =Host.CreateApplicationBuilder(args);builder.Services.AddSingleton<ApiAccessTokenService>();builder.Services.AddTransient<ApiConsumerService>();usingIHosthost=builder.Build();var consumer =host.Services.GetRequiredService<ApiConsumerService>();awaitconsumer.ExecuteAsync();publicstaticclassConstants{ // the base URL of the Umbraco site - change this to fit your custom setuppublicstaticstring Host =>"https://localhost:44391";}// This is the API consumer, which will be listing the first few available content items - including protected ones.publicclassApiConsumerService{privatereadonlyApiAccessTokenService _apiAccessTokenService;publicApiConsumerService(ApiAccessTokenService apiAccessTokenService)=> _apiAccessTokenService = apiAccessTokenService;publicasyncTaskExecuteAsync() { // get an access token from the access token service.var accessToken =_apiAccessTokenService.GetAccessToken();if (accessToken isnull) {Console.WriteLine("Could not get an access token, aborting.");return; }var client =newHttpClient();client.SetBearerToken(accessToken); // fetch [pageSize] content items from the "all content" Delivery API endpoint.constint pageSize =5;var apiResponse =awaitclient.GetAsync($"{Constants.Host}/umbraco/delivery/api/v2/content?take={pageSize}");var apiContentResponse =await apiResponse .EnsureSuccessStatusCode() .Content .ReadFromJsonAsync<ApiContentResponse>();if (apiContentResponse isnull) {Console.WriteLine("Could not parse content from the API response.");return; }Console.WriteLine($"There are {apiContentResponse.Total} items in total - listing the first {pageSize} items.");foreach (var item inapiContentResponse.Items) {Console.WriteLine($"- {item.Name} ({item.Id})"); } }}// This service ensures the reuse of access tokens for the duration of their lifetime.// It must be registered as a singleton service to work properly.publicclassApiAccessTokenService{privatereadonlyLock _lock =new();privatestring? _accessToken;privateDateTime _accessTokenExpiry =DateTime.MinValue;publicstring?GetAccessToken() {if (_accessTokenExpiry >DateTime.UtcNow) { // we already have a token, reuse it.return _accessToken; }using (_lock.EnterScope()) {if (_accessTokenExpiry >DateTime.UtcNow) { // another thread fetched a new token before this thread entered the lock, reuse it.return _accessToken; }var client =newHttpClient();var tokenResponse =client.RequestClientCredentialsTokenAsync(newClientCredentialsTokenRequest { Address =$"{Constants.Host}/umbraco/delivery/api/v1/security/member/token", ClientId ="umbraco-member-my-client", ClientSecret ="my-client-secret" } ) // cannot await inside a using. .GetAwaiter().GetResult();if (tokenResponse.IsError||tokenResponse.AccessTokenisnull) {Console.WriteLine($"Error obtaining a token: {tokenResponse.ErrorDescription}");returnnull; } _accessToken =tokenResponse.AccessToken; _accessTokenExpiry =DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn-20);returntokenResponse.AccessToken; } }}publicclassApiContentResponse{publicrequiredint Total { get; set; }publicrequiredApiContentItemResponse[] Items { get; set; }}publicclassApiContentItemResponse{publicrequiredGuid Id { get; set; }publicrequiredstring Name { get; set; }}