Accessing the Content Delivery API endpoints when logged in with the built-in authentication system

Vote:
 

Hi,

We have a CMS 12 setup that uses the Content Delivery API (version 3.6.0), where we want all the API endpoints to require authentication – preferably restricted to specific roles.

This is easily accomplished by setting the DisableScopeValidation option to false when adding the API:

services.AddContentDeliveryApi(currentAuthScheme, options =>
{
    options.DisableScopeValidation = false;
});

By default, this will require a scope (see https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles?tabs=aspnetcore for info about scopes) called "epi_content_delivery", but other scopes can be added by means of "options.AllowedScopes.Add("some_scope")".

If nothing else is done, no users at all are able to acces the API. (A response header will complain about a missing scope.) Thus, users must somehow be assigned scopes.

For authentication methods that are set up with an expilcit call to the AddCookie() method, it was possible with some experimentation to find a way of doing that through the OnSigningIn event. For example, with WsFederation:

services
    .AddAuthentication("signin-scheme")
    .AddCookie("cookie-scheme", options =>
    {
        options.Events.OnSigningIn = async context =>
        {
            // For example, assign the "epi_content_delivery" scope to any user who has the "WebAdmins" role
            if (context.Principal?.Identity is ClaimsIdentity claimsIdentity &&
                claimsIdentity.Claims.Any(c => c.Type.EndsWith("role") && c.Value == "WebAdmins") &&
                claimsIdentity.Claims is IList<Claim> claims)
            {
                claims.Add(new Claim("scope", "epi_content_delivery"));
            }
            await Task.CompletedTask;
        }
        options.Events.OnSignedIn = async context =>
        {
            if (context.Principal?.Identity is ClaimsIdentity claimsIdentity)
            {
                var synchronizingUserService = context.HttpContext.RequestServices.GetRequiredService<ISynchronizingUserService>();
                await synchronizingUserService.SynchronizeAsync(claimsIdentity, new[] { "scope" });
            }
        };
    })
    .AddWsFederation("signin-scheme", options =>
    {
        options.SignInScheme = "cookie-scheme";
        options.SignOutScheme = "cookie-scheme";
        options.MetadataAddress = "https://...xml";
        options.Wtrealm = "http...";
    });

With the above code in Startup.cs, users who are authenticated with WsFederation and have the WebAdmins role will be able to access the Content Delivery API.

However, how can the same thing be accomplished for users who are signed in with the built-in authentication – "EPi_AspNetIdentityUserProvider", added with "services.AddCmsAspNetIdentity<ApplicationUser>()"? With the Content Delivery API for CMS 11, it was easy, since the API options allowed roles that could access the API to be specified directly. But with CMS 12, we have to specify scopes instead.

Maybe I am missing something that is obvious to people with better understanding of scopes in ASP.NET Core, but I cannot find a way of adding the "epi_content_delivery" scope (or any other scope) to the claims associated with users authenticated by "EPi_AspNetIdentityUserProvider".

Presumably, there are ways of overriding various stages of the "EPi_AspNetIdentityUserProvider" authorization handling, but I do not know where to start. Also, for a seemingly simple task like this, we would prefer a simple solution, similar to the OnSigningIn event code that works for WsFederation – i.e. preferably not something where several default authorization handling classes need to be wrapped and customized.

It would have been good if https://docs.developers.optimizely.com/content-cloud/v1.5.0-content-delivery-api/docs/breaking-changes-version-3 that states "MinimumRoles: Obsoleted. Use ContentDeliveryApiOptions.AllowedScopes instead to control who can make API calls." had included an example of a simple way of achieving that in connection with the built-in CMS user management.

#296466
Feb 13, 2023 22:30
Vote:
 

I think you should be able to use an IniializationModule to update this.  I did not test but think this may work.

public void Initialize(InitializationEngine context)
{
      var options = context.Locate.Advanced.GetInstance<IOptionsMonitor<CookieAuthenticationOptions>>().Get(IdentityConstants.ApplicationScheme);
      if (options != null)
      {
          options.Events.OnSigningIn = async context =>
         {
              // For example, assign the "epi_content_delivery" scope to any user who has the "WebAdmins" role
              if (context.Principal?.Identity is ClaimsIdentity claimsIdentity &&
                claimsIdentity.Claims.Any(c => c.Type.EndsWith("role") && c.Value == "WebAdmins") &&
                claimsIdentity.Claims is IList<Claim> claims)
              {
                   claims.Add(new Claim("scope", "epi_content_delivery"));
              }
               await Task.CompletedTask;
          }
       }
 }
#296502
Feb 13, 2023 23:19
Vote:
 

Note that scopes is an additional way of controlling access to the API. By enabling scope validation, you're basically removing anonymous access to the API. Roles (role claims) are evaluated against the content's access control list still, so you can remove the 'Everyone' role's read access from the content to achieve the same, but on a more granuarly level.

Scopes should be requested up-front before autenticating, i.e. auth should fail if the user is not allowed to request the scope.

#296516
Feb 14, 2023 13:44
Vote:
 

Thanks a lot – hopefully the solution by Mark Hall works – we will have to test it in practice to verify.

Johan, “removing anonymous access to the API” is exactly what we want. There is no need for it in our setup, and some publicly accessible content items have data fields that are for backend use only that we do not want to expose to anonymous users. While those fields can be filtered out with an IContentFilter implementation, it seems like a good idea to also disable anonymous API access when it is not needed.

Still, it is useful for developers to be able to access the API endpoints for debugging and inspection (while being logged in). That is what I hoped to achieve for "EPi_AspNetIdentityUserProvider"-authenticated users. (For WsFederation-authenticated users it already works fine.)

I am not sure what “Scopes should be requested up-front before autenticating” means in this context. The users may already be signed in (e.g. for accessing edit mode) when they access the API. Is it simply a statement to explain that the API will request a scope before the authentication of a specific individual HTTP request takes place? Still, if the user did not receive a claim for the scope when they signed in, the authentication of the API request will by necessity fail, right? (Once again, hopefully Mark’s code solves that by adding a claim for the scope upon sign-in.)

#296520
Feb 14, 2023 14:48
Vote:
 

I can confirm that Mark Hall’s code sample works as expected. Thank you!

#296619
Feb 15, 2023 23:15
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.