Authentication

The Authentication Service provides a full authentication framework for stateless and session-based applications. It authenticates requests against back end providers and offers interpretation and parsing of request scope information to provide Single-Sign-On (SSO). Authenticated requests will have user accounts created and synchronized within Cloud CMS either automatically or as part of a registration form process. Authenticated users have Cloud CMS connectivity state managed for them.

The Authentication Service consists of the following:

  • Authentication Providers
  • Filters
  • Request Adapters

Using the Authentication Service, you can configure your Node applications so that end users can login using their credentials for pluggable Authentication Providers. The following providers are available out-of-the-box:

Many of these Authentication Providers are built using the Passport.js library - a well-established library that offers a wide variety of connectors for a diversity of authentication strategies. Implementing additional strategies is fairly straightforward.

The Authentication Service works with request scope variables to determine who the user is. It is compatible with Express and also supports JWT (Java Web Token) public and confidential tokens.

Authentication Providers

An Authentication Provider is a bit of code that connects the Authentication Service to a back end provider such as Facebook or Twitter. The Authentication Provider handles the handshake with the back end service and does the appropriate URL juggling necessary to let an end user sign in.

Authentication Providers follow a flow like this:

  1. The browser is directed to /auth/{providerId}
  2. The browser is then redirected to the authentication provider's login page.
  3. The end user logs in using their credentials.
  4. The authentication provider may then ask the user if they want to grant the application rights to log you in.
  5. If they fail to log in or do not grant rights, the browser is redirected to a failure page.
  6. If they do log in and grant rights, the browser is redirected to /auth/{providerId}/callback

In general, the callback receives a code that can be used to validate that the callback wasn't spoofed. The callback generally does the following:

  1. Checks with the authentication provider to verify the code.
  2. Acquires the access token and refresh token and user profile for the user.
  3. Checks to make sure that a Cloud CMS user exists in the domain for the application.

At this point, one of three things can happen:

a. If a user doesn't exist and autoRegister is false, redirect to the failure page. b. If a user doesn't exist and autoRegister is true, then create a user. c. If a user doesn't exist and registrationRedirect is set, then redirect the user for registration. The registration form, once submitted, results in a user being created in Cloud CMS and the process continues.

And then things continue:

  1. The Cloud CMS user is loaded and authenticated against.
  2. If you've supplied a custom login function, it will be executed.

Authentication providers are run either by redirecting the browser to the URL directly (`/auth/{providerId}') or via a configured Authentication Filter.

Configuration

Authentication providers are configured in the auth block within a providers block. Each provider should have a unique providerId. It should contain a type of provider as well as a configuration block that is passed to the provider factory method.

A sample configuration with common properties might look like this:

{
    "auth": {
        "enabled": true,
        "providers": {
            "{providerId}": {
                "type": "{providerType}",
                "config": {
                    "successRedirect": "",
                    "failureRedirect": "",
                    "passTokens": true,
                    "passTicket": true
                }
            }
        }
    }
}

Where:

  • successRedirect is the URL to redirect to once the authentication provider handshake has completed. This URL will be redirected to when the user has been authenticated, their Cloud CMS user has been synchronized and the driver connection has been established.
  • failureRedirect is the URL to redirect to if anything goes wrong in the handshake.
  • passTokens indicates whether to authenticated user's Cloud CMS access token back as a request parameter during the redirect to successRedirect
  • passTicket indicates whether to authenticated user's Cloud CMS ticket back as a request parameter during the redirect to successRedirect

Depending on the type of provider being configured, there may be additional properties needed in the configuration block, such as endpoint URLs, client IDs or application IDs required by the backend (such as Google or Facebook).

The following providers are available out-of-the-box:

Filters

By default, when requests arrive to the Express server, they are handled by Express middleware and eventually make their way to your route handler. The Cloud CMS authentication services don't step in the way and nothing really exciting happens at all.

In other words, the framework assumes by default that you're handling end-user authentication on your own, perhaps using an Express session or talking to your own custom database. There is no implicit assumption or requirement that you use the framework's authentication providers, Cloud CMS user sync or Cloud CMS backend user authentication.

This is assumed to be true because most often, in practice, your authentication requirements will differ from client to client and customer to customer. Some folks strive to build a purely standalone app and others want Cloud CMS to handle 100% of the authentication. Still others want to use Google as their authentication provider and have user accounts synchronized on-the-fly. And still others may have to bulk load user identities from an LDAP server and populate them in to Cloud CMS, then use an SSO token to automatically authenticate.

In the end, there are a lot of authentication strategies that you can imagine. Fortunately, the authentication framework's filters give you a way to adapt to most if not all of them.

Filters consist of a middleware function that runs early in the request handling chain. The filter looks at the incoming request and parses out identifier information, usually from a request header or a cookie. This identifier information is usually an SSO token, a JWT token or a session identifier.

Filters acquire this identifier through use of a Request Adapter.

The filter then determines whether the identifier information is trusted and whether anything can be extracted from it. In the case of JWT, for example, the user profile may be extractable from the token itself. Contrast that with OAuth 2.0 access tokens, which don't prescriptively contain any identifying information. JWT tokens that are encrypted and signed are examples of trusted tokens. They are efficient in that no additional trust assertion or profile loading is required.

If the identifier is untrusted, it is asserted against the filter's configured authentication provider. For example, you might have a Facebook access token. This token is passed back to Facebook to ask whether it is valid. This provides protection against spoofing (in which a middleman attempts to mock up an access token to trick their way in).

If trust cannot be established, the token is rejected and the framework either rejects the user with a 401 Unauthenticated error or it redirects them to the failureURL.

However, if trust is established, then the framework makes sure that a user profile is loaded from the authentication provider. As noted, this step is not required for JWT encrypted token case.

After this point, the authentication provider takes over and does its magic. It performs the same logic that happens during a validated callback.

  1. It asserts that a Cloud CMS user exists for this identifier in the current application's domain.
  2. If a user doesn't already exist...

  3. if autoRegister is set true, a user is automatically created

  4. if registrationRedirect is provided, the request is redirected to a registration process

  5. The user is then authenticated to Cloud CMS. The authentication information is cached so that subsequent requests pick up the authentication info and don't have to do the network hop each time.

The filter chain then completes and calls next() so that any additional middleware can run, including eventually your actual route function.

Filter chains consist of configuration that identifies a request adapter, an authentication provider and any other information needed.

A sample configuration might look like this:

{
    "auth": {
        "enabled": true,
        "adapters": {
            "foo": {
                "type": "default",
                "config": {
                    "cookie": "FACEBOOK_TOKEN"
                }
            }
        },
        "providers": {
            "bar": {
                "type": "facebook",
                "config": {
                    ...                    
                }
            }
        },
        "filters": {
            "{filterId}": {
                "adapter": "foo",
                "provider": "bar}"
            }        
        }
    }
}

The filter above interprets the incoming requesting using the given adapter and then interacts with the specified provider to figure out whether the user is trusted, who they are and to load their profile. The rest relies on the provider's configuration for user synchronization, Cloud CMS authentication and so on.

Request Adapters

In a fashion similar to authentication providers, request adapters configurations are declared in a block called adapters and are given an ID. The ID is then referenced from the filter.

Each request adapter configuration block provides a type and a config section that is loaded into the request adapter ahead of being executed.

At the moment, two request adapters are provided - default and jwt.

Default Request Adapter

The default request adapter parses simple header and cookie information.

A full configuration is like this:

{
    "adapters": {
        "{adapterId}": {
            "type": "default",
            "config": {
                "header": "{headerName}",
                "cookie": "{cookieName}",
                "trusted": false
            }
        }
    }
}

Where {adapterId} can be any unique ID across the adapters. This is the ID that you reference from the filters configuration section.

To grab the identifier from a header named SSO_TOKEN, you might do:

{
    "adapters": {
        "{adapterId}": {
            "type": "default",
            "config": {
                "header": "SSO_TOKEN"
            }
        }
    }
}

To grab the identifier from a cookie named USER:

{
    "adapters": {
        "{adapterId}": {
            "type": "default",
            "config": {
                "cookie": "USER"
            }
        }
    }
}

By default, the value acquired is assumed to be untrusted meaning that it needs to be passed back to the authentication provider to verify it's real. If you're in a secure architecture where the only way the request information is supplied is via a trusted source or something that you control and you're over HTTPS and you're a really smart dude, then you can force trust on like this:

{
    "adapters": {
        "{adapterId}": {
            "type": "default",
            "config": {
                "cookie": "USER",
                "trusted": true
            }
        }
    }
}

In general, it's best to stick with the defaults and leave it untrusted. There's a lot of bad people out there.

JWT Request Adapter

The JWT request adapter is similar to the default adapter in that it lets you pull a JWT token from either a named header or a cookie.

A full configuration is like this:

{
    "adapters": {
        "{adapterId}": {
            "type": "jwt",
            "config": {
                "header": "{headerName}",
                "cookie": "{cookieName}",
                "trusted": false,
                "secret": "{secret}",
                "field": "{userIdField}"
            }
        }
    }
}

The difference here is that the value received from either the header or the cookie consists of encoded (and optionally encrypted) JSON information that includes the profile.

If the value is encrypted, it is considered secure since the only way to decrypt it is to the know the shared secret that was used to encrypt it. This provides strong reassurance against a man-in-the-middle attack. However, if the value isn't encrypted and is simply encoded, then it might be useful but only if we validate it first.

In all cases, JWT tokens have a signature computed for them which provides some further assurance against a man-in-the-middle attack. However, the JWT Request Adapter will assume the token to be untrusted unless it is encrypted. If you wish, you can change this assumption using the trusted property.

The secret property lets you provide your shared secret so that any JWT encrypted tokens can be unencrypted and utilized by the framework.

Once the user profile is acquired, you should tell the request adapter which dot-delimited field to pick off inside of your user profile to serve as a primary key for your user. You can do this using the field property.

For example, suppose the user profile came through in a cryptographically signed JWT using the header JWT. It was signed with the secret abc123 and has a primary field which we identify as user.name.

The profile might look like:

{
    "id": "1234567890",
    "_json": {
        ... some stuff
    },
    "user": {
        "name": "jsmith",
        "firstName": "Joe",
        "lastName": "Smith"
    },
    "foo": {
        "bar": 42
    }
}

And the request adapter config could look like:

{
    "adapters": {
        "foo": {
            "type": "jwt",
            "config": {
                "header": "JWT",
                "secret": "abc123",
                "field": "user.name"
            }
        }
    }
}

This adapter will automatically be switched to trusted mode since the JWT value was encrypted. The user field is picked off and any backend user synchronization will utilize this information going forward.

Filters - Securing Routes

As noted, the authentication framework stays out the way unless you want it to play a role in your application. To get it involved, simply configure your authentication providers and any filters. And then bind it into your middleware.

The app.auth(filterId, fn) function is a factory that generates a middleware method to implement the given filter.

For example, suppose we have a filter defined like this:

{
    "auth": {
        "enabled": true,
        "adapters": {
            "bar": {
                "type": "default",
                "config": {
                    "header": "facebookID"
                }
            }
        },
        "providers": {
            "bar": {
                "type": "facebook",
                "config": {
                    ...                    
                }
            }
        },
        "filters": {
            "foo": {
                "adapter": "foo",
                "provider": "bar"
            }        
        }
    }
}

And suppose that we want the URI /mydocuments to be protected. The rest of the URLs in the site might be anonymous or allow guest access, but we want the /mydocuments route to require an authenticated user. In this case, we want the user to be redirected to Facebook, sign in, get their user account created in Cloud CMS and then proceed onward to see their documents.

We could do it like this:

server.routes(function(app, callback) {
    var auth = app.auth("foo");
    app.get("/documents", auth, function(req, res) {
        res.render("documents, {});
    });
});

The app.auth() method takes in the ID of a filter to execute. In this case, we want to execute the filter foo.

We can also pass in a function as the last argument that acts as a final method to execute once the user is logged in and everything is all set to go. This is often a good place to insert custom sign in logic.

var loginFn = function(req, res, next) {
    req.user = req.gitana_user;
    next();
};
server.routes(function(app, callback) {
    var auth = app.auth("foo", loginFn);
    app.get("/documents", auth, function(req, res) {
        res.render("documents, {});
    });
});

If you don't provide a function as a final argument, the loginFn shown above is used by default anyway. It simply binds the request's user variable to the Cloud CMS user representing the user. You only need to provide a custom function if you wish to change this behavior or set up the request differently ahead of any routes to the rest of your application.

You may also wish for your application to be protected globally which is to say that all routes should be protected by the authentication filter. Instead of specifying one route at a time, you can do something like this:

server.routes(function(app, callback) {

    var auth = app.auth("foo", loginFn);
    app.use("*", auth);

    app.get("/documents", function(req, res) {
        res.render("documents, {});
    });
});

Filters - Request Variables

When a filter completes its handshake, it will have loaded a number of useful variables onto the request. These are shown here:

  • req.gitana_user - the Cloud CMS user object
  • req.gitana_user_id - the Cloud CMS user ID
  • req.gitana_platform - the Driver Platform (authenticated as user)
  • req.gitana_apphelper - (optional) the Driver AppHelper (authenticated as user)
  • req.gitana_access_token - the OAuth 2.0 access token for connecting as this user to Cloud CMS
  • req.gitana_refresh_token - the OAuth 2.0 refresh token for connecting as this user to Cloud CMS
  • req.gitana_ticket - the ticket for connecting as this user to Cloud CMS
  • req.userGitana - either the platform the app helper, depending on what is configured in gitana.json

SDK

The Cloud CMS SDK provides examples of these service providers in action.

Please download the SDK to inspect the code and see how these are used in action.

Docker

In addition, a number of examples are provided as Docker kits for on-premise users. These include pre-integrated examples of SSO against Keycloak, Facebook, Twitter and LinkedIn.