@leisurelink/trusted-endpoint

Nodejs utility classes for participating in LeisureLink's federated security as a trusted endpoint.

Usage no npm install needed!

<script type="module">
  import leisurelinkTrustedEndpoint from 'https://cdn.skypack.dev/@leisurelink/trusted-endpoint';
</script>

README

trusted-endpoint

Nodejs utility classes for participating in LeisureLink's federated security as a trusted endpoint.

Background

Reference to Key Terms

LeisureLink's federated security is a claims-based security model. Key definitions and terminology can be found in the readme's for auth-context and claims.

Scope of this Module

This module's purpose is to encapsulate the data and logic necessary for an endpoint to recognize trusted calls and establish the authority of each security principal involved in an operation.

It is important to recognize that all applications have local authority. In other words, an application always operates within its own authority.

If an application is callable, such as an API, in the scope of responding to such call, the application should establish the caller's authority. We refer to the caller's authority as remote authority.

There are two primary kinds of users in LeisureLink's federated security; system users and human users. System users can be many kinds of things; they may be other micro-services in our service-inventory, they may be websites we maintain, they may be partner API's that call our own API's. Regardless of the nature of the system, when it participates in our trust-model we call it a trusted-endpoint.

Any system that makes use of LeisureLink's platform must do so via a trusted-endpoint. When this requirement is met, it means that our systems only accept trusted-calls.

If a trusted-call is being made in response to a human user's activity, such as when website activity flows through to our API's, those trusted-calls should include the end-user's auth-token. By decoding the user's auth-token, a trusted-endpoint establishes user authority.

trusted-endoint recognizes trusted-calls and may establish the following:

  • local-authority – represents the local process's authority.
  • remote-authority – represents the remote process's authority.
  • user-authority – represents the end user's authority within the system.

Install

npm install --save @leisurelink/trusted-endpoint

Use

Plugins/Middleware

Import It

var endpoint = require('@leisurelink/trusted-endpoint');

All subsequent examples assume this import!

API

@leisurelink/trusted-endpoint exports the following classes:

  • TrustedEndpoint – A utility class for reasoning about trust in relation to an HTTP Signature and LeisureLink's federated security and claims model.
  • TrustedEndpointCache – A subclass of TrustedEndpoint extended in order to support a short-term, local-memory cache for resolved and trusted public keys and claims.
  • KeyId – A utility class for parsing key identifiers in HTTP Signatures and reasoning about the parts that make up a key identifier in LeisureLink's federated security.
  • MalformedKeyIdError – A specialized error class thrown when a parsed KeyId is malformed.

@leisurelink/trusted-endpoint may be initialized and used as a singleton. Such use is discouraged.

.create(options, useAsSingleton)

Creates a new instance of the TrustedEndpoint class using the specified options. Optionally uses the create instance as the module's singleton.

arguments:

  • options : object, required – an object specifying:
    • keyId : string or KeyId, required – key identifier used to authenticate the current process as a system principal/trusted-endpoint. See KeyId later in this readme.
    • scope : AuthScope, required – an authorization scope used by the endpoint to verify auth-tokens.
    • resolveEndpointKey : function, required – a function with signature resolveEndpointKey(lang, keyId): Promise; invoked when the endpoint needs to lookup the private key identified by keyId. See Endpoint Key Resolver later in this readme.
    • resolveEndpointClaims : function, required – a function with signature resolveEndpointClaims(lang, principalId): Promise; invoked when the endpoint needs to resolve another trusted endpoint's claims. See Endpoint Claims Resolver later in this readme.
    • lang : string, required – the BCP 47 language code identifying the language used when resolving endpoint claims.
    • useCache: bool, optional – indicates whether the instance should cache resolved keys and claims. Default: false.
    • keyCacheTimeoutSeconds: intenger, optional – the lifespan of cached keys. Default: 0 - forever.
    • localAuthTimeoutSeconds: intenger, optional – the lifespan of the local endpoint's authorization in the cache. Default: 15 minutes.
    • remoteAuthTimeoutSeconds: intenger, optional – the lifespan of the remote endpoint's authorization in the cache. Default: 5 minutes.
  • useAsSingleton : bool, optional – indicates whether the create instance should be used as the module's singleton.

returns: A new TrustedEndpoint instance.

example:

var auth = require('@leisurelink/auth-context');
var trusted = require('@leisurelink/trusted-endpoint');

let options = {
  issuer: 'test',                      // JWT issuer we trust
  audience: 'test',                    // JWT audience we expect
  issuerKeyFile: './test/test-key.pub' // The issuer's public key, so we can verify the issuer's digital signature
};

