@leisurelink/auth-context

LeisureLink client-side authentication & authorization context for node.

Usage no npm install needed!

<script type="module">
  import leisurelinkAuthContext from 'https://cdn.skypack.dev/@leisurelink/auth-context';
</script>

README

auth-context

Client-side convenience classes for decoding, trusting, and reasoning about endpoint and user authority within LeisureLink's federated security.

The auth in auth-context refers to both authentication and authorization — for brevity we refer to these as someone's authority. The actual security primatives encoded in our auth-tokens are security claims, these include facts, roles, and permissions. More precise information about claims is avaiable in the claims module's README.

Install

npm install --save @leisurelink/auth-context

Use

Import It

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

All subsequent examples assume this import!

Create an AuthScope

var 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
};

var scope = new auth.AuthScope(options);

Verifying an auth-token Creates an AuthContext

// assuming we've created a scope...
scope.verify(token, (err, ctx) => {
  if (err) {
    console.log(`Unable to verify the specified auth-token: ${err}`);
  }

  assert.equal(ctx.verified, true);
  assert.equal(ctx.isExpired, false);

  console.log(`token is valid for ${ctx.principalId}(${ctx.email}) until ${ctx.expiresAt}`);
});

Inspect a Principal's Claims (fact example)

// assume app.user is-an AuthContext, such as when using trusted-app...

function userInfoMiddleware(req, res, next) {
  let app = req.app;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json
  // usr claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/usr-claimset-spec.json
  app.user.get('#/sys/em', '#/usr/fn', '#/usr/ln', (err, res) => {
    if (err) {
      next(err);
    }

    res.locals.email = res['#/sys/em'];
    res.locals.firstName = res['#/usr/fn'];
    res.locals.lastName = res['#/usr/ln'];
    next();
  });
}

Check for Principal Role Membership (role example)

// assume app.remoteAuth is-an AuthContext, such as when using trusted-app...

function remoteEndpointPrincipalIsSecurityOfficer(req, res, next) {
  let app = req.app;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json
  app.remoteAuth.has('#/sys/secofr', (err, res) => {
    if (err) {
      next(err);
    }

    res.locals.endpointIsSecurityOfficer = res; // roles are truth values.
    next();
  });
}

Check if Principal Has Permission (permission example)

// assume app.user is-an AuthContext, such as when using trusted-app...

function userPrincipalReadUpdatePermissions(req, res, next) {
  let app = req.app;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json
  // NOTE: we're only querying for read(r) and update(u) permissions. Permissions are a set of flags, refer to the sys claimset spec for the defined permission flags [acrudq].
  app.user.get('#/sys/pri[ru]', (err, res) => {
    if (err) {
      next(err);
    }

    // response (truth) is always in relation to the permissions queried.
    res.locals.canReadPrincipals = res;
    res.locals.canUpdatePrincipals = res;
    next();
  });
}

Identity Claims

A subset of a principal's claims are designated identity claims; these claims are encoded in the principal's auth-token and provide a short-curcuit trust. The above examples may invoke a remote call to the claims authority, thus they are asynchronous. In the case of identity claims, a remote call is not necessary and there are synchronous alternatives provided through the AuthContext.ident property:

Inspect a Principal's Identity Claims (fact)

// assume app.user is-an AuthContext, such as when using trusted-app...

function userInfoMiddleware(req, res, next) {
  let user = req.app.user;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json
  // usr claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/usr-claimset-spec.json
  res.locals.email = user.ident.get('#/sys/em');
  res.locals.firstName = user.ident.get('#/usr/fn');
  res.locals.lastName = user.ident.get('#/usr/ln');
  next();
}

Check for Principal's Identity Role Membership

// assume app.remoteAuth is-an AuthContext, such as when using trusted-app...

function remoteEndpointPrincipalIsSecurityOfficer(req, res, next) {
  let remoteAuth = req.app.remoteAuth;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json

  res.locals.endpointIsSecurityOfficer = remoteAuth.ident.has('#/sys/secofr');
  next();
}

Check Principal's Identity Permissions

// assume app.user is-an AuthContext, such as when using trusted-app...

function userPrincipalReadUpdatePermissions(req, res, next) {
  let user = req.app.user;
  // sys claimset spec: https://github.com/LeisureLink/authentic-api/blob/develop/data/sys-claimset-spec.json
  // NOTE: we're only querying for read(r) and update(u) permissions. Permissions are a set of flags, refer to the sys claimset spec for the defined permission flags [acrudq].

  res.locals.canReadPrincipals = user.ident.get('#/sys/pri[r]');
  res.locals.canUpdatePrincipals = user.ident.get('#/sys/pri[u]');
  next();
}

API

