Communicate with a custom API endpoint in Umbraco 14 – Part 1

Umbraco API endpoint

Communicating with a custom API through Typescript in Umbraco 14 was something which caused my team quite a few issues, considering the general lack of documentation regarding these subjects so far, so I figured I’d write a blog to document how I did it. Is it the perfect way? Probably not, but it might help someone out there who’s also looking to implement their own custom API in Umbraco 14.

Our case

Setting up a new website currently costs us a lot of time in simple, repetitive manual tasks such as creating the Document types we use in every installation, so we wanted to look into automating this process. Our end goal was to write an application that communicates remotely with the Umbraco 14 management API and automatically creates the Document Types, Dictionary Items etc. which we always use to a new project. Early on in the process, I ran into the issue of the bearer token that is needed for authentication. Sure, one can connect to the API on the new installation using e.g. Postman and copy the bearer token, but I figured it’d be more convenient if our sending application would be able to authenticate itself. Therefore, I needed to be able to create my own custom client ids which I could use to authenticate my application to communicate with the Management API.

The solution (or the start of it)

I was able to find the existing Auth0 client ids in the ‘umbracoOpenIddictApplications’ database table of Umbraco, but I wasn’t able to use any of those considering I needed to set a custom return URL, this needed to be the URL of my sending application. So, I decided to write a package I could install to the new installations, which would allow me to add custom OpenID applications which I could then use to authenticate myself when calling the Management API from the sending application.

First thing I decided to do was to create a Dashboard which displayed the existing OpenID applications and their return URLs. I’m not able to use any of these, but I wanted to have a neat overview which I could then add a ‘create’ button and endpoint to which I could use to add custom OpenID applications. This blog will cover creating this dashboard overview, since I already ran into quite a few issues here. I still have to actually write the adding custom OpenID applications to the dashboard and using those to authenticate, but I might document those steps as I go in future blogs.

Typescript Dashboard

Creating an Umbraco dashboard would usually not be much of a struggle, except for that Umbraco 14 has made some major changes to the backoffice, and I’d now have to write my custom OpenId Applications dashboard in Typescript. The dashboard itself could be created through manifest files which referred to a Typescript application which would render the HTM. The dashboard would be pretty useless without data in it though, and I wasn’t able to directly connect to the database through Typescript as one would be able to do in Umbraco controllers.

Umbraco comes with a lot of premade database repositories one can use in Typescript, but these are generally only for the entities and objects you can actually edit in the Umbraco backoffice already. For example, one can import the UserGroup repository to Typescript and then retrieve all user groups from it:

import { type UmbUserGroupDetailModel, UmbUserGroupCollectionRepository } from '@umbraco-cms/backoffice/user-group';

#userGroupRepository = new UmbUserGroupCollectionRepository(this);

const {data: groupRepositoryData} = await this.#userGroupRepository.requestCollection();
const groupData: Array<UmbUserGroupDetailModel> = groupRepositoryData?.items ?? [];

Unfortunately, this wasn’t the case for the OpenID Applications. Since these weren’t editable in the backoffice yet, no repository existed for them either. So, the first thing I had to do was to make the data from the ‘umbracoOpenIddictApplications’ database table available through the Umbraco API.

Extending the API

Making the data available through an API call was simple and could be handled in Umbraco controllers entirely. I created a Model for the database entity, a Service that’d actually communicate with the database and a Controller which added a custom API endpoint. This endpoint doesn’t do more than pull the entity data out of the database, map it to the model and return it to be accessed through the API and Swagger.

public class Application
{
    public string ClientId { get; set; }
    public string DisplayName { get; set; }
    public string RedirectUris { get; set; }
    public string Permissions { get; set; }
}
[ApiController]
[ApiVersion("1.0")]
[MapToApi("openid-api-v1")]
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[JsonOptionsName(Constants.JsonOptionsNames.BackOffice)]
[Route("api/v{version:apiVersion}/openid")]
public class ApplicationsApiController(IScopeProvider scopeProvider): Controller
{
    [HttpGet("get-applications")]
    [MapToApiVersion("1.0")]
    [ProducesResponseType(typeof(IList<Application>), StatusCodes.Status200OK)]
    public IEnumerable<Application> GetApplications()
    {
        return await applicationService.GetApplicationAsync(applicationId);

    }
}
public async Task<Application> GetApplicationAsync(string applicationId)
{
    using var scope = scopeProvider.CreateScope();
    var queryResults = await scope.Database.FetchAsync<Application>("SELECT * FROM umbracoOpenIddictApplications where UPPER(Id)=UPPER(@applicationId)", new {applicationId});
    scope.Complete();
    return queryResults.First();
}

To test if it worked, I opened Swagger and was able to successfully retrieve the data.

API client

So, the data could be accessed, but now I had to access it through Typescript. The API has authorization, so a simple Get request to the endpoint wasn’t going to work, I needed to authenticate through Typescript somehow, and this is something the Umbraco 14 docs were still completely blank on.

By browsing through some of the Umbraco 14 backoffice code and some packages which had already been updated to Umbraco 14 I was able to figure out how to authenticate myself through Typescript, so here’s the first thing I had to do:

I installed an NPM package called “openapi-typescript” to the Project where I was building my Typescript dashboard. The package can be found here, and it can be used to generate an API client based on your Swagger output: https://www.npmjs.com/package/openapi-typescript

In its configuration, you can set it to directly pull from your Custom API definition or link to a .JSON file containing it, in case you didn’t set up Swagger docs.

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
    client: 'legacy/fetch',
    input: "https://localhost:44310/umbraco/swagger/openid-api-v1/swagger.json",
    output: {
        format: 'prettier',
        path: 'src/api',
    },
    services: {
        asClass: true,
    },
    schemas: {
        type: 'json',
    },
    types: {
        enums: 'javascript',
    },
});

You can then add a command to your package.json to kick off the API client generation, my full package.json for the Typescript dashboard now looked like this:

{
  "name": "openidapplications",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "watch": "vite build --watch",
    "generate": "openapi-ts"
  },
  "dependencies": {
    "lit": "^3.2.0"
  },
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.53.0",
    "@umbraco-cms/backoffice": "^14.2.0",
    "typescript": "^5.5.4",
    "vite": "^5.4.1"
  }
}

Executing ‘npm run generate’ now kicks off the openapi-typescript package.

The package created the whole api folder for me:

This ended up being just what I was looking for, because while browsing through the Umbraco backoffice code I was able to find their API client, but it was protected and I wasn’t allowed to use it from my Typescript code. This package prevented me from having to copy it all by hand, and now when I update my custom API endpoint or add more custom API endpoints, I can just run the ‘npm run generate’ command again and it’ll update the client and schemas according to the updated Swagger documentation.

API client authentication

I now had my API client ready, but I still wasn’t actually authorized to use it. The package had created a class, but calling it from my Typescript controller still resulted in a 401 response.

export class V1Service {
    /**
     * @returns unknown OK
     * @throws ApiError
     */
    public static getApiV1OpenidGetApplications(): CancelablePromise<GetApiV1OpenidGetApplicationsResponse> {
        return __request(OpenAPI, {
            method: 'GET',
            url: '/api/v1/openid/get-applications',
            errors: {
                401: 'The resource is protected and requires an authentication token'
            }
        });
    }
   
}
connectedCallback() {
    super.connectedCallback();
    V1Service.getApiV1OpenidGetApplications().then(applications => {
        this.createTableItems(applications)
    });
}

Again, this part wasn’t documented (yet), so I had to dig into the Umbraco backoffice and packages to figure out where to pull the backoffice authentication data from. I ended up with the following solution:

I created an index.ts that is used as an entry file in my Vite config file. This index file has an entrypoint that is called on initialization.

import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { manifests } from './manifests.js';
import {UmbEntryPointOnInit} from "@umbraco-cms/backoffice/extension-api";
import { OpenAPI } from './src/api';
export const onInit: UmbEntryPointOnInit = (_host, extensionRegistry) => {
    // register the manifests
    extensionRegistry.registerMany(manifests);

    _host.consumeContext(UMB_AUTH_CONTEXT, (_auth) => {
        const umbOpenApi = _auth.getOpenApiConfiguration();
       
        OpenAPI.TOKEN = umbOpenApi.token;
        OpenAPI.BASE = umbOpenApi.base;
        OpenAPI.WITH_CREDENTIALS = umbOpenApi.withCredentials;
    });
};

The entrypoint pulls the authentication data from the UMB_AUTH_CONTEXT, which we can access by importing it into our Typescript file. We then store this data in the OpenAPI object that was generated by the openapi-typescript package. The package uses this OpenAPI object for authentication in the API client, so with the authentication data provided on initialization, it was able to perform the API calls successfully.

Final step, mapping the data to Typescript

Using the API client calls and the models it had generated for me, I was able to map the custom API endpoint data to my table items, and render it in my custom dashboard.

import {Application, V1Service} from "../src/api";

@state()
private _tableConfig: any = {
    allowSelection: false,
};

@state()
private _tableColumns: Array<any> = [
    {
        name: 'Client Id',
        alias: 'clientId',
    },
    {
        name: 'Display Name',
        alias: 'displayName',
    },
    {
        name: 'Redirect Uris',
        alias: 'redirectUris',
    },
    {
        name: 'Permissions',
        alias: 'permissions',
    }
];

@state()
private _tableItems: Array<any> = [];

connectedCallback() {
    super.connectedCallback();
    V1Service.getApiV1OpenidGetApplications().then(applications => {
        this.createTableItems(applications)
    });
}

createTableItems(applications: Array<Application>) {
    this._tableItems = applications.map((application) => {
        return {
            id: application.clientId ?? '',
            icon: 'icon-lock',
            data: [
                {
                    columnAlias: 'clientId',
                    value: application.clientId,
                },
                {
                    columnAlias: 'displayName',
                    value: application.displayName,
                },
                {
                    columnAlias: 'redirectUris',
                    value: application.redirectUris,
                },
                {
                    columnAlias: 'permissions',
                    value: application.permissions,
                }
            ],
        };
    });
}

What now?

I still need to write the part that allows me to add new OpenAPI Applications, and then actually use that new Application to authenticate while calling the management API. But, now that I have this framework with a working API client set up, I at least have a foundation to build on and it should be easier going from here on out. I might write a follow-up blog if I feel like that might help some more people out in the future!

As mentioned at the start of the blog, this might not be the perfect or most efficient way to go about it, but with the current lack of documentation it might help push someone else in the right direction. Happy coding!

  • Bernadet Goey, Umbraco Developer @ ilionx