Skip to content
Wrapper for AppAuthJS to assist with silent token acquisition and renewal
JavaScript HTML
Branch: master
Clone or download
Latest commit 98529c1 Jul 19, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.babelrc Introduction of IE 11 support Jul 23, 2019
.eslintignore Introduction of IE 11 support Jul 23, 2019
.eslintrc.json Introduces Service Worker as a means of automatically adding access t… May 4, 2019
.gitignore First version of working code Dec 12, 2018
IdentityProxyCore.js Introduction of IE 11 support Jul 23, 2019
IdentityProxyServiceWorker.js Introduction of IE 11 support Jul 23, 2019
IdentityProxyXHR.js Introduction of IE 11 support Jul 23, 2019
LICENSE Adds appropriate license and copyright notices Jul 23, 2019
README.md Updates to docs regarding IE support via XHR proxy Jul 23, 2019
appAuthHelper.js Introduction of IE 11 support Jul 23, 2019
appAuthHelperBundle.js Adds appropriate license and copyright notices Jul 23, 2019
appAuthHelperCompat.js Introduction of IE 11 support Jul 23, 2019
appAuthHelperFetchTokens.js Introduction of IE 11 support Jul 23, 2019
appAuthHelperFetchTokensBundle.js Adds appropriate license and copyright notices Jul 23, 2019
appAuthHelperFetchTokensCompat.js Introduction of IE 11 support Jul 23, 2019
appAuthHelperRedirect.html Performs token request in redirect.html rather than in SPA context Jul 8, 2019
appAuthServiceWorker.js Adds appropriate license and copyright notices Jul 23, 2019
appAuthServiceWorkerCore.js Introduction of IE 11 support Jul 23, 2019
compat-copyrights.js Adds appropriate license and copyright notices Jul 23, 2019
copyrights.js Adds appropriate license and copyright notices Jul 23, 2019
index.html Introduction of IE 11 support Jul 23, 2019
package-lock.json 0.2.0 Jul 23, 2019
package.json 0.2.0 Jul 23, 2019
service_workers.md Updates to docs regarding IE support via XHR proxy Jul 23, 2019

README.md

App Auth JS Helper

Wrapper for AppAuthJS to assist with the full OAuth2 / OIDC token life-cycle.

Purpose

The primary goal of both AppAuth and this helper is to allow your single-page application to obtain OAuth2 access tokens and OpenID Connect id tokens. AppAuth for JavaScript provides an SDK for performing a PKCE-based Authorization Code flow within a JavaScript-based application. It is designed to be the generic underlying library for any type of JS app - not necessarily browser-based single-paged applications. The specific patterns for how you would use it within a single-page application are therefore not very clear. The goal of this helper library is to make that specific integration much easier.

There are several aspects that this helper aims to add on top of AppAuth:

  • Simpler application integration
  • Silent token acquisition
  • Pairing access tokens with resource servers
  • Transparent access token usage
  • Automatic access token renewal
  • Simple log out support
  • Direct access to id token claims
  • Token storage
  • Support for alternative log-in flows

Why PKCE for a Single-Page App?

Single-page applications are called a "user-agent-based application" in the OAuth2 Spec for Client Types. As it says in the description for these sorts of clients, they are "public" clients - this means they are "incapable of maintaining the confidentiality of their credentials". These sorts of clients typically have no "client secret" associated with them, and so to obtain an access token they must be implemented with a grant type that does not require one.

Public clients have two types of grants available to implement - Authorization Code and Implicit. Based on the descriptions in the specification, it may appear that a SPA should be built using the implicit grant; however, industry trends and best current practices that have emerged since the initial spec was written suggest that this is not the best choice after all. Instead, use of the authorization code grant as a public client is considered more secure.

While the authorization code grant is an improvement over implicit, there is one additional concern remaining - the risk of exposing the code during redirection. Any malicious third party that is able to intercept a public client's code could use that code to obtain an access token. The PKCE extension to OAuth2 was designed specifically to protect against this type of exposure. While it should be very difficult to intercept an authorization code served over HTTPS, using PKCE provides a valuable additional layer of protection.

How it works