@leisurelink/auth-context defines the following classes:

  • AuthScope – An encapsulation of information identifying an authority that we trust to sign auth-tokens.
  • AuthContext – An encapsulation of a decoded auth-token and provides convenience methods for accessing the authenticated principal associated with the auth-token, as well as that principal's authorization information.

AuthScope Class

The AuthScope class provides a mechanism by which we partition the security space. Notably, each scope encapsulates trust for a particular issuer and audience [these terms are defined by JWT, which we use internally to generate auth-tokens].

.constructor(options)

A constructor that creates a new AuthScope instance using the specified options.

arguments:

  • options : object, optional – an object specifying:
    • issuer : string, optional – the name of the issuer that we trust to issue and digitally sign auth-tokens. Default: 'test'.
    • audience : string, optional – the name of the audience, among the audiences that the issuer issues auth-tokens for, that the new scope should trust. Default: 'test'.
    • issuerKeyFile : string, optional – the file system path to the issuer's public key, used to verify the auth-token's signature.
    • issuerKey : Buffer, optional – the issuer's public key, used to verify the auth-token's signature.

Either options.issuerKeyFile or options.issuerKey is required in order to verify auth-tokens.

example:

var 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
};

var scope = new auth.AuthScope(options);

.issuer

A readonly property that indicates the name of the issuer trusted by the scope.

.audience

A readonly property that indicates the audience in which the scope is participating.

.verify(token, callback)

A method that verifies the specified auth-token.

arguments:

  • token : string, required – the auth-token to be verified.
  • callback : function, required – a function with signature callback(err, context); called with the results of the operation:
    • err : Error – specified upon error; indicates what went wrong
    • context : AuthContext – specified upon success, must be inspected to determine the token's validity.

example:

scope.verify(token, (err, ctx) => {
  if (err) {
    console.log(`Oopsie; an unexpected: ${err}`);
  }
  if (ctx.verified) {
    // ok, indeed signed by the issuer we trust...
    if (!ctx.isExpired) {
      // ok, its still valid...
      console.log(`Verified an auth-token for: ${ctx.principalId}`);
    }
  }
});

AuthContext Class

The AuthContext class encapsulates a decoded auth-token and provides properties and methods over the token enabling inspection, interrogation, and reasoning about the authenticated principal and their security claims.

It is important to note that not all claims are encoded in the auth-token. In a large service inventory the total number of security claims proffered to the claims authority on behalf of a security principal could be numerous, making it unfeasable to encode all such claims in the auth-token. Therefore, a subset of security claims, designated identity claims, are encoded in and travel with the auth-token. All other claims are resolved on demand and cached inside the AuthContext.

It is anticipated that AuthContext has a short lifespan. In most cases they should last no-longer than a single web request or API operation. This scheme enables efficient propagation of security claims across the network without undue likelihood of encountering stale authorizations. Perform your own testing to ensure you reach an appropriate level of assurance as to the veracity of a principal's security claims.

AuthContext exposes readonly properties for many of the most frequently used claims contained in an auth-token:

  • id – indicates the auth-token's unique identity; corresponds with JWT's jti property.
  • systemId – a (relatively short), system assigned, globally unique identifier for the principal.
  • principalId – indicates the principal's human readable identity. In most cases, for principals of kind usr, principalId will be the user's primary email address. For principals of kind end, principalId will be the endpoint/system's well-known, unique name.
  • kind – indicates the principal's kind; kinds are usr for human users and end for trusted endpoints/systems.
  • lang – inidcates the BCP-47 language code of the principal's preferred language.
  • firstName – for principals of kind usr, the user's first name.
  • lastName – for principals of kind usr, the user's last name.
  • email – the principal's primary email address; for principals of kind end, email refers to the primary contact's email address.
  • expiresAt – the auth-ticket's expiration date.

Understanding the Identifiers

The fact that an auth-token has 3 identifiers can be a point of confusion. It is important to understand the purpose and intended use of each identifier:

identifier purpose intended use
systemId Immutable, globally unique, system defined identifier. Used throughout the federated system to refer to the principal across authentication sessions, such as when stored on the file system or in a database.
principalId The principal's human readable identifier. These should be unique within the federated system but may change over time, such is the case when emails are used for this value or when user-selected screen names are used. Used throughout the federated system to instill user confidence that we know who they are, such as a label. (logged in as: fakedood@noobtube.co)
id Immutable auth-token identifier. Uniquely identifies a single authenticated session. May be used throughout the federation to refer to a principal's session.

.constructor(ticket, verified, token, resolverConfig)

A constructor that creates a new AuthContext instance.

arguments:

  • ticket : object, optional – an object representation of the decoded auth-token.
  • verified : boolean, optional – indicates whether the auth-token's digital signature verified.
  • token : string, optional – the original auth-token.
  • resolverConfig : object, optional – a trusted configuration object used to resolve security claims not present in the auth-token.