let scope = new auth.AuthScope(options);
let endpoint = trusted.create({
  keyId: 'my/key',
  scope,
  resolveEndointKey: function (lang, keyId) { /* hrm, gotta resolve the key here! */ },
  resolveEndointClaims: function (lang, principalId) { /* hrm, gotta resolve the claims here! */ }
})

.singleton()

Gets the TrustedEndpoint instance that keeps the module's state.

NOTE: It is an error to use the module as-a TrustedEndpoint prior to initializing the module for such use. This restriction is enforced by direct and indirect calls to the .singleton() method. Initialization requires a call to .create(options, true).

returns:

  • The TrustedEndpoint instance acting as the module's singleton.

.getLocalAuth()

Gets the current process' local authority as an instance of AuthContext

returns:

  • A Promise that is resolved with an instance of AuthContext, created on the current process' auth-token, if such can be verified, otherwise the promise is rejected with an appropriate error.
endpoint.getLocalAuth()
  .then(ctx => {
    console.log(`Got claims for ${ctx.principalId} that are valid until ${ctx.expiresAt}.`);
  })
  .catch(err => {
    console.log(`Oops, something unexpected happened: ${err}.`);
  });

.getRemoteAuth(request)

For the specified HTTP request, gets the authority associated with the request.

arguments:

returns:

  • A Promise that is resolved with a success object upon success and rejected with an error if the operation fails. The success object has the following properties:
    • remoteEndpoint: an instance of AuthContext representing the remote authority. This member is only present if the request bore a valid Authorization header.
    • remoteUser: an instance of AuthContext representing the user authority. This member is only present if the request was accompanied with a human user's auth-token.

examples:

var AuthContext = require('@leisurelink/auth-context').AuthContext;

// ...

endpoint.getRemoteAuth(request)
  .then(res => {
    if (AuthContext.isContext(res.remoteEndpoint)) {
      let ep = res.remoteEndpoint;
      console.log(`Remote endpoint is a trusted ${ep.kind}: ${ep.principalId}.`);
    }
    if (AuthContext.isContext(res.remoteUser)) {
      let usr = res.remoteUser;
      console.log(`Remote user is a trusted ${usr.kind}: ${usr.principalId}.`);
    }
  })
  .catch(err => {
    console.log(`Oops, something unexpected happened: ${err}.`);
  });

Endpoint Key Resolver

An endpoint key resolver is a callback function specified as an option when creating new TrustedEndpoint instances. The resolver is called when a key needs to be resolved.

arguments:

  • lang : string, required – the BCP 47 language code identifying the suggested language for any textual output rendered for a human user.
  • keyId : string or KeyId, required – the key's identity. This value may be an instance of KeyId. In either case it is coercible to a string.

returns:

  • A promise, resolved with the public key in PEM format upon success, otherwise rejected with the error that occurred.

example:

var assert = require('assert');
var fs = require('fs');
var path = require('path');

var AuthenticClient = require('@leisurelink/authentic-client');
var trusted = require('@leisurelink/trusted-endpoint');

let keyFile = path.normalize(path.join(__dirname, '../test/test-key.pem'));
let key = fs.readFileSync(keyFile);

let client = new AuthenticClient('http://localhost:2999', 'my/key', key);

function resolveEndpointKey(lang, keyId) {
  assert.ok(typeof(lang) === 'string', 'lang must be specified');
  assert.ok(typeof(keyId) === 'string' || keyId instanceof trusted.KeyId, 'lang must be specified');
  keyId = (typeof(keyId) === 'string') ? trusted.KeyId.parse(keyId) : keyId;
  return new Promise((resolve, reject) => {
    client.getEndpointKey(lang, keyId.principalId, keyId.keyId, function(err, res, body) {
      if (err) {
        reject(err);
      } else {
        resolve(body.result);
      }
    });
  });
}

Endpoint Claims Resolver

An endpoint claim resolver is a callback function specified as an option when creating new TrustedEndpoint instances. The resolver is called when an endpoint's claims need to be resolved.

arguments:

  • lang : string, required – the BCP 47 language code identifying the suggested language for any textual output rendered for a human user.
  • principalId : string, required – the endpoint's identity.

returns:

  • A promise, resolved with an auth-token upon success, otherwise rejected with an error.

example:

var assert = require('assert');
var fs = require('fs');
var path = require('path');

var AuthenticClient = require('@leisurelink/authentic-client');
var trusted = require('@leisurelink/trusted-endpoint');

let keyFile = path.normalize(path.join(__dirname, '../test/test-key.pem'));
let key = fs.readFileSync(keyFile);

let client = new AuthenticClient('http://localhost:2999', 'my/key', key);

function resolveEndpointClaims(lang, principalId) {
  return new Promise((resolve, reject) => {
    client.getEndpointClaims(lang, principalId, function(err, res, body) {
      if (err) {
        reject(err);
      } else {
        resolve(body.result);
      }
    });
  });
}