Your SPA needs access tokens so that it can make requests to resource server endpoints. It needs id tokens so that it can know who is logged in (and possibly also so that it can know other details about the user's session). This helper reduces the boilerplate code that you would need to write in order to invoke AppAuth for these purposes.

In order to obtain those tokens, the browser operates as an OIDC Relying Party (RP). It initiates a PKCE-based authorization code flow to the OpenID Provider (OP), the completion of which results in fresh tokens. The difficulty is that this flow normally involves a very noticeable and jarring redirection of the browser. Sometimes, that is unavoidable - when the user isn't currently logged into the OP, then they have to do so. But if the user has a valid session within the OP (and if they have already granted consent for the scopes this RP is asking for) then that obvious redirection shouldn't be necessary.

To make the interaction between the RP and the OP more smooth, this library is designed to hide most of it from your application code. To do this, it uses a hidden iframe and an "identity proxy". When the user has an active OP session, the hidden iframe will silently obtain the tokens - there is no obvious browser redirection involved. When your application code makes a request to a resource server, the identity proxy will intercept that request and add the appropriate access token to the request. This allows your SPA to worry much less about token management and instead focus on the business logic associated with the resource server APIs.

Each resource server your application wants to work with should use a unique access token. This is considered the best practice, as it limits the exposure of scopes to only those resource servers which ought to be using them. AppAuthHelper automates this best practice - for each resource server you declare, a unique access token (with the appropriate scopes) will be requested. The identity proxy will find the appropriate token for your request and automatically include it.

In the case when your request fails because the access token has expired, the identity proxy will automatically attempt to obtain a fresh access token and then retry the request with that fresh token. This can be detected when the resource server responds with a www-authenticate header along with an error=invalid_token detail. If this happens while the user still has a valid session within the OP, a new access token can be silently obtained using the iframe. This is essentially the process described in https://tools.ietf.org/html/rfc6750#section-3.1:

invalid_token The access token provided is expired, revoked, malformed, or invalid for other reasons. The resource SHOULD respond with the HTTP 401 (Unauthorized) status code. The client MAY request a new access token and retry the protected resource request.

Thanks to the identity proxy, you won't have to worry about implementing this retry logic yourself. Just make the calls to your APIs and let the proxy handle the tokens. For more details on how the identity proxy accomplishes this, review this article: Service Workers as an Identity Proxy.

Using this library

A trivial example for using this library is included as part of the project, under index.html.

You will need to make sure at least two files are included in the base of your application - appAuthHelperRedirect.html and appAuthServiceWorker.js. If you want to call these files different names, you will need to provide their new names to the AppAuthHelper.init function. Also, you will need to make sure the path within appAuthHelperRedirect.html (or its equivalent) to appAuthHelperFetchTokensBundle.js is accurate.

You will also need to register appAuthHelperRedirect.html (or its equivalent) as the redirect_uri within your OP.

Next, you need to alter your application code to invoke the module. The "AppAuthHelper" module can be loaded in two ways:

  • using a global variable by directly including a script tag: <script src="appAuthHelperBundle.js"></script>
  • as a CommonJS module: var AppAuthHelper = require('appauthhelper');

Once the library is loaded, you have to provide the environmental details along with the function you'd like to trigger when the tokens are available. Here's an example:

Initializing the environment:

AppAuthHelper.init({
    clientId: "myRP",
    authorizationEndpoint: "https://login.example.com/oauth2/authorize",
    tokenEndpoint: "https://login.example.com/oauth2/access_token",
    revocationEndpoint: "https://login.example.com/oauth2/token/revoke",
    endSessionEndpoint: "https://login.example.com/oauth2/connect/endSession",
    resourceServers: {
        "https://login.example.com/oauth2/userinfo": "profile",
        "https://rs.example.com/": "rs_custom_scope"
    },
    interactionRequiredHandler: function () {
        // Add whatever is appropriate for your app to do when the user needs to log in.
        // Default behavior (when this handler is unspecified) is to redirect the window
        // to the authorizationEndpoint.

        // A good example of something you might want to do is render the authorizationEndpoint login prompt
        // within an iframe (for a more tightly-integrated login experience). You can do that like so:

        AppAuthHelper.iframeRedirect(document.getElementById('loginIframe'));

        // this assumes that 'loginIframe' is an iframe that has already been mounted to the DOM
    },
    tokensAvailableHandler: function (claims) {
        // This is a great place to startup the parts of your SPA that are for logged-in users.
        // The "claims" parameter is the content of the id_token, which tells you useful details
        // about the logged-in user.

        // At this point your application code can start making network calls to the resource servers
        // you have configured, above.
        fetch("https://login.example.com/oauth2/userinfo").then((resp) => resp.json()).then((profile) => {
            //...
        })
    },
    renewCooldownPeriod: 1,
    oidc: true,
    redirectUri: "appAuthHelperRedirect.html", // can be a relative or absolute url
    serviceWorkerUri: "appAuthServiceWorker.js" // can be a relative or absolute url
});

Details you need to provide to the init function:

  • clientId - The id of this RP client within the OP
  • scopes - Space-delimited list of scopes requested by this RP
  • authorizationEndpoint - Full URL to the OP authorization endpoint
  • tokenEndpoint - Full URL to the OP token endpoint
  • revocationEndpoint - Full URL to the OP revocation endpoint
  • endSessionEndpoint - Full URL to the OP end session endpoint
  • resourceServers - Optional map of resource server urls to the scopes which they require. Map values are space-delimited list of scopes requested by this RP for use with this RS. If not specified, no tokens will be automatically included in any network request.
  • tokensAvailableHandler - function to be called when tokens are first available
  • interactionRequiredHandler - optional function to be called when the user needs to interact with the OP; for example, to log in.
  • renewCooldownPeriod [default: 1] - Minimum time (in seconds) between requests to the authorizationEndpoint for token renewal attempts
  • oidc [default: true] - indicate whether or not you want to get back an id_token
  • redirectUri [default: appAuthHelperRedirect.html] - The redirect uri registered in the OP
  • serviceWorkerUri [default: appAuthServiceWorker.js] - Path to the service worker script. Make sure it is located low enough in your URL path so that its scope encapsulates all application code making network requests. See Why is my service worker failing to register? if you have questions.

You will need to make sure the redirect_uri used for this is registered with the OP. By default, you can use the included appAuthHelperRedirect.html as the uri to register. Whatever you choose to use, be sure there is similar JavaScript code as is included within appAuthHelperRedirect.html.

Requesting tokens:

AppAuthHelper.getTokens();

When this function is called, the library will work to return tokens to your application (via the tokensAvailableHandler defined in the init function). If there are existing tokens in browser storage (IndexedDB), this function will be called immediately. Otherwise, there will be a background authorization code flow initiated. If there is an active session in the OP (such that the tokens can be returned immediately without user interaction) then those will be fetched and saved in browser storage, followed by triggering that function.

If there is no way to fetch the tokens non-interactively, the default behavior is for the parent frame to be redirected to the OP authorization endpoint, allowing the user to log in (and possibly provide consent for this RP). Upon successful authentication, the OP will redirect you back to the configured "redirectUri" which will resume execution within your SPA (ultimately using the authorization code returned to fetch the tokens and save them in browser storage).

Logging in within an iframe:

If you want your users to be able to log in without having to leave your app, you can specify that behavior in the interactionRequiredHandler. For example, you can declare an iframe somewhere within your page and then using interactionRequiredHandler you can provide that frame reference to AppAuthHelper.iframeRedirect. This will trigger an immediate call to the OP's authentication endpoint within the context of that frame. When the user returns from the OP, the tokensAvailableHandler will be triggered in the same way as it would if the user had been redirected within the context of the full window.

Logging Out:

AppAuthHelper.logout().then(function () {
    // whatever your application should do after the tokens are removed
});

Calling AppAuthHelper.logout() will trigger calls to both the access token revocation endpoint, as well as the id token end session endpoint. When both of those have completed, the tokens are removed from browser storage. A promise is returned from logout(); use .then() to do whatever is appropriate for your application after the session is terminated.

Using Tokens

Once tokensAvailableHandler has been called, your application can start using the tokens. If you are curious about them, you can find them within your browser's IndexedDB under "appAuth/clientId". You shouldn't need to develop any code that directly accesses them there, however; you should be able to rely on the identity proxy managing them for your requests. Instead just make simple calls to your resource server APIs, and trust that the Authorization header with the access token included as a bearer will be added. For example:

fetch("https://login.example.com/oauth2/userinfo").then((resp) => resp.json())

You can also read the details about the authenticated user (called "claims") from the argument passed to the tokensAvailableHandler. Claims are useful for your application, particularly if you need your application to behave differently for different types of users. The structure of the claims object is like so:

{
    "at_hash": "7LsOpEFOK4zH46H96iDOHg",
    "sub": "amadmin",
    "auditTrackingId": "b2e094db-b135-4504-85a2-05897fcb7e6c-31192",
    "iss": "https://login.example.com/oauth2",
    "tokenName": "id_token",
    "aud": "appAuthClient",
    "c_hash": "X7O8AL3Zt4B2Cr6BwmeFmg",
    "acr": "0",
    "org.forgerock.openidconnect.ops": "xJ-cc7K4RQR6gx4kNrfLIIRNg5k",
    "s_hash": "I3riYOxd8FcFEm0aPZrxaw",
    "azp": "appAuthClient",
    "auth_time": 1540235130,
    "realm": "/",
    "exp": 1540238731,
    "tokenType": "JWTToken",
    "iat": 1540235131
}

Depending on the settings in your OP, there may be more claim values included. See the OpenID Connect spec on claims for more details.

Supporting Legacy Browsers

If you want to support legacy browsers (such as IE 11) which do not support service workers (and other modern browser features like promises, fetch, crypto, etc...) you can do so with the "compat" build of AppAuthHelper. There is a cost associated with this support, in terms of download size. The "compat" build is about 60kb bigger than the "modern" build, due to the need to supply the various feature polyfills. However, if you decide that this support is required, it is an option.

To include the "compat" version in your CommonJS environment, you simply need to refer to the correct module path, like so:

var AppAuthHelper = require('appauthhelper/appAuthHelperCompat');

If you are using AppAuthHelper as a global variable, you can get the "compat" build by using the npm script defined within package.json, like so:

npm run build-compat

This will produce versions of appAuthHelperBundle.js and appAuthHelperFetchTokensBundle.js that are usable in IE 11. For convenience, this is the version that is built by default and checked-into the source project. If you want the slimmed-down, modern build you simply need to use this command, instead:

npm run build

Note that there is no straight polyfill for service worker support. Instead, appAuthHelper will detect whether or not service workers are supported in the browser; if not, it will fall back to a alternative identity proxy implementation that is built on customizing the XMLHttpRequest object. The end result is the same behavior for your code, so you shouldn't need to worry about which implementation is ultimately used.

Contributing

AppAuthHelper was developed by ForgeRock, Inc. Please file issues and open pull requests, as you see fit.

License

Apache 2.0. Portions Copyright ForgeRock, Inc. 2018-2019

You can’t perform that action at this time.