Basic API – Coding Key Points

Background

Previously we covered SPA Coding Key Points for the first code sample. In this post we’ll drill into some important identity related code in the API.

Code Layout

The API uses Node.js Express as the HTTP server and is coded in TypeScript. The business functionality is in a logic folder and the plumbing, including OAuth handling, is in the host folder:

Dependencies

Use of third party libraries has been kept fairly simple, and the most interesting dependency in the package.json file is the jose security library, used to validate JWTs:

"dependencies": {
  "cors": "^2.8.5",
  "express": "^4.18.2",
  "fs-extra": "^11.1.1",
  "jose": "^5.0.1",
  "on-headers": "^1.0.2",
  "https-proxy-agent": "^5.0.1",
  "winston": "^3.8.2"
}

API Entry Point

When npm start is run, the startup/app.ts class executes, which configures the Express HTTP server and then starts listening for requests:

(async () => {

    const logger = new ApiLogger();
    try {

        const configBuffer = await fs.readFile('api.config.json');
        const configuration = JSON.parse(configBuffer.toString()) as Configuration;

        const expressApp = express();
        const httpServer = new HttpServerConfiguration(expressApp, configuration, logger);
        await httpServer.initializeApi();

        httpServer.initializeWebStaticContentHosting();
        httpServer.startListening();

    } catch (e) {

        const error = ErrorFactory.fromServerError(e);
        logger.startupError(error);
    }
})();

The startup logic is in the HttpServerConfiguration class, whose main role is to define the following aspects:

  • How each REST URL is processed, via the get expressions
  • How cross cutting concerns are handled, via the use expressions
public async initializeApi(): Promise<void> {

    const corsOptions = {
        origin: this._configuration.api.trustedOrigins,
        maxAge: 86400,
    };
    this._expressApp.use('/api/*', cors(corsOptions) as any);
    this._expressApp.use('/api/*', this._apiController.onWriteHeaders);

    this._expressApp.use('/api/*', this._catch(this._apiLogger.logRequest));
    this._expressApp.use('/api/*', this._catch(this._apiController.authorizationHandler));

    this._expressApp.get('/api/companies', this._catch(this._apiController.getCompanyList));
    this._expressApp.get(
        '/api/companies/:id/transactions',
        this._catch(this._apiController.getCompanyTransactions));

    this._expressApp.use('/api/*', this._apiController.onRequestNotFound);
    this._expressApp.use('/api/*', this._apiController.onException);
}

The API then continues by configuring an HTTP listener, after which it runs indefinitely.

API Configuration

The API uses a configuration file in a similar manner to the SPA, and this includes details used for JWT validation, as discussed shortly:

{
    "api": {
        "port": 80,
        "trustedOrigins": [
            "http://web.mycompany.com"
        ],
        "useProxy": false,
        "proxyUrl": "http://127.0.0.1:8888"
    },
    "oauth": {
        "jwksEndpoint": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_CuhLeqiE9/.well-known/jwks.json",
        "algorithm": "RS256",
        "issuer": "https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_CuhLeqiE9",
        "audience": ""
    }
}

Web Static Content Hosting

Our sample API also uses Express to host web static content, so that our SPA’s files can be downloaded to the browser. This is done for the following reasons:

  • Demonstrating  that a web host should require only minimal code
  • Reducing the number of parts so that the back end is easier to run
