@leisurelink/auth-context
v3.0.0
Published
LeisureLink client-side authentication & authorization context for node.
Downloads
8
Keywords
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 signauth-tokens
.AuthContext
– An encapsulation of a decodedauth-token
and provides convenience methods for accessing the authenticated principal associated with theauth-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 theauth-token
's signature.issuerKey
: Buffer, optional – the issuer's public key, used to verify theauth-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 – theauth-token
to be verified.callback
: function, required – a function with signaturecallback(err, context)
; called with the results of the operation:err
: Error – specified upon error; indicates what went wrongcontext
: 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 theauth-token
's unique identity; corresponds with JWT'sjti
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
– theauth-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: [email protected]) |
| 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 decodedauth-token
.verified
: boolean, optional – indicates whether theauth-token
's digital signature verified.token
: string, optional – the originalauth-token
.resolverConfig
: object, optional – a trusted configuration object used to resolve security claims not present in theauth-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:
claimId
: string, required – specifies one or more claim identifiers to resolve. Claim identifiers are encoded as RFC 6901 JSON Pointers in URI Fragment Identifier Representation, see theclaims
module's README for more about claim identifiers.callback
: function, optional – a function with signaturecallback(err, result)
; called with the results of the operation
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 <[email protected]>
});
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:
claimId
: string, required – specifies one or more claim identifiers to resolve. Claim identifiers are encoded as RFC 6901 JSON Pointers in URI Fragment Identifier Representation, see theclaims
module's README for more about claim identifiers.callback
: function, optional – a function with signaturecallback(err, result)
; called with the results of the operation
returns:
- A boolean (
result
) indicating whether the principal has all of the specified claims. If an error occurs, returns the error aserr
.
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:
claimId
: string, required – specifies one or more claim identifiers to resolve. Claim identifiers are encoded as RFC 6901 JSON Pointers in URI Fragment Identifier Representation, see theclaims
module's README for more about claim identifiers.callback
: function, optional – a function with signaturecallback(err, result)
; called with the results of the operation
returns:
- A boolean (
result
) indicating whether the principal has any of the specified claims. If an error occurs, returns the error aserr
.
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.
- crProvider#getSpecResolver(lang, authenticClient) - getSpecResolver must return a function implementing
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.