Securing REST endpoints with API keys

Goal: Implement a simple REST-Service and secure it via API keys.

You can download the whole Kickstarter-based sample application from our GitLab repository: um-public/tutorial-securing-rest-apis

This tutorial requires UM version 7.56 or newer.

Start a new Kickstarter project

First, choose a Java-style package name for your new plugin – e.g. de.acme.tutorial. (See also ACME Corporation)
Please note that com..., net... and org... denote reserved namespaces and can not be used as top-level package names.

Second, use UM-Kickstarter to begin a new UM project:

umkickstarter -n de.acme.tutorial cd umkickstarter gradle setup run

Implement REST controller

Create an .mjs file in your plugin’s rest/ folder:

cmsbs-conf/cse/plugins/de.acme.tutorial/rest/api.mjs

/// <reference path="../../../.vscode.js"/> import { RouterBuilder } from "@de.pinuts.apirouter/shared/routing.es6"; /** * @param {HttpRequest} req * @param {HttpResponse} res */ const listEntries = (req, res) => { res.send({ data: UM.query('entrytype="customer"') .page(100) .map(e => e.toSeedJSON()) }); } /** * @param {HttpRequest} req * @param {HttpResponse} res */ const createEntry = (req, res) => { const e = UM.addEntry(null, 'customer'); e.set('firstname', req.jsonRequestBody.firstname); e.set('lastname', req.jsonRequestBody.lastname); e.set('email', req.jsonRequestBody.email); UM.commitEntries(); res.send({ data: e.toSeedJSON() }, 201); } de.acme.tutorial.apiController = new RouterBuilder() .protectFromCaching() .get('/entries', listEntries) .post('/entries', createEntry) .build();

Restart your UM instance:

gradle run

Invoke REST service

Try to invoke the new REST service in your browser:

http://localhost:8080/cmsbs/rest/de.acme.tutorial.api/entries

Open the UM’s backoffice UI and manually create some Customer entries. Invoke the above REST service again. You should be presented with a JSON formatted list of your newly created customer entries.

Try to create a new customer entry by performing a POST request via curl:

Secure your REST endpoint

As you may have noticed, there is currently no authentication required, so virtually everybody could call that very endpoint and create or query entries.

Since UM 7.56 you can create and manage API keys in the UM’s backoffice and use them to authenticate (and authorize) callers of your APIs.

Modify your RouterBuilder from above and retry the before mentioned GET and POST requests:

By calling .requireApiKey() you tell the RouterBuilder to expect an Authorization request header containing valid Basic Auth credentials.

Try retrieving the list of entries again:

This will yield a 401 Unauthorized!

Create API key

Go to http://localhost:8080/cmsbs/Custom/s_/de.pinuts.cmsbs.apitoken/index and create a new API key:

image-20240704-122019.png

Click Create and copy Key and Secret from the following screen…

image-20240704-122120.png

…and use them as USERNAME and PASSWORD when invoking your REST endpoint:

Since the secret key is stored as a hash digest in the database, it cannot be viewed again at a later time.

If you should happen to loose your secret key, simply delete the API key and create a new one.

Add more specific authorization checks

If we want to be more specific as to what permissions are required to call your endpoint, we can introduce and check for project specific permissions by adding them to our cmsbs-conf/cse/plugins/de.acme.tutorial/plugin.desc.json file:

This will introduce the following new API permissions that we can assign to our API key:

  • de.acme.tutorial:ListEntries

  • de.acme.tutorial:CreateEntry

Restart your UM instance and visit http://localhost:8080/cmsbs/Custom/s_/de.pinuts.cmsbs.apitoken/index again to specifically assign the newly added de.acme.tutorial.ListEntries permission to your API key:

Now, modify your RouterBuilder once again to check for the appropriate permissions in both of the routes:

With the permissions from the screenshot above, you should be able to retrieve the entry list…

… but should be denied to create a new entry: