Final Java API – Overview

Background

Previously we described a .NET Coding Model that included some behaviours from this blog’s API Journey – Server Side blog post. Next we will provide an equivalent API in Java and Spring.

Final API Code Sample Overview

As for other code examples on this blog, the API has a simple money based theme of investments. It provides only a couple of simple endpoints that return fictional hard coded data.

The API aims to be visual and can serve this blog’s apps, such as the Final SPA below. Running both together is a good way to ensure that the API is client focused. API logs are then generated via requests from the client:

API URLs

During development the API is run as a standalone component. Either JUnit tests or a load test can be used as API clients. These tests can get user level access token to call the API and verify its OAuth security. This is done by pointing the API to a mock authorization server:

Component Base URL
API https://apilocal.authsamples-dev.com:446
Mock Authorization Server https://login.authsamples-dev.com:447

Prerequisite 1: Domain Setup

First update the hosts file to create domains for local development. Include the web domain if running the SPA and API together:

127.0.0.1 localhost apilocal.authsamples-dev.com login.authsamples-dev.com web.authsamples-dev.com 
::1 localhost

Prerequisite 2: Install Java

We will use the latest long term support version of Java that Spring Boot and the libraries we depend upon support, which is Java 21 at the time of writing.. This blog uses the open source Azul Java SDK.

Prerequisite 3: Optionally Install a Java IDE

I use the free community edition of Intellj Idea for Java based development. The project structure must be uploaded to use the downloaded JDK. Also install the Lombok plugin and optionally the Checkstyle plugin.

Then wait for the gradle sync to complete, which will take a couple of minutes. Then you can run the SampleApiApplication.java file, to run the API and listen for requests at an HTTPS URL.

Prerequisite 4: Optionally Install Node.js

If you want to run the SPA as well as the API, install Node.js for your operating system, which is used to build the SPA’s JavaScript bundles.

Prerequisite 5: Install Docker Desktop

To run API tests, or to run the full SPA and API solution, utility components are run in Docker. Therefore ensure that a local Docker Engine, such as Docker Desktop is installed.

Step 1: Build and Run the API

The API sample is available here and can be be downloaded / cloned to your local PC:

  • git clone https://github.com/gary-archer/oauth.apisample.javaspringboot
  • cd oauth.apisample.javaspringboot

Next run the following script, which downloads some OpenSSL generated development certificates, then uses gradle to run the API on port 446:

./start.sh

Step 2: Configure SSL Trust

The API runs over SSL using a development certificate, and to prevent trust problems from tests or the browser you will need to trust the highlighted root certificate below, by Configuring Browser Trust:

Tests will make HTTPS requests to the Java API, and scripts require a JAVA_HOME environment variable to be configured, which will be a value similar to the following, depending on your operating system:

/usr/lib/jvm/zulu-21-amd64

The root certificate used in HTTPS requests must be trusted by the Java runtime. This is done via the following command:

sudo "$JAVA_HOME/bin/keytool" -import -alias authsamples.ca -cacerts -file ./certs/authsamples-dev.ca.pem -storepass changeit -noprompt

You can revoke trust for the root CA later if required, using this command:

sudo "$JAVA_HOME/bin/keytool" -delete -alias authsamples.ca -cacerts -storepass changeit -noprompt

Step 3: Run Integration Tests

To run JUnit tests, the API must be pointed to a mock authorization server, so must run with a test configuration. Stop the API if it is running, then re-run it via this command:

./testsetup.sh

Next run JUnit tests with the following command:

./gradlew test

This will spin up a mock JWKS URI hosted at HTTPS endpoints provided by the Wiremock utility. A number of tests are then run, which make HTTP requests to the API. These tests focus on the API’s key security behaviour:

Step 4: Run a Basic Load Test

While the API is running with a test configuration, you can also run a basic load test. This test ensures that the API code has no multi-threading bugs, by firing concurrent API requests:

./gradlew loadtest

The test fires 100 HTTP requests to the API, in batches of 5 concurrent requests at a time. This intentionally rehearses certain types of error, and the expected result is 3 errors from a total of 100 requests:

For further details on testing the API with user level tokens but without running a full user login flow, see the  API Automated Tests blog post.

Step 5: Run the SPA Client

If required, run the Final SPA using the following commands, where SPA resources are downloaded to a parallel folder:

cd ..
git clone https://github.com/gary-archer/oauth.websample.final
cd oauth.websample.final
./build.sh LOCALAPI
./run.sh LOCALAPI

The run.sh executes a number of commands in child terminals, to run the default browser at https://web.authsamples-dev.com/. You can then sign into the SPA using this blog’s test credential:

  • User: guestuser@mycompany.com
  • Password: GuestPassword1

The SPA will then make OAuth secured requests to the API, which will result in the API’s code writing JSON logs.

Main Feature 1: Extensible Authorization

The API implements its security according to these two blog posts, using a JOSE library and some custom claims handling:

The overall behaviour should be to deal with OAuth security in the correct ways, while setting up the API’s business logic with the authorization values it needs. These values may originate from multiple data sources and may not always be issued to access tokens:

@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class CompanyService {

    private final CompanyRepository repository;
    private final ClaimsPrincipalHolder claimsHolder;

    public CompanyService(final CompanyRepository repository, final ClaimsPrincipalHolder claimsHolder) {
        this.repository = repository;
        this.claimsHolder = claimsHolder;
    }
}

Main Feature 2: Production Supportability

The other main objective of the API code sample will be JSON logging of API requests, to include useful technical support fields. Logging console output looks like this on a development computer:

{
  "id" : "b8ae0e13-0752-4ce1-ac3c-692c26528855",
  "utcTime" : "2022-12-10T13:30:42.032527148Z",
  "apiName" : "SampleApi",
  "operationName" : "getCompanyTransactions",
  "hostName" : "WORK",
  "method" : "GET",
  "path" : "/investments/companies/4/transactions",
  "resourceId" : "4",
  "clientApplicationName" : "FinalSPA",
  "userId" : "a6b404b1-98af-41a2-8e7f-e4061dc0bf86",
  "statusCode" : 200,
  "millisecondsTaken" : 288,
  "millisecondsThreshold" : 500,
  "correlationId" : "f17ac322-661f-1b26-aba1-8ccc3f55b62c",
  "sessionId" : "004d32bc-9755-b50e-6315-5be09f277ebe"
}

Meanwhile, logs are also written to files in a bare JSON format, ready for log shipping:

The Elasticsearch Log Aggregation Setup can also be followed, to enable  Technical Support Queries against the API. This would be useful in a production system, where there would be many API requests per day.

Cloud Native Deployment

The API would typically be deployed as a container in a cloud native environment. The API includes some Docker and Kubernetes deployment resources. A basic Docker deployment can be run with these commands:

./deployment/docker-local/build.sh
./deployment/docker-local/deploy.sh
./deployment/docker-local/teardown.sh

Where Are We?

We have described the key behaviour of our final Java API. In the next post we will take a closer look at how this has been coded.

Next Steps