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',
audience: 'test',
issuerKeyFile: './test/test-key.pub'
};
var scope = new auth.AuthScope(options);
Verifying an auth-token Creates an AuthContext
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)
function userInfoMiddleware(req, res, next) {
let app = req.app;
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)
function remoteEndpointPrincipalIsSecurityOfficer(req, res, next) {
let app = req.app;
app.remoteAuth.has('#/sys/secofr', (err, res) => {
if (err) {
next(err);
}
res.locals.endpointIsSecurityOfficer = res;
next();
});
}
Check if Principal Has Permission (permission example)
function userPrincipalReadUpdatePermissions(req, res, next) {
let app = req.app;
app.user.get('#/sys/pri[ru]', (err, res) => {
if (err) {
next(err);
}
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)
function userInfoMiddleware(req, res, next) {
let user = req.app.user;
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
function remoteEndpointPrincipalIsSecurityOfficer(req, res, next) {
let remoteAuth = req.app.remoteAuth;
res.locals.endpointIsSecurityOfficer = remoteAuth.ident.has('#/sys/secofr');
next();
}
Check Principal's Identity Permissions
function userPrincipalReadUpdatePermissions(req, res, next) {
let user = req.app.user;
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',
audience: 'test',
issuerKeyFile: './test/test-key.pub'
};
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 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) {
if (!ctx.isExpired) {
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.
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']}>`);
});
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:
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}`);
}
});
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:
context.any('#/sys/pid', (err, res) => {
if (err) {
console.log(`An unexpected error occurred: ${err}`);
return;
}
assert.equal(res, true);
});
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
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.