.get(claimId, callback)

This method has variable arity:

  • .get(claimId) // deprecated
  • .get([claimId, claimId, ...], callback)
  • .get(claimId, claimId, ..., callback)

Gets the principal's claim corresponding to the specified claimId(s).

arguments:

returns:

  • An object is returned upon success. The object's properties will correspond to each claimId specified by the caller.
// Get first name, last name, and email address from the auth-token...
context.get('#/usr/fn', '#/usr/ln', '#/sys/em', (err, res) => {
    if (err) {
      console.log(`An unexpected error occurred: ${err}`);
      return;
    }

    console.log(`The authenticated user is: ${res['#/usr/fn']} ${res['#/usr/ln']} <${res['#/sys/em']}>`);
    // The authenticated user is: Phillip Clark <pclark@leisurelink.com>
  });

NOTE: One of the reasons a claimId is-a JSON Pointer is that this encoding provides us with a namespacing mechanism. In the example above, the claims in our query come from two different namespaces; sys and usr. As you work with claims, keep in mind that the first JSON Pointer path segment identifies a claim set. Each claim is a member of a claim set; these claim sets may be proffered to federated security by different micro-services performing the role of claim set provider.

.has(claimId, callback)

This method has variable arity:

  • .has(claimId) // deprecated
  • .has([claimId, claimId, ...], callback)
  • .has(claimId, claimId, ..., callback)

This method is an alias for:

  • .hasAllOf(claimId) // deprecated
  • .hasAllOf([claimId, claimId, ...], callback)
  • .hasAllOf(claimId, claimId, ..., callback)
  • .role(claimId) // deprecated
  • .role([claimId, claimId, ...], callback)
  • .role(claimId, claimId, ..., callback)

Determines if the principal has claims corresponding to the specified claimId(s).

This method is often used to determine if a principal is a member of a role. Remember, there are 3 types of claims: facts, roles, and permissions. Facts and permissions have associated values, so should be retrieved and evaluated via .get(claimId, callback), whereas roles are truth values, meaning if the role is present the principal is a member.

arguments:

returns:

  • A boolean (result) indicating whether the principal has all of the specified claims. If an error occurs, returns the error as err.

examples:

// Check whether the principal is a member of the sysadmin role...
context.has('#/sys/adm', (err, res) => {
    if (err) {
      console.log(`An unexpected error occurred: ${err}`);
      return;
    }

    if (res) {
      console.log(`Principal is a sysadmin: ${context.principalId}`);
    }
  });
// Check for an email address, first name, and last name...
context.has('#/sys/em', '#/usr/fn', '#/usr/ln', (err, res) => {
    if (err) {
      console.log(`An unexpected error occurred: ${err}`);
      return;
    }

    if (res) {
      console.log('Looks like a user prinicpal!')
    } else {
      console.log('Probably a system/endpoint principal.');
    }
  });

.any(claimId, callback)

This method has variable arity:

  • .any(claimId) // deprecated
  • .any([claimId, claimId, ...], callback)
  • .any(claimId, claimId, ..., callback)

This method is an alias for:

  • .hasAnyOf(claimId) // deprecated
  • .hasAnyOf([claimId, claimId, ...], callback)
  • .hasAnyOf(claimId, claimId, ..., callback)

Determines if the principal has any of the claims corresponding to the specified claimId(s).

arguments:

returns:

  • A boolean (result) indicating whether the principal has any of the specified claims. If an error occurs, returns the error as err.

examples:

// Check for a principalId...
context.any('#/sys/pid', (err, res) => {
    if (err) {
      console.log(`An unexpected error occurred: ${err}`);
      return;
    }

    assert.equal(res, true); // all auth-tokens have a principalId!
  });
// Check for a first name, last name, or an email address...
context.any('#/usr/fn', '#/usr/ln', '#/sys/em', (err, res) => {
    if (err) {
      console.log(`An unexpected error occurred: ${err}`);
      return;
    }

    if (res) {
      console.log('Yep, we\'ve got some human readable identifying info!');
    }
  });

Dependent Types

These are types that types in this repository may use

  • crProvider (Claim Resolution Provider) - A object (or library) which exposes methods for resolving local and remote claims. A object must minimally expose two methods to be considered a crProvider:

    • crProvider#getSpecResolver(lang, authenticClient) - getSpecResolver must return a function implementing function (csid, callback) where csid is a Claimset Id
    • crProvider#getClaimResolver(lang, authenticClient) - getClaimResolver must return a function implementing function (clid, systemId, callback) where clid is a Claim Id and SystemId is an identifier for a principal.

More information about the nature of the requirements placed on the returned functions can be found here.

Both of the Claim Resolutoin Provider functions accept a lang, e.g. en_US, and an instatiated AuthenticClient object.