@t-bowersox/bouncer v0.2.0
bouncer
A tool for checking user authorization.
Installation
npm i @t-bowersox/bouncerChangelog
See release notes.
Usage
Bouncer performs two primary sets of tasks:
- Issuing, revoking, & validating signed access tokens.
- Validating a user's attributes against a permissions ruleset.
Requirements
In order to create a Bouncer instance, you must have the following:
- A private key string in PEM format.
- If your private key is encrypted by a passphrase, you must provide it as well.
- A public key string in PEM format.
- An object that implements the
TokenStoreinterface.
Need help generating keys? This article provides a good explainer using OpenSSL.
Token signing
The private and public keys are necessary for signing and verifying access tokens. Under the hood, Bouncer uses the
Node.js crypto.createPrivateKey
and crypto.createPublicKey
methods to create a KeyObject for each.
Those KeyObjects are used with instances of
Node's Sign
and Verify classes when signing and
verifying access tokens, respectively. The algorithm used for signatures is SHA256.
The Token interface stores only a unique session ID, the user's ID, and an expiration timestamp. The session ID is
generated using
Node's crypto.randomUUID, which
uses a cryptographic pseudorandom number generator.
Token stores
The TokenStore interface requires an object to have the following methods, which are used for tracking revoked tokens:
storeTokenData- This is called by Bouncer when creating a token to store its session ID, user ID, and timestamp values.addToDenyList- This is called by Bouncer to add a session ID to the "Deny List", which is a list of revoked tokens.isOnDenyList- This is called by Bouncer during token validation to see if a session ID has been revoked.
It is up to you to handle where to store the data, whether it's a key-value store, relational database, etc.
Authorization flow
Creating a Bouncer instance
import { Bouncer } from "@t-bowersox/bouncer";
// This TokenStore implementation is just for demonstration
const tokenStore = {
async storeTokenData(
sessionId: string,
userId: string | number,
timestamp: number
): Promise<boolean> {
const database = new SomeDatabaseClass();
return await database.insert(sessionId, userId, timestamp);
},
async addToDenyList(sessionId: string, timestamp: number): Promise<boolean> {
const database = new SomeDatabaseClass();
return await database.insert(sessionId, timestamp);
},
async isOnDenyList(sessionId: string): Promise<boolean> {
const database = new SomeDatabaseClass();
return await database.get(sessionId);
},
};
// You'll most likely want to use env variables for the keys & passphrase
const bouncer = new Bouncer(
tokenStore,
"my-private-key-pem",
"my-public-key-pem",
"my-private-key-passphrase"
);Creating an access token
An access token is a dot-separated string that includes:
- The base64 encoded token.
- The base64 encoded signature for that token.
For example: <base64-encoded-token>.<base64-encoded-signature>. Being base64 encoded, it's suitable for returning to
the user in a secure, HTTP-only cookie.
To create a token, pass the user's ID and an expiration date (as a Date object) to the createToken method.
const token = bouncer.createToken(1, new Date("2022-02-28 00:00:00"));Revoking an access token
In cases where you need to invalidate a session ID, you can call the revokeToken method. This will add the session ID
to Bouncer's Deny List, which is a store of all revoked tokens. Bouncer checks this list during the token validation
process.
To add a token to the Deny List, pass its session ID to revokeToken. You can retrieve these by querying the database
you're using for the TokenStore's storeTokenData function.
revokeToken will return true if it was successful, false if not.
const revoked = bouncer.revokeToken("abcd-1234-efgh-5678");Validating a token
To validate a token, simply pass it to the validateToken method. Bouncer will return a boolean response based on the
following criteria:
- Is the token's signature verified? If so, then check:
- Has the token expired? If not, then check:
- Is the token on the Deny List? If not, return
true.
Otherwise, Bouncer returns false indicating the token is invalid. That's a signal to your app to deny access to that
user.
const validated = bouncer.validateToken(
"<base64-encoded-token>.<base64-encoded-signature>"
);Validating a user
Bouncer's validateUser method allows you to compare a user's attributes to a RuleSet of validator functions. Each of
the functions you add to a RuleSet take a value and, based on your criteria, returns a boolean: true if
validated, false if not.
Each RuleSet keeps two internal sets of functions: one for synchronous functions and the other for async functions.
All functions in the sets must return true in order for Bouncer's validateUser method to return true. If just one
returns false, then so will validateUser.
type ValidationRule<T = any> = (userData: T) => boolean;
type AsyncValidationRule<T = any> = (userData: T) => Promise<boolean>;To avoid unnecessary async calls, Bouncer will first evaluate the synchronous validators if present. Only if all of
those return true will it proceed to the async validators.
You can create as many instances of the RuleSet class as you need for your application. For example, you could
have RuleSets for different user types, different application routes, etc.
import { Bouncer, RuleSet } from "@t-bowersox/bouncer";
import { User } from "your-app-code";
const user: User = {
id: 1,
role: "regular",
permissions: { read: true, write: false },
};
const syncRuleExample = (user: User): boolean =>
user["permissions"]["read"] === true;
const asyncRuleExample = async (user: User): Promise<boolean> => {
const database = new MyDatabaseClass();
const result = await database.getSomeUserData(user.id);
return !!result;
};
let ruleSet = new RuleSet([syncRuleExample], [asyncRuleExample]);
// Or...
ruleSet = new RuleSet()
.addSyncRule(syncRuleExample)
.addAsyncRule(asyncRuleExample);
// Then...
const bouncer = new Bouncer(/*...*/);
const validUser = await bouncer.validateUser(user, ruleSet);API
Class Bouncer
Creates a new Bouncer instance.
class Bouncer {
constructor(
tokenStore: TokenStore,
privatePem: string,
publicPem: string,
passphrase?: string
) {}
}Parameters:
tokenStore: an object implementing theTokenStoreinterface, used for adding to and checking the Deny List of tokens.privatePem: a string containing a private key in PEM format, used for signing tokens.publicPem: a string containing the correspondnig public key in PEM format, used for verifying tokens.passphrase: if the private key was encrypted with a passphrase, pass it to this parameter.
Method createToken
Creates a new access token and stores its data in the TokenStore.
class Bouncer {
async createToken(
userId: string | number,
expirationDate: Date
): Promise<string> {}
}Parameters:
userId: a string or a number that is the unique ID for your user.expirationDate: aDateobject that indicates when the token should expire.
Returns:
- A promise resolving to a dot-separated string containing the base64-encoded token and its base64-encoded signature (
i.e.
<base64-encoded-token>.<base64-encoded-signature>)
Method revokeToken
Adds a token to the TokenStore's Deny List.
class Bouncer {
async revokeToken(unparsedToken: string): Promise<boolean> {}
}Parameters:
unparsedToken: the string returned fromcreateToken.
Returns:
- A promise resolving to
trueif the token was successfully added to the Deny List,falseif not.
Method validateToken
Evaluates it a token is valid based on its signature, expiration date, and Deny List status.
class Bouncer {
async validateToken(unparsedToken: Base64String): Promise<boolean> {}
}Parameters:
unparsedToken: the string returned fromcreateToken.
Returns:
- A promise resolving to
trueif the token is valid,falseif not.
Method validateUser
Evaluates a user's attributes against a RuleSet to determine if the user meets the criteria for access.
class Bouncer {
async validateUser<T>(userData: T, rules: Ruleset): Promise<boolean> {}
}Parameters:
userData: the data to be evaluated by theRuleSet.rules: aRuleSetinstance containing one or moreValidationRuleand/orAsyncValidationRulefunctions.
Returns:
- A promise resolving to
trueif all rules in the set returnedtrue, otherwisefalse.
Interface TokenStore
Contains async methods used by Bouncer to add revoked tokens to a database (referred to as the Deny List), as well as check for the existance of a token in that database.
interface TokenStore {
storeTokenData(
sessionId: string,
userId: string | number,
timestamp: number
): Promise<boolean>;
addToDenyList(sessionId: string, timestamp: number): Promise<boolean>;
isOnDenyList(sessionId: string): Promise<boolean>;
}Method storeTokenData
Stores a token's session ID, user ID, and timestamp in a database.
interface TokenStore {
storeTokenData(
sessionId: string,
userId: string | number,
timestamp: number
): Promise<boolean>;
}Parameters:
sessionId: the token's session ID generated by Bouncer.userId: the token's user ID passed to thecreateTokenmethod.timestamp: the token'sexpirationTime, based on the date passed to thecreateTokenmethod.
Returns:
- A promise resolving to
trueif the token data was saved successfully,falseif not.
Method addToDenyList
Stores the session ID and timestamp of a token in a database.
interface TokenStore {
addToDenyList(sessionId: string, timestamp: number): Promise<boolean>;
}Parameters:
sessionId- a string containing the revoked token's session ID.timestamp- a number containing the current timestamp provided byDate.now().
Returns:
- A promise resolving to
trueif the token was successfully saved,falseif not.
Method isOnDenyList
Looks for the session ID of a token in a database.
interface TokenStore {
isOnDenyList(sessionId: string): Promise<boolean>;
}Parameters:
sessionId- a string containing the session ID to look for.
Returns:
- A promise resolving to
trueif the token was found,falseif not.
Interface Token
An access token. This is encoded to JSON then to base64 before it is returned by createToken.
interface Token {
sessionId: string;
userId: string | number;
expirationTime: number;
}Type Base64String
Indicates a string has been base64-encoded.
type Base64String = string;Class RuleSet
class RuleSet {
constructor(
syncRules?: ValidationRule[],
asyncRules?: AsyncValidationRule[]
) {}
}Parameters:
syncRules: an optional array ofValidationRulefunctions.asyncRules: an optional array ofAsyncValidationRulefunctions.
Method addSyncRule
Adds a synchronous ValidationRule to the RuleSet.
class RuleSet {
addSyncRule(rule: ValidationRule): Ruleset {}
}Parameters:
rule: theValidationRulefunction to add.
Returns:
- The
RuleSetinstance so you can chain multiple calls.
Method addAsyncRule
Adds an AsyncValidationRule to the RuleSet.
class Ruleset {
addAsyncRule(rule: AsyncValidationRule): Ruleset {}
}Parameters:
rule: theAsyncValidationRulefunction to add.
Returns:
- The
RuleSetinstance so you can chain multiple calls.
Method hasSyncRule
Checks if the RuleSet contains a specific ValidationRule.
class RuleSet {
hasSyncRule(rule: ValidationRule): boolean {}
}Parameters:
rule: theValidationRuleto look for.
Returns:
trueif present in theRuleSet, otherwisefalse.
Method hasAsyncRule
Checks if the RuleSet contains a specific AsyncValidationRule.
class RuleSet {
hasAsyncRule(rule: AsyncValidationRule): boolean {}
}Parameters:
rule: theAsyncValidationRuleto look for.
Returns:
trueif present in theRuleSet, otherwisefalse.
Method deleteSyncRule
Deletes a ValidationRule from the RuleSet.
class RuleSet {
deleteSyncRule(rule: ValidationRule): boolean {}
}Parameters:
rule: theValidationRuleto delete.
Returns:
trueif value was in the set,falseif not.
Method deleteAsyncRule
Deletes an AsyncValidationRule from the RuleSet.
class RuleSet {
deleteAsyncRule(rule: AsyncValidationRule): boolean {}
}Parameters:
rule: theAsyncValidationRuleto delete.
Returns:
trueif value was in the set,falseif not.
Method clearSyncRules
Deletes all ValidationRules from the RuleSet.
class RuleSet {
clearSyncRules(): void {}
}Method clearAsyncRules
Deletes all AsyncValidationRules from the RuleSet.
class RuleSet {
clearAsyncRules(): void {}
}Method evaluateSync
Compares user data against the RuleSet's internal set of ValidationRules. This normally should not be called
directly. Instead, use Bouncer's validateUser method.
class RuleSet {
evaluateSync<T>(userData: T): boolean {}
}Parameters:
userData: the data to evaluate in theValidationRulefunctions.
Returns:
trueif allValidationRules returned true, otherwisefalse.
Method evaluateAsync
Compares user data against the RuleSet's internal set of AsyncValidationRules. This normally should not be called
directly. Instead, use Bouncer's validateUser method.
class RuleSet {
evaluateAsync<T>(userData: T): Promise<boolean> {}
}Parameters:
userData: the data to evaluate in theValidationRulefunctions.
Returns:
- A promise resolving to
trueif allAsyncValidationRules returned true, otherwisefalse.
Type ValidationRule
A function that evaluates user data synchronously.
type ValidationRule<T = any> = (userData: T) => boolean;Parameters:
userData: the data to evaluate.
Returns:
trueif the data passed validation,falseif not.
Type AsyncValidationRule
A function that evaluates user data asynchronously.
type AsyncValidationRule<T = any> = (userData: T) => Promise<boolean>;Parameters:
userData: the data to evaluate.
Returns:
- A promise resolving to
trueif the data passed validation, orfalseif not.
Contributing
This is primarily a package that I intend to reuse in my own projects. I've decided to open source it in case there are other folks who might also find it useful.
With that in mind, I only expect to make changes to Container that jibe with how I intend to use it myself.
But if you do have ideas or found bugs, please do file an issue and I'll gladly review it. 🙂