Logo

dev-resources.site

for different kinds of informations.

Mastering Authorization in Umbraco 14/15: Real-World Management API Challenges and Solutions

Published at
11/4/2024
Categories
umbraco
headless
umbraco14
managementapi
Author
muhammed_zaid_afreed
Author
20 person written this
muhammed_zaid_afreed
open
Mastering Authorization in Umbraco 14/15: Real-World Management API Challenges and Solutions

When I first started working with Umbraco's Management API, I was both excited and curious. This API promises headless management capabilities that can truly elevate automation and the integration of custom apps with Umbraco's backend. But as with many new tools, the real challenge emerged when I tried to implement it beyond the well-documented test environments.

In this article, I'll share the lessons I learned while trying to navigate the tricky landscape of Management API authorization in both local and production setups. Whether you're an experienced Umbraco developer or just getting started, I hope this helps you understand not only how to get things working but also why the current state of documentation can make it such a difficult journey.

Understanding the Management API Authorization

The Management API is a significant evolution for Umbraco, replacing the previous backoffice controllers with a more robust, RESTful approach. While its core functions are well-documented, the process of getting authorization tokens and managing secure access is not without its quirks, especially if you're looking beyond Swagger or Postman(https://docs.umbraco.com/umbraco-cms/reference/management-api/postman-setup-swagger).
Source : (https://docs.umbraco.com/umbraco-cms/reference/management-api)

The Swagger documentation for the Management API is primarily enabled in non-production environments. This means you can use it effectively for testing purposes, leveraging OAuth2 with PKCE in Postman or Swagger itself. However, when you move to a production environment, where Swagger is disabled, things become considerably more challenging. Only the 'umbraco-back-office' client is allowed in production, requiring careful handling to avoid conflicts and missing documentation coverage.

In production, Swagger isn't available, and the documentation leaves a lot of gaps for developers trying to set up a connection with OpenID Connect. Only the "umbraco-back-office" client is allowed to connect, while in non-production, you can use clients like "umbraco-swagger" or "umbraco-postman". This approach is understandable from a security perspective, but it introduces hurdles when setting up custom client integrations or ensuring a smooth workflow for deployments.

Missing Pieces: Local and Production Authorization

The current documentation provided by Umbraco is helpful for the initial setup in non-production environments, with clear instructions on using Postman to connect a backoffice user through OAuth2. However, if you're trying to replicate this authorization process in a local development environment or—more critically—in a production environment, you quickly find yourself without a map. Additionally, the documentation is not complete at the moment and may be updated by Umbraco HQ later, potentially when they have more concrete plans for the Management API, as they have recently introduced API users in Umbraco 15.

I recently set up authorization for the Management API on both local and production environments, and it became apparent that a lot of the steps were undocumented or required piecing together information from different parts of the Umbraco community. The official documentation focuses heavily on Swagger and Postman, which is ideal for testing but not quite enough when dealing with actual client applications or custom workflows.

For instance, in local environments, getting OpenID Connect to work smoothly often required manually adjusting configurations to align with non-production rules. Swagger and Postman setups would default to using "umbraco-swagger" or "umbraco-postman" as the client_id, which isn't valid in local production contexts. Moreover, in production, ensuring secure access meant diving into OAuth2 flows without a client secret—something not explicitly covered for most local and production scenarios in the documentation.

Authorization Pain Points and Workarounds

One of the key issues I encountered was trying to use the "umbraco-back-office" client as the client ID. You can specify the callback URL in the appsettings under Umbraco:CMS:Security:AuthorizeCallbackPathName, but there's a significant problem: the Umbraco backoffice uses the same client, which results in a broken callback and disrupts the backoffice login flow. Additionally, this issue is not documented, which makes troubleshooting even more challenging.
After investigating this matter, the best approach for now is to extend the OpenIdDictApplicationManagerBase and create a new client. By doing so, you can create a dedicated client for your integration without interfering with the default backoffice client, thus avoiding conflicts.

appsetting.json

{
  "BaseUrl": "https://localhost:44329",
  "ClientId": "newclientId", // generated through /create-client
  "AuthorizationEndpoint": "/umbraco/management/api/v1/security/back-office/authorize",
  "TokenEndpoint": "/umbraco/management/api/v1/security/back-office/token",
  "RedirectUri": "https://localhost:44329/callback"
}

Enter fullscreen mode Exit fullscreen mode

Here is a simplified version of my setup, using Minimal API to create a custom application manager:

using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using UmbDemo14.Web;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Infrastructure.Security;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddDeliveryApi()
    .AddComposers()
    .Build();

builder.Services.AddScoped<CustomApplicationManager>(provider =>
{
    var applicationManager = provider.GetRequiredService<IOpenIddictApplicationManager>();
    return new CustomApplicationManager(applicationManager);
});
builder.Services.AddHttpClient();
builder.Services.AddSession();

builder.Services.AddTransient<Auth>();

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAllOrigins",
        policy =>
        {
            policy.AllowAnyOrigin()   // Allows requests from any origin
                  .AllowAnyHeader()   // Allows any header
                  .AllowAnyMethod();  // Allows any HTTP method (GET, POST, etc.)
        });
});

