byu-wabs
Table of Contents
About WABS
The Web Application Bootstrap Service (WABS) is a NodeJS Express middleware that provides a useful set of common tools, such as:
- CAS Authentication
- WSO2 Authorization
- Interoperability with Legacy Applications
- Integrates with byu-browser-oauth
CAS Authentication
WABS uses CAS to make sure that the user is correctly signed in when they should be and signed out when they should be.
BYU's Central Authentication System (CAS) is used to establish user identity. CAS offers single sign-on, so if a user signs into one application that uses CAS authentication then goes to another application that uses CAS authentication then they could already be signed into the second application. CAS also offers single sign-out, causing a sign out of CAS in one application to sign out everyone.
Unfortunately not all of BYU's web applications properly support CAS's single sign-on, sometimes requiring the user to click the "Sign In" button even if they are already signed in. Additionally, no BYU web applications currently support single sign-out.
WSO2 Authorization
BYU uses OAuth 2 and WSO2 to allow your application and it's users to access internal BYU web services. WABS provides tools to simplify the WSO2 authorization process. Additionally WABS will automatically manage OAuth tokens, refreshing tokens when necessary.
A problem that WABS solves with WSO2 is the authenticated user synchronization issue. A times WSO2 may believe erroneously that the signed in user is one person when it is actually another. (This issue can occur if two people are using the same computer.) WABS fixes this issue by making sure that the user's CAS identity matches their WSO2 identity and forcing an update in the case where they do not match.
Legacy Applications
Many of BYU's legacy applications require state to be maintained on a per tab (not per browser) basis. This data was stored into what became known at BYU as a brownie. Because there was no way to maintain tab specific data prior to browser's session storage, BYU used HTML forms to POST the tab specific information from one page to the next.
WABS will automatically receive brownies and send brownies in a way that is compliant with legacy applications. Additionally it uses session storage to store data per tab, automatically determining when that data is no-longer valid, and clearing it.
Getting Started
To run your application using WABS you first need to set a few things up:
Create a WSO2 Application
You may need to get access to create a WSO2 application. Contact OIT if this is the case.
- Visit https://api.byu.edu/store.
- Select the link My Applications.
- Fill out the form to add a new application. The
callbackURL
should be set to your domain name. For example, if you are developing locally on port 3000 then thecallbackUrl
should behttp://localhost:3000/
. - Save the WSO2 application.
- Select the link My Subscriptions.
- From the drop down menu select your newly created application.
- Click the button to generate keys for your application.
- You will need your
Consumer Key
andConsumer Secret
values for the WABS configuration.
Create a WABS Configuration
If you plan to use authentication / authorization then you have two options:
-
Recommended option: Store the configuration in the AWS parameter store.
-
Easy option: Create a JSON file outside of your project directory that contains the protected configuration properties.
These are all of the options you can use with a WABS configuration:
Option | Type | Description | Default |
---|---|---|---|
appName | String |
[ REQUIRED ] The name of the application. This value must be unique to your application. This can consist of letter, numbers, underscore and dash. | |
awsParameterName | String |
The name of the AWS parameter store parameter to read additional configuration options from. | Default AWS Parameter Name |
awsSsmConfig | Object |
A configuration to pass to the AWS SDK SSM constructor. | { region: 'us-west-2' } |
casCallbackPath | String |
The path to redirect the browser to after CAS authentication. This value should match the path that your WABS init middleware is running from. |
/ |
configFile | String |
A file path to read additional configuration options from. | |
consumerKey | String |
The WSO2 consumer key. 2 | |
consumerSecret | String |
The WSO2 consumer secret. 2 | |
encryptSecret | String |
A seed to use to encrypt your applications sensitive information. This must be a string of at least 40 characters in length. 2 | |
hardTimeout | Number |
The maximum number of minutes to allow someone to be logged into your application. | 600 |
host | String |
The protocol, domain, and port used to get to your server. For example: http://localhost:3000 . Generally this value can be determined automatically, but if you are having issues when people try to log in then you can try to set the host manually. |
|
apiHost | String |
The hostname of the API Manager used for authentication. | |
openProxy | Boolean |
Whether to allow the web browser to indiscriminately proxy web service requests through the server, using the server's identity to communicate with web serves. | false |
reservedPath | String |
A reserved path that the init middleware uses. | "/__wabs" |
wso2CallbackPath | String |
The path to redirect the browser to after WSO2 authorization. This value must match the path (excluding protocol, domain, and port) that you specified when creating your WSO2 application. | / |
Default AWS Parameter Name
The default value for awsParameterName
is derived from a combination of the appName
, the environment (defined by the environment variables NODE_ENV
or HANDEL_ENVIRONMENT_NAME
, defaulting to "dev"
), and the suffix "WABS_CONFIG"
joined by dots (.
). For example if your app is named "my-app"
and no environment is specified then this value will default to "my-app.dev.WABS_CONFIG"
.
Protected Configuration Options
The consumer key, consumer secret, and encrypt secret are all required if your application uses authentication / authorization. Mechanisms are in place to prevent you from putting your consumer key, consumer secret, and encrypt secret in unsafe locations. NEVER STORE YOUR WSO2 CLIENT ID, WSO2 CLIENT SECRET, OR ENCRYPT SECRET IN YOUR CODE OR WITHIN YOUR APPLICATION DIRECTORY.
Where to Put the Configuration
If you plan to use authentication and authorization with your application then the consumerKey
, consumerSecret
, and encryptSecret
cannot be stored within your code or within a configuration file that resides within in your project directory. You can store the consumerKey
, consumerSecret
, and encryptSecret
in a configuration file outside of your project directory or in an AWS Parameter store.
In Your Code
Within your code you can specify the appName
, awsConfig
, awsParameterName
, casCallbackPath
, configFile
, hardTimeout
, host
, and wso2CallbackPath
. This excludes only the consumerKey
, consumerSecret
, and encryptSecret
which, if you attempt to include, will throw an error due to security concerns.
Define with App Name Only
const Wabs = require('byu-wabs');
const middleware = Wabs('my-app-name'); // shortcut for defining just the app name
Define with Configuration File Path
The file path must include a path delimiter to be recognized as a path. This path cannot exist within your application directory or an error will be thrown.
const Wabs = require('byu-wabs');
const middleware = Wabs('/path/to/config.json');
Define with Configuration
const Wabs = require('byu-wabs');
const middleware = Wabs({
appName: 'my-app-name',
awsParameterName: 'my-app-param-name',
awsSsmConfig: { region: 'us-west-2' },
casCallbackPath: '/',
configFile: '/path/to/config.json',
hardTimeout: 600,
host: 'http://localhost:3000',
wso2CallbackPath: '/'
});
In the AWS Parameter Store
This is the recommended method for storing your configuration. By keeping this configuration in a central location other developers can also work on your code without having to acquire the secure configuration of your app.
The name of this parameter can be specified in the middleware configuration using the awsParameterName
option. Alternatively it will use the default name.
Create a parameter in the AWS Parameter Store that has a value that is a JSON string containing all configuration options you'd like to store on AWS. Tell your configuration in code to use that parameter name.
server.js
const Wabs = require('byu-wabs');
const middleware = Wabs('my-app-name');
parameter store
Assuming that the environment has not been set, because the awsParameterName
has not been set, the default will be used with is derived to "my-app-name.dev.WABS_CONFIG"
1
my-app-name.dev.WABS_CONFIG = { "consumerKey": "<consumer-key>", "consumerSecret": "<consumer-secret>", "encryptSecret": "<encrypt-secret>" }
In a Configuration File
Due to security concerns, the configuration file must reside outside of your project directory. Any configuration option can be specified in this file but may be overwritten by configuration settings made in the application code. In other words, if you specify an option here that is already specified in the application code then the application code will take precedence.
server.js
const Wabs = require('byu-wabs');
const middleware = Wabs('/path/to/config.json');
/path/to/config.json
{
"appName": "my-app-name",
"awsParameterName": "my-app-param-name",
"awsSsmConfig": { "region": "us-west-2" },
"casCallbackPath": "/",
"consumerKey": "<consumer-key>",
"consumerSecret": "<consumer-secret>",
"encryptSecret": "<encrypt-secret>",
"hardTimeout": "600",
"host": "http://localhost:3000",
"wso2CallbackPath": "/"
}
Add WABS Middleware to Your Server
You'll need to be familiar with Express Middleware to understand this section.
WABS is a middleware that you can use to add functionality to your server. That functionality includes the mechanisms for authentication, authorization, token management, and interoperability with legacy code.
// get dependencies
const cookieParser = require('cookie-parser');
const express = require('express');
const path = require('path');
const wabs = require('byu-wabs')('my-app-name');
// create the express app
const app = express();
// must parse cookies before wabs.init
app.use(cookieParser(wabs.config.encryptSecret));
app.use(wabs.init());
// serve index.html and static files
const publicDirectoryPath = '/path/to/app/public';
app.use(wabs.index({ render: publicDirectoryPath + '/index.html' }));
app.use(express.static(publicDirectoryPath))
// start listening for requests
app.listen(3000, function(err) {
if (err) return console.error(err.stack);
console.log('Server listening on port: 3000');
});
At this point you are ready to start your application and open a browser.
Server Middleware
This section covers the middlewares bundled with WABS and how to use them. It does not cover details about configuring the WABS instance. For that information check out the Getting Started section.
Middleware Table of Contents
- authenticated - An mid-route middleware for ensuring the user is authenticated.
- brownie - Run brownie receiving and parsing middleware.
- clientGrantProxy - Middleware for proxying client requests.
- index - Middleware for keeping authentication in sync and loading the index.html file.
- init - A required initialization middleware.
- login - A middleware for creating a login endpoint.
- logout - A middleware for creating a logout endpoint.
- sync - A middleware for synchronizing the current authentication / authorization state.
authenticated
The authenticated middleware can be run mid route to ensure that a user is either authenticated or forbidden from accessing the route. Useful if you don't want a user to to access a protected route without being authenticated.
Parameters
-
options - A configuration object.
-
authenticate - Whether to redirect the user to authenticate if they are not logged in. This can only occur for GET requests. Defaults to
false
.
-
authenticate - Whether to redirect the user to authenticate if they are not logged in. This can only occur for GET requests. Defaults to
Returns a middleware function.
app.get('/protected', wabs.authenticated(), function(req, res) {
res.send('Protected data');
});
Table of Contents | Middleware Table of Contents
brownie
This middleware provides legacy application interoperability where brownies are in use by listening for POST
requests. If a POST
is received then this middleware will read the body and attempt to decode it as a brownie payload.
Parameters
None
Returns a middleware function.
app.use(wabs.brownie());
Table of Contents | Middleware Table of Contents
clientGrantProxy
The clientGrantProxy middleware can be used to proxy requests from the browser and use OAuth 2's client grant credentials to access a web service on another server. Note that client grant credentials are tied to your application and do not contain any information about the user, therefore it is best to use this as a proxy for endpoints where the user does not need to be authenticated.
Parameters
-
requestOptions - A
Function
or aString
.If a
Function
then the function will be called with the current request object as it's parameter. The function should return an object that will be passed to the request (v2) module.If a
String
then this value will be used as the base URL for the request and the rest of the request will be forwarded to proxied endpoint.
Returns a middleware function.
In the following examples a request to "/my-proxy-path/abc/123"
for this endpoint would proxy to "https://api.byu.edu/my-endpoint/abc/123"
:
app.use('/my-proxy-path', wabs.clientGrantProxy('https://api.byu.edu/my-endpoint'));
app.use('/my-proxy-path', wabs.clientGrantProxy(function(req) {
return {
baseUrl: 'https://api.byu.edu/my-endpoint',
body: req.body || '',
headers: Object.assign({}, req.headers),
method: req.method,
qs: Object.assign({}, req.query),
url: req.path
}
});
Table of Contents | Middleware Table of Contents
index
This middleware is used to produce the index.html file, to receive brownies, and to synchronize authentication / authorization.
Parameters
-
options - A configuration object.
-
brownie - A
Boolean
that is set totrue
to allow reception and decoding of brownies. Defaults totrue
. -
injectScript - A
Boolean
that if set totrue
will inject the<script>
tag into yourindex.html
file that will cause the WABS client script to be downloaded. -
ignore - A array of strings or regular expressions (
Array.<String|RegExp>
) or aFunction
.If
Array.<String|RegExp>
then each item in the array will be compared to the request path and if found to be a match will cause the index.html not to be returned.If a
Function
then the function will receive the current path and should returntrue
if the path should not return the index.html. -
render - REQUIRED. A
String
or aFunction
.If a
String
then this should be the file path to your index.html file.If a
Function
then the function will be called with two parameters: 1) request and 2) a callback function. The callback function should be called with two parameters: 1) the error (if any), 2) the final HTML content to send as your index.html content. -
sync - A
Boolean
that if set totrue
will get the latest authentication / authorization before loading the index.html content. This behavior is recommended if you're building a single page app.
-
Returns a middleware function.
app.use(wabs.index({ render: '/path/to/index.html' });
app.use(wabs.index({
render: function(req, callback) {
const content = '<html><body>Hello, World!</body></html>';
callback(null, content);
}
});
Table of Contents | Middleware Table of Contents
init
This middleware must execute before any other wabs middleware.
Parameters: None
Returns a middleware function.
app.use(wabs.init());
Table of Contents | Middleware Table of Contents
login
This middleware can be used to define a login endpoint. This middleware is not necessary if you only plan to use the client side script wabs.auth.login()
.
Parameters
-
options - An optional configuration object.
-
failure - A URL to redirect to if the login fails. This can be overwritten if the incoming request has a
failure
query parameter. Defaults to success value. -
gateway - Whether to use the CAS gateway. Using the gateway will check to see if the user is logged in but will not display the login form if they are not logged in. This can be overwritten if the incoming request has a
gateway
query parameter set to"true"
or"false"
. Defaults tofalse
. -
query - An optional object for adding additional query parameters to the CAS login URL.
-
success - A URL to redirect to if the login success. This can be overwritten if the incoming request has a
success
query parameter. Defaults to"/"
. -
wso2 - Whether the browser should be redirected to WSO2 after successful CAS authentication. Defaults to
true
.
-
Returns a middleware function.
app.use('/login', wabs.login());
Table of Contents | Middleware Table of Contents
logout
This middleware can be used to define a logout endpoint. This middleware is not necessary if you only plan to use the client side script wabs.auth.logout()
.
Parameters
-
options - An optional configuration object.
-
cas - Set to
true
to have this middleware also log out of CAS. Defaults totrue
. -
cFramework - Set to
true
to have this middleware also log out of the C-Framework. Defaults totrue
. -
redirect - The URL to redirect to after logout. This value can be overwritten if the incoming request has a
redirect
query parameter. Defaults to"/"
. -
wso2 - Set to
true
to have this middleware also log out of WSO2. Defaults totrue
.
-
Returns a middleware function.
app.use('/logout', wabs.logout());
Table of Contents | Middleware Table of Contents
sync
Middleware that will transparently redirect the request through CAS and WSO2 to get the latest authentication and authorization data.
Parameters None
Returns a middleware function.
In this example when the browser navigates to "/some/path"
it will be redirected to CAS and WSO2 prior to calling the routerHandler
function. This will help ensure that the current user's authentication and authorization is up to date.
app.get('/some/path', wabs.sync(), function routeHandler(req, res) {
// ...
});
Table of Contents | Middleware Table of Contents
Client Script
The WABS client script is automatically added to your index file (immediately before your first script tag) when requested by a browser. This script file will finish executing prior to the execution of any of your scripts. If you have set either the NODE_ENV
or HANDEL_EVIRONMENT_NAME
to a value other than "dev"
then this script will be minified. It provides the following functionality:
- Automatic synchronization of authenticated status between other WABS applications.
- Automatically refreshes the authenticated user's OAuth access token before expiration.
- Provides functionality to manually manage authentication and authorization.
- Easy access to get or set the brownie.
- Automatic re-encoding of the brownie.
- Automatic and manual sending of the brownie.
- Fires events for important changes in state.
Client Script Table of Contents
- Events - Subscribe to events to keep your app authorization / authentication synchronized.
- byu.ajax - A small AJAX helper function
- byu.auth.accessToken - A getter for the current access token.
- byu.auth.expires - A getter for the current access token expiration date.
- byu.auth.login - A function to initiate a login.
- byu.auth.logout - A function to initate a logout.
- byu.auth.proxy - A function to make proxied AJAX requests that will use the WSO2 client grant token for authorization.
- byu.auth.refreshToken - A function to refresh the access token.
- byu.auth.request - A wrapper around byu.ajax that adds the code grant access token to the request and automatically retries if the token has expired.
- byu.auth.sync - A function to perform a authorization / authentication synchronization.
- byu.navigateTo - A function to POST brownie data to another page.
- byu.user - The currently logged in user.
Events
WABS will emit events that can be captured at the document level:
<script>
document.addEventListener('wabs-auth-login', function(event) {
console.log('User logged in: ' + byu.user.netId);
});
</script>
These are the emitted events:
-
wabs - This event will be emitted immediately after any other events. It can be used to capture all WABS events. The event
detail
property will contain thename
of the original event as well as itsdata
.<script> document.addEventListener('wabs', function(event) { console.log('Event name: ' + event.detail.name); console.log('Event data: ' + event.detail.data); }); </script>
-
wabs-auth-login - Ths event is emitted when a user is authenticated. The access token may or may not be available at this point, but the event
detail
will tell whether or not it isgettingAccessToken
. If set to false you may want to callbyu.auth.login()
which will begin the process of getting the accessToken.<script> document.addEventListener('wabs-auth-login', function(event) { if (!event.detail.gettingAccessToken) byu.auth.login(); }); </script>
-
wabs-auth-logout - This even is emitted when the user loses authentication.
<script> document.addEventListener('wabs-auth-logout', function() { console.log('User logged out'); }); </script>
-
wabs-access-token-update - This event is emitted when the access token changes. That could mean there is a new access token or none at all.
<script> document.addEventListener('wabs-access-token-update', function() { console.log('New access token: ' + byu.auth.accessToken); }); </script>
Table of Contents | Client Script Table of Contents
byu.ajax
A simple AJAX wrapper. If you're not using IE11 then just use JavaScript fetch. Also, this function a light AJAX implementation and is not intended to solve every use case, although it will work for most.
Signature byu.ajax(options [, callback ])
Parameters
-
options - A
string
or anObject
the defines the request. If astring
then the value will be used as the request URL and all other default values will be kept. If anObject
then the following properties can be set.-
body - A
string
or anObject
. If anObject
then the value will automatically be serialized for transmission and the headers forContent-Type
will automatically be set toapplication/json
. -
headers - An
Object.<string,string>
with name value pairs where the name represents the header name and the value is the header value. All header names will automatically be converted to lower case. -
method - The HTTP method to use. Defaults to
"GET"
. -
url - The URL to send the request to. This value is required.
-
-
callback - An optional function to call once the request has completed. This function will receive the parameters: 1) body and 2) statusCode.
Returns The XMLHttpRequest
instance that sent the request if the callback is provided or a promise if the callback parameter was omitted. The promise resolves to an object with the properties body
and status
.
byu.ajax('http://someurl.com/', function(body, code) { ... });
byu.ajax({
method: 'POST',
url: 'http://someurl.com',
headers: {
'x-header': 'value'
},
body: {
propertyName: 'propertyValue'
}
}, function(body, code) { ... });
Table of Contents | Client Script Table of Contents
byu.auth.accessToken
Get the WSO2 access token for the authenticated user. Just because a user is authenticated doesn't mean that they also have an access token although most often they will.
Table of Contents | Client Script Table of Contents
byu.auth.expires
Get the date that the WSO2 access token expires.
Table of Contents | Client Script Table of Contents
byu.auth.login
Initialize a login sequence by redirecting the browser through the CAS login screen and the WSO2 authorization page.
Signature byu.auth.login([ success ,] options ])
Parameters
-
success -The URL to navigate to after successful login. This parameter can be omitted while still providing the options parameter. Defaults to the current page URL.
-
options - A
String
or anObject
. If aString
then this represents the URL to navigate to if authentication fails. If anObject
then it has the following properties:-
failure - The URL to navigate to if authentication fails. Defaults to the
success
URL value. -
popup - Whether to use a popup window for login. Defaults to
false
.
-
Returns undefined
Example
<button onclick="byu.auth.login()">Sign In</button>
Table of Contents | Client Script Table of Contents
byu.auth.logout
Initialize a logout sequence. This attempts to be a single sign-out as much as possible by logging out of CAS, WSO2, cFramework applications, and other WABS applications.
Signature byu.auth.logout([[ redirect ,] options ])
Parameters
-
redirect - The URL to navigate to after logout. This parameter can be omitted while still providing the options parameter. Defaults to the current page URL.
-
options - An object that specifies which services to log out of.
-
cas - Log out of CAS. Defaults to
true
. -
cFramework - Log out of the cFramework. Defaults to
true
. -
popup - Cause the log out to happen via a popup window (will not allow logging out of the cFramework). Defaults to
false
. -
wso2 - Log out of WSO2. Defaults to
true
.
-
Returns undefined
Example
<button onclick="byu.auth.logout()">Sign Out</button>
Table of Contents | Client Script Table of Contents
byu.auth.proxy
Make an AJAX request that is proxied by the server. The server will add its own access token to the request.
Note that calling this function will not work unless the option openProxy
on the constructor is set to true
.
Signature byu.auth.proxy( options [, callback ])
Parameters
-
options - A
String
or anObject
. If aString
then the request will use the value as the URL and all other options will be default values.-
body - The body to send with the request.
-
method - The http method to use. Defaults to
"GET"
. -
headers - An
Object
with name value pairs. -
url - REQUIRED The URL to proxy against.
-
-
callback - A callback to call when the request completes. The callback receives the response body and status code.
Example
byu.auth.proxy('https://api.byu.edu/some/api', function(body, status) {
console.log(body, status);
});
Table of Contents | Client Script Table of Contents
byu.auth.refreshToken
Manually refresh the WSO2 access token.
Signature byu.auth.refreshToken([ callback ])
Parameters
- callback - The function to call once the refresh request has completed.
Returns undefined
Example
byu.auth.refreshToken(function(body, status) {
console.log(body, status, byu.auth.accessToken);
});
Table of Contents | Client Script Table of Contents
byu.auth.request
Make an AJAX request that automatically adds the user's access token to the authorization header. The user MUST be logged in for this function to work. If the response for the request indicates an invalid access token then the access token is automatically refreshed and one more attempt is made.
Signature byu.auth.request( options [, callback ])
Parameters
-
options - A
String
or anObject
. If aString
then the request will use the value as the URL and all other options will be default values.-
body - The body to send with the request.
-
method - The http method to use. Defaults to
"GET"
. -
headers - An
Object
with name value pairs. -
url - REQUIRED The URL to proxy against.
-
-
callback - An optional callback to call when the request completes. The callback receives the response body and status code. If omitted then a promise will be returned.
Returns A promise if the callback parameter was omitted. The promise resolves to an object with the properties body
and status
.
Example
byu.auth.request('https://api.byu.edu/some/api', function(body, status) {
console.log(body, status);
});
Table of Contents | Client Script Table of Contents
byu.auth.sync
Synchronize the current authentication and authorization state. This will cause a popup window to appear briefly that will check with CAS for current authentication and will go through WSO2 to acquire the latest access token.
This function will be called automatically if any other WABS application signs in our out.
Signature byu.auth.sync([ callback ])
Parameters
- callback - If provided, the callback will be called after synchronization has completed and it will receive two paramters: 1) the current user, 2) the current access token.
Returns undefined
byu.auth.request('https://api.byu.edu/some/api', function(user, accessToken) {
if (user) {
console.log('User logged in: ' + user.netId);
} else {
console.log('User not logged in.');
}
});
Table of Contents | Client Script Table of Contents
byu.navigateTo
Navigate while posting current brownie data to the receiving URL. Note that any links (<a>
) with a target of _blank
or that go to a C-Framework application will automatically call this function.
Signature byu.navigateTo( url [, target [, callback ] ])
Parameters
-
url - The URL to navigate to.
-
target - The window target to cause to navigate. Defaults to
_self
. -
callback - A function to call immediately prior to navigation. If an error occurs then this function will receive that error as its first parameter.
Returns undefined
byu.navigateTo('https://myapp.byu.edu');
Table of Contents | Client Script Table of Contents
byu.user
Get the currently authenticated user. If not authenticated then this value will be null
.
console.log(byu.user);