public initializeWebStaticContentHosting(): void {

    this._express.use('/spa', express.static('../spa'));
    this._express.use('/favicon.ico', 
}

In the real world you would use a separate web host component, and this will often be a Content Delivery Network. The Final SPA will switch to using a dedicated web host.

Service Logic Classes

The most mainstream coding model for API technology stacks involves receiving API requests into a controller class, then calling other testable business logic classes.

In our first Node.js sample we will wire up these classes manually via an ApiController class, though we will use dependency injection in later API code samples:

public async getCompanyList(request: Request, response: Response): Promise<void> {

    const reader = new JsonFileReader();
    const repository = new CompanyRepository(reader);
    const service = new CompanyService(repository, response.locals.claims);

    const result = await service.getCompanyList();
    ResponseWriter.writeObjectResponse(response, 200, result);
}

API is Non Blocking

Whenever the API performs I/O, as for the below CompanyRepository class, it does so using a standard async / await coding model, so that other code can execute on the current thread while I/O is in progress:

public async getCompanyTransactions(id: number): Promise<CompanyTransactions | null> {

    const companyList = await this._jsonReader.readData<Company[]>('data/companyList.json');
    const foundCompany = companyList.find((c) => c.id === id);
    if (foundCompany) {

        const companyTransactions =
            await this._jsonReader.readData<CompanyTransactions[]>('data/companyTransactions.json');

        const foundTransactions = companyTransactions.find((ct) => ct.id === id);
        if (foundTransactions) {
            foundTransactions.company = foundCompany;
            return foundTransactions;
        }
    }

    return null;
}

Authorization Middleware

Express uses a middleware terminology for describing functions that handle cross cutting concerns. The first of these in our code sample is to validate the JWT access token before allowing business logic to run:

public async authorizationHandler(
    request: Request,
    response: Response,
    next: NextFunction): Promise<void> {

    const claims = await this._accessTokenValidator.execute(request);
    response.locals.claims = claims;
    next();

There are two main responsibilities involved in authorization in an OAuth secured API. The second of these occurs in the business logic:

Responsibility Description
Authenticate Requests Digitally verify received JWT access tokens and return a 401 response if not valid
Perform Authorization Trust claims from the payload of the JWT and use them for authorization

Authenticating Requests

The API uses an ‘AccessTokenValidator‘ class to do the work for the first of the above tasks. The OAuth token verification is done by the security library, which only requires a little code:

public async execute(request: Request): Promise<ClaimsPrincipal> {

    try {

        const accessToken = this._readAccessToken(request);
        if (!accessToken) {
            throw ErrorFactory.fromMissingTokenError();
        }

        const options = {
            algorithms: [this._configuration.algorithm],
            issuer: this._configuration.issuer,
            audience: this._configuration.audience,
        };
        const result = await jwtVerify(accessToken, this._jwksRetriever.remoteJWKSet, options);

        const userId = this._getClaim(result.payload.sub, 'sub');
        const scope = this._getClaim(result.payload['scope'], 'scope');
        return new ClaimsPrincipal(userId, scope.split(' '));

    } catch (e: any) {

        if (e.code === 'ERR_JOSE_GENERIC') {
            throw ErrorFactory.fromJwksDownloadError(e);
        }

        throw ErrorFactory.fromTokenValidationError(e);
    }
}

The API is responsible for providing correct inputs to the library, so it is important for API developers to understand these, which are summarised in a separate Access Token Validation page.

Claims Principal

After the JWT has been digitally verified, its claims can be trusted and used by the API’s business logic, to authorize access to resources. The claim values are provided by an object that we will call a ‘ClaimsPrincipal‘:

export class ClaimsPrincipal {

    private _subject: string;
    private _scopes: string[];

    public constructor(subject: string, scopes: string[]) {
        this._subject = subject;
        this._scopes = scopes;
    }

    public get subject(): string {
        return this._subject;
    }

    public get scopes(): string[] {
        return this._scopes;
    }
}

Authorizing Requests

The code sample then injects the claims principal into its business logic classes as follows:

export class CompanyService {

    private readonly _repository: CompanyRepository;
    private readonly _claims: ClaimsPrincipal;

    public constructor(repository: CompanyRepository, claims: ClaimsPrincipal) {
        this._repository = repository;
        this._claims = claims;
    }
}

If an attempt is made to access unauthorized data then a forbidden error can be returned:

private _unauthorizedError(companyId: number): ClientError {
    return new ClientError(
        404,
        ErrorCodes.companyNotFound,
        `Company ${companyId} was not found for this user`);
}

Claims Based Authorization

For the first code sample default claims are used, but any real world system will need to use claims from business data in order to implement their authorization. We will drill into this topic in future posts.

API Logs

The initial API code sample does some basic logging, where each API request writes a ‘log entry‘ in JSON format, represented by the following class:

export class LogEntry {

    public readonly _utcTime: Date;
    public _path: string;
    public _method: string;
    public _statusCode: number;
    public _error: ClientError | ServerError | null;
}

By default this leads to lightweight output as follows:

API Error Handling

The API also performs solid error handling, with these main classes:

Class Description
ServerError Represents a technical problem in the API
ClientError Represents a useful error response for an API client
ExceptionHandler Catches any exceptions thrown by the API
ErrorFactory Used to create exceptions to throw
ErrorCodes A list of error causes

An unhandled exception handler is used to deal with adding error details to logs and returning a useful response to the SPA:

public static handleError(exception: any, response: Response): ClientError {

    const handledError = ErrorFactory.fromException(exception);
    if (exception instanceof ClientError) {

        const clientError = handledError as ClientError;
        const logEntry = response.locals.logEntry as LogEntry;
        logEntry.setError(clientError);
        return clientError;

    } else {

        const serverError = handledError as ServerError;
        const logEntry = response.locals.logEntry as LogEntry;
        logEntry.setError(serverError);
        return serverError.toClientError();
    }
}

Errors are included in logs and any technical problems, such as when contacting OAuth endpoints, are output with a 500 status and include extra details to help with fast problem resolution:

Supportability

Future posts will drill much deeper into logging and error handling. These are high value areas for back end systems, and people productivity is improved when they are implemented in a solid end-to-end manner.

Where Are We?

The initial API integrates with an Authorization Server and validates JWTs correctly. The API also does some initial work on claims, logging and error handling, and we will do more work on these areas for future samples.

Next Steps