WebApplication app = builder.Build();

await app.BootUmbracoAsync();

app.UseSession();
app.UseCors("AllowAllOrigins");

app.UseUmbraco()
    .WithMiddleware(u =>
    {
        u.UseBackOffice();
        u.UseWebsite();
    })
    .WithEndpoints(u =>
    {
        u.UseBackOfficeEndpoints();
        u.UseWebsiteEndpoints();
    });

app.MapPost("/create-client", async (ClientModel model, CustomApplicationManager applicationManager) =>
{
    try
    {
        if (string.IsNullOrEmpty(model.ClientId))
            return Results.BadRequest("Client ID is required.");

        if (!Uri.TryCreate(model.RedirectUri, UriKind.Absolute, out var redirectUri))
            return Results.BadRequest("Invalid redirect URI.");

        await applicationManager.EnsureCustomApplicationAsync(model.ClientId, redirectUri);
        return Results.Ok("Client created/updated successfully.");
    }
    catch (Exception ex)
    {
        return Results.Problem(ex.Message);
    }
}).WithName("CreateClient");

app.MapGet("/login", (Auth auth, IConfiguration config, IBackOfficeApplicationManager backOfficeApplicationManager) =>
{
    var baseUrl = config["Umbraco:BaseUrl"];
    var authorizationUrl = auth.GetAuthorizationUrl();
    return Results.Redirect(baseUrl + authorizationUrl);
});

app.MapGet("/callback", async (Auth auth, HttpContext httpContext, IConfiguration configuration) =>
{
    var code = httpContext.Request.Query["code"];
    var state = httpContext.Request.Query["state"];
    if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
    {
        return Results.BadRequest("Invalid callback parameters");
    }
    try
    {
        var tokenResponse = await auth.HandleCallback(code, state);
        // Store the token securely
        httpContext.Response.Cookies.Append("UmbracoToken", tokenResponse, new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict
        });
        return Results.Redirect("/dashboard");
    }
    catch (Exception ex)
    {
        return Results.BadRequest($"Authentication failed: {ex.Message}");
    }
});

await app.RunAsync();

public class ClientModel
{
    public string ClientId { get; set; }
    public string RedirectUri { get; set; }
    //public string ClientSecret { get; set; } // only use if it's a confidential app
}
Enter fullscreen mode Exit fullscreen mode

And here is the implementation for the CustomApplicationManager class:
Source : (https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs)

using OpenIddict.Abstractions;
using Umbraco.Cms.Infrastructure.Security;

namespace UmbDemo14.Web
{
    public class CustomApplicationManager : OpenIdDictApplicationManagerBase
    {
        public CustomApplicationManager(IOpenIddictApplicationManager applicationManager)
            : base(applicationManager)
        {
        }

        public async Task EnsureCustomApplicationAsync(string clientId, Uri redirectUri, CancellationToken cancellationToken = default)
        {
            if (redirectUri.IsAbsoluteUri == false)
            {
                throw new ArgumentException("The provided URL must be an absolute URL.", nameof(redirectUri));
            }

            var clientDescriptor = new OpenIddictApplicationDescriptor
            {
                DisplayName = "Custom Application",
                ClientId = clientId,
                RedirectUris = { redirectUri },
                ClientType = OpenIddictConstants.ClientTypes.Public, // change to confidential for client secret
                Permissions = {
                    OpenIddictConstants.Permissions.Endpoints.Authorization,
                    OpenIddictConstants.Permissions.Endpoints.Token,
                    OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
                    OpenIddictConstants.Permissions.ResponseTypes.Code
                }
            };

            await CreateOrUpdate(clientDescriptor, cancellationToken);
        }
        public async Task DeleteCustomClientApplicationAsync(string clientId, CancellationToken cancellationToken)
        {
            await Delete(clientId, cancellationToken);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Moving Forward: How We Can Improve

Umbraco's Management API is a powerful tool, but like any tool, its full potential can only be realized if users can understand how to wield it effectively. I'd like to see Umbraco expand its documentation to include more practical, step-by-step guides for setting up the Management API in both local and production environments. This should include:

  • Detailed Walkthroughs for Production Authorization: Covering OAuth2 setup, best practices for security, and using "umbraco-back-office" in a production scenario without Swagger.

  • Local Environment Tips: Providing more explicit instructions on how to translate Swagger/Postman authorizations into a local setup. This would help reduce confusion for developers working in a hybrid workflow.

By addressing these areas, Umbraco can significantly enhance the developer experience, ultimately making it easier for the community to adopt, extend, and innovate with the Management API.

Conclusion

The Management API's current state is a solid foundation, but authorization remains an area where there's room for improvement. Developers working with local and production environments need more than just Swagger or Postman examples; they need a complete guide to setting up secure and flexible integrations. I'm hopeful that with continued community feedback and contributions, Umbraco's documentation will grow to cover these gaps, making the Management API more accessible to everyone.

headless Article's
30 articles in total
Favicon
India’s Best Headless SEO Company
Favicon
CMS: Payload CMS
Favicon
Mastering Authorization in Umbraco 14/15: Real-World Management API Challenges and Solutions
Favicon
Disable Frontend in Headless Wordpress
Favicon
What is New for Developers in Strapi 5: Top 10 Changes
Favicon
WordPress Headless + CPT + ACF: Building a Flexible Content Platform
Favicon
Built with Astro, Crystallize, and Stripe (Nerd Streetwear Online Store) Part I
Favicon
Strapi or WordPress: How to Decide Which CMS is Right for You
Favicon
Announcing a Storyblok Loader for the Astro Content Layer API
Favicon
Building the Frontend with Astro and Integration with Stripe (Nerd Streetwear Online Store) Part II
Favicon
Using Headless Woo commerce store api v1 in nextjs : issue faced and solutions
Favicon
Headless Browser Test: What Is It and How to Do It?
Favicon
7 Awesome Multi-Tenant Features in Headless CMS That'll Make Your Life Easier
Favicon
Making headless components easy to style
Favicon
How to Survive If You Still Have a Traditional CMS
Favicon
Demystifying Headless Commerce: Exploring Top Solutions and Their Benefits
Favicon
Comparing Sitecore XP (.NET) and Sitecore XM Cloud (TypeScript): Solr vs. GraphQL for Queries
Favicon
How to Build an E-commerce Store with Sanity and Next.js
Favicon
Getting to Know ButterCMS
Favicon
How to Set Up a Headless WordPress Site with Astro
Favicon
Utilize React Native's Headless JS for developing advanced features! 🚀
Favicon
Looking to Build Your 1st Headless or JamStack site?
Favicon
Moving to WP Headless
Favicon
Announcing Live Preview for Storyblok’s Astro Integration
Favicon
Save Time Building Jamstack / Headless CMS Backed Websites
Favicon
An Unforgettable Experience at UDLA: Exploring Sitecore XM Cloud and Headless Development
Favicon
Incerro Partners with Sanity to Accelerate Scalable Digital Solution Development
Favicon
Customize Your Hashnode Blog Frontend with Headless Frontend and Laravel
Favicon
Generate Types for your components with the Storyblok CLI!âš¡
Favicon
What are headless UI libraries?

Featured ones: