Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Welcome to cbSSO a ColdBox module to help integrate SSO into your application easily.
Bundled in this module are several SSO provider implementations that allow you to quickly and easily integrate with Identity Providers such as Microsoft, Google, GitHub or Facebook using standard protocols like SAML and oAuth.
To install run
box install cbsso
Once installed you can configure your settings like so
Your app now has the ability to direct users to Google for authentication!
The rest of this documentation will cover topics like
Built-in Providers
Custom Providers
Configuration
Accessing User Data
The cbSSO module is maintained under the guidelines as much as possible. Releases will be numbered in the following format:
And constructed with the following guidelines:
Breaking backward compatibility bumps the major (and resets the minor and patch)
New additions without breaking backward compatibility bumps the minor (and resets the patch)
Bug fixes and misc changes bumps the patch
Apache 2 License: ​
Code:
Issues:
The cbSSO module is a professional open-source software backed by offering services like:
Custom Development
Professional Support & Mentoring
Training
Server Tuning
Because of His grace, this project exists. If you don't like this, then don't read it; it's not for you.
"Therefore being justified by faith, we have peace with God through our Lord Jesus Christ: By whom also we have access by faith into this grace wherein we stand, and rejoice in hope of the glory of God." Romans 5:5
// config/modules/cbsso.cfc
component {
function configure(){
return {
"providers": [
{
type: "GoogleProvider@cbsso",
clientId: getJavaSystem().getProperty( "GOOGLE_CLIENT_ID" ),
clientSecret: getJavaSystem().getProperty( "GOOGLE_CLIENT_SECRET" )
}
]
};
}
}Code Reviews
<major>.<minor>.<patch>CBSSO publishes the following interception points.
CBSSOAuthorization
CBSSOMissingProvider
Even though there are multiple different SSO strategies out in the wild they generally tend to work on similiar principles. A user visits a website (the service provider) and is provided a set of options for authentication (identity providers). The user selects one of the presented identity providers and proceedes to login in and authorize the service provider to access some of their information. The identity provider then redirects the user back to the web application and confirms to the service provider that the user is who they say they are.
This module provides the utilities necessary to accomodate this pattern while being flexible enough to allow for the differences between different protocols and identity providers.
When installed cbSSO provides some routes to your application.
/cbsso/auth/:providerName/start
This route is used by your application to determine which provider a user has selected and to provide the application a chance to do any work necessary before redirecting to the identity provider service.
/cbsso/auth/:providerName
This route is used as the redirect URI for the identity provider to communicate back to your application.
Our GoogleProvider is meant to be used with .
component {
public any function configure(){
return {
"providers" : [
{
// name is optional, can be used to control the redirect uri
// with name: https://your.app.com/cbsso/auth/g
// without name: https://your.app.com/cbsso/auth/google
name: "g",
type: "GoogleProvider@cbsso",
// these values are configured with Microsoft and available in your app dashboard
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET",
// optional - this is the default
scope: "openid profile email"
}
]
};
}
}The GitHubProvider can be configured to interact with Github. You can find their documentation here https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow
component {
public any function configure(){
return {
"providers" : [
{
// name is optional, can be used to control the redirect uri
// with name: https://your.app.com/cbsso/auth/g
// without name: https://your.app.com/cbsso/auth/GitHub
name: "g",
type: "GitHubProvider@cbsso",
// these values are configured with Microsoft and available in your app dashboard
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET",
// optional - this is the default
scope: "user user:email"
}
]
};
}
}An example config/cbssso.cfc file.
component {
public any function configure(){
return {
// enable this for cbAuth integration
"enableCBAuthIntegration": false,
// where cbSSO should redirect when a SSO has an unhandled error
"errorRedirect": "",
// where cbSSO should redirect after a successful proecess
// you probably won't need this and will instead want to redirect manually
// in the CBSSOOnAuthorization interception event
"successRedirect": "",
// enable this for easy integration with cbSecurity
// see the "CBSecurity Integration" section for more information
"enableCBSecurityIntegration": false,
// register your SSO providers
"providers" : [
{
// name is optional, can be used to control the redirect uri
// with name: https://your.app.com/cbsso/auth/fbook
// without name: https://your.app.com/cbsso/auth/Facebook
name: "fbook",
type: "FacebookProvider@cbsso",
// these values are configured with Microsoft and available in your app dashboard
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET"
}
]
};
}
}The cbSSO module was built to be as easy to use as possible and configuration can be as simple as registering a single provider.
Provided out of the box but disabled by default is the ability to integrate your SSO workflow with the cbSecurity module. In order to fully enable the integration between the two modules you will need to enable this setting and also implement some additional functions in your UserService.
See UserService Additions for more information.
Out of the box we have implemented the following providers for your use.
Each provider implements the cbsso.models.ISSOIntegrationProvider interface and provides the necessary functionality to interact with a specific identity provider.
This interface is primarily for use by the ProviderService@cbsso and is not necessarily intended for direct use but is available if you need it.
In case you come across a situation where one of our provided providers doesn't provide for your prerequisites you can implement your own custom provider. Use the following template to get started.
Once your CustomProvider is implemented you can configure it within your application like so.
CBSSO is buitl to be cbAuth aware out of the box. Setting up cbAuth is very similiar to writing your own interceptor for a custom implementation but reduces the boilerplate and is much more organized.
Wherever you have configured your cbSSO settings you will need to set enableCBAuthIntegration to true.
By enabling this setting cbSSO will register an interceptor that provides the basic pattern for receiving a response from an SSO provider and connecting it to a user. While cbSSO is able to enforce a general pattern for handling the SSO authentication flow it needs the application to provide some specific functionality. It does this through interacting with your application's implementation of cbAuth's
interface {
public string function getName();
public string function startAuthenticationWorflow( required any event );
public any function processAuthorizationEvent( required any event );
}// this can be anywhere but we will say it is in models/CustomProvider.cfc
component implements="cbsso.models.ISSOIntegrationProvider" accessors=true {
// custom properties you need
property name="clientId";
property name="clientSecret";
// The name of your provider
// this will be used to construct the redirect URI
// in this case it will be /cbsso/auth/CustomProvider
public string function getName(){
return "CustomProvider";
}
// Return the icon url for easy rendering.
// This can return an empty string if you don't use the
// auto icon rendering feature of the ProviderService.
public string function getIconURL(){
return "";
}
// Return the auth endpoint URL of your identity provider.
public string function startAuthenticationWorflow( required any event ){
return "http://someidentityprovider.com/oauth/signin";
}
// This function will be called when the identity provider responds.
// You should process the request from the IP and return a
// properly configured instance of SSOAuthorizationResponse.
public any function processAuthorizationEvent( required any event ){
var authResponse = wirebox.getInstance( "SSOAuthorizationResponse@cbsso" );
return authResponse.setWasSuccessful( true )
.setFirstName( event.getValue( "firstName" ) )
.setEmail( event.getValue( "email" ) );
}
}component {
public any function configure(){
return {
"providers" : [
{
// provide the proper dsl to target your custom provider
type: "CustomProvider",
// any properties here will invoke their associated setter
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET"
}
]
};
}
}IUserServiceIntegrating cbSSO and cbAuth requires that your IUserService implement the following functions.
Once these pieces are all in place you should have a fully working SSO implementation fully integrated with cbAuth!
/**
* This function is used to tell cbSSO which user is associated with an ssoAuthorizationResponse.
*
* @param ssoAuthorizationResponse An instance of ISSOAuthorizationResponse that was successful
* @param provider The configured provider used for this SSO event
*
* @return An cbAuth.models.IUser instance or null
*/
public any function findBySSO( required any ssoAuthorizationResponse, required any provider );
/**
* Create a new user based off of information from the ISSOAuthorizationResponse.
*
* @param ssoAuthorizationResponse An instance of ISSOAuthorizationResponse that was successful
* @param provider The configured provider used for this SSO event
*
* @return An cbAuth.models.IUser instance
*/
public any function createFromSSO( required any ssoAuthorizationResponse, required any provider );
/**
* Update an existing user based off of the ssoAuthorizationResponse.
*
* @param The user associated with the ssoAuthorizationResponse
* @param ssoAuthorizationResponse An instance of ISSOAuthorizationResponse that was successful
* @param provider The configured provider used for this SSO event
*
*/
public void function updateFromSSO( required any user, required any ssoAuthorizationResponse, required any provider );MicrosoftSAMLProvider you will need to add some java libraries to your server.
If using a CommandBox server.json you can do that like socomponent {
public any function configure(){
return {
"providers" : [
{
// name is optional, can be used to control the redirect uri
// with name: https://your.app.com/cbsso/auth/entra
// without name: https://your.app.com/cbsso/auth/MicrosoftSAMLProvider
name: "entra",
type: "MicrosoftSAMLProvider@cbsso",
// these values are configured with Microsoft and available in your app dashboard
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET",
authEndpoint: "https://login.microsoftonline.com/YOUR-TENANT-ID/saml2",
expectedIssuer: "https://sts.windows.net/YOUR-TENANT-ID/",
federationMetadataURL: "https://login.microsoftonline.com/YOUR-TENANT-ID/federationmetadata/2007-06/federationmetadata.xml"
}
]
};
}
}```jsonc
{
"app":{
// add this line to ensure the java library is loaded at the appropriate level
"libDirs":"modules/cbsso/lib"
}
}
```The busy developer's guide to getting up and running in no time.
Suppose we wanted to add GitHub as a SSO provider to our application located at myssoapp.com
Navigate to https://github.com/settings/developers.
Click the "New OAuth App" button to bring up the following form.
Once your app is registered you will see a screen with your client credentials. You need to get the provided client ID as well as generate a new client secret. Make sure you save it! Many systems do not allow you see the secret after it is first generated!
This can be configured in either your ColdBox.cfc or in config/modules/cbSSO.cfc
Now that our app is registered with GitHub and our client credentials have been configured in our module settings, we must handle the event. This is done through an interception point.
The final step is to prevent our SSO options to a user.

moduleSettings = {
"cbsso" : {
"providers" : [
{
type: "GitHubProvider@cbsso",
clientId: getJavaSystem().getProperty( "GITHUB_CLIENT_ID" ),
clientSecret: getJavaSystem().getProperty( "GITHUB_CLIENT_SECRET" )
}
]
}
};public void function CBSSOAuthorization( event, data ){
// the provider that was used for SSO
var provider = data.provider;
// an instance of ISSOAuthorizationResponse that contains our data
var ssoAuthorizationResponse = data.ssoAuthorizationResponse;
if( !ssoAuthorizationResponse.wasSuccessful() ){
logger.error( "Failed SSO workflow: #ssoAuthorizationResponse.getErrorMessage()#" );
relocate( "/login" );
}
var user = UserService.findByEmail( ssoAuthorizationResponse.getEmail() );
// check if we have a user record for this person already
if( isNull( user ) ){
user = UserService.new();
user.setEmail( ssoAuthorizationResponse.getEmail() );
user.setFirstName( ssoAuthorizationResponse.getFirstName() );
user.setLastName( ssoAuthorizationResponse.getLastName() );
user.save();
}
// log them in - YAY SSO!
user.login();
relocate( "/dashboard" );
} <!-- in views/login.cfm -->
<cfoutput>
<cfscript>
providerOptions = getInstance( "ProviderService@cbsso" ).getProviderOptions();
</cfscript>
<form action="/login">
<h2>Login</h2>
<input name="username" type="text" />
<input name="password" type="password" />
<button type="submit">Submit</button>
</form>
<p>-- or --</p>
<div>
<!-- Loop through our providers and use the provided URL to start SSO -->
<cfloop array="#providerOptions#" index="option" />
<a class="link-button" href="#option.url#">Continue with #option.name#</a>
</cfloop>
</div>
</cfoutput>


There are two options for starting a SSO workflow. You can either present a UI with options for your user to pick from or initiate the flow programmatically.
Ultimately providing SSO choices to your user will be an application specific task. That being said, many applications present their SSO integration in a similar fashion choosing to display some buttons which each initiate a SSO workflow with a different provider.
This can be set up fairly easily using the ProviderService
<!-- in views/login.cfm -->
<cfoutput>
<cfscript>
providerOptions = getInstance( "ProviderService@cbsso" ).getRenderableProviderData();
</cfscript>
<form action="/login">
<h2>Login</h2>
<input name="username" type="text" />
<input name="password" type="password" />
<button type="submit">Submit</button>
</form>
<p>-- or --</p>
<div>
<cfloop array="#providerOptions#" index="option" />
<a class="link-button" href="#option.url#">Continue with #option.name#</a>
</cfloop>
</div>
</cfoutput>To programmatically start a SSO workflow you can use the following code.
This will redirect the current request to the /cbsso/auth/:providerName endpoint of your application that is provided by this module.
Ultimately whether the user clicks a link or we redirect them programmatically they will end up at the same place. First, we will determine which of our configured providers they are using and then we will redirect them to the appropriate service.
At this point the user is now the responsibility of the identity provider. If they have not logged in before they will be prompted to review the permissions scope of your application and decide whether or not they will grant permission to your application to see their user data. Whether they accept or reject the request they will be redirected back to your application and you will have a chance to handle the response.
getInstance( "ProviderService@cbsso" ).start( providerName );To use Facebook as an identity provider you will need to configure the FacebookProvider . In order to fully configure it you will need to register for a Facebook developer account on their developer site.
component {
public any function configure(){
return {
"providers" : [
{
// name is optional, can be used to control the redirect uri
// with name: https://your.app.com/cbsso/auth/fbook
// without name: https://your.app.com/cbsso/auth/Facebook
name: "fbook",
type: "FacebookProvider@cbsso",
// these values are configured with Microsoft and available in your app dashboard
clientId: "YOUR-CLIENT-ID",
clientSecret: "YOUR-CLIENT-SECRET",
// optional - this is the default
scope: "openid email"
}
]
};
}
}While most identity providers follow similar protocols there is room for each one to provide slightly different workflows. Some providers use SAML, some use oAuth. And even if two providers use the same protocol there may be variation in how they use or present different pieces of information.
Part of cbSSO's goal is to help hide as much of these implementation details as possible. To accomplish this, we have created the cbsso.models.ISSOAuthorizationResponse interface and have provided an implementation as well.
After a SSO workflow has been initiated eventually the identity provider will respond to the initiating application. The format of the response varies by provider. To handle the responses each provider implements a method (processAuthorizationEvent )that will take the response, parse it, transform it into an ISSOAuthorizationResponse and return it to your app for further processing.
Once a response has been received and parsed cbSSO fires of the CBSSOAuthorization event. One way you could handle this event would be
Unless you implement a custom provider you shouldn't need to worry to much about how the SSO responses are handled. That being said, here is an example that shows how the GitHub provider implements this processAuthorizationEvent( required any event ) method.
public void function CBSSOAuthorization( event, data ){
// the provider that was used for SSO
var provider = data.provider;
// an instance of ISSOAuthorizationResponse that contains our data
var ssoAuthorizationResponse = data.ssoAuthorizationResponse;
if( !ssoAuthorizationResponse.wasSuccessful() ){
logger.error( "Failed SSO workflow: #ssoAuthorizationResponse.getErrorMessage()#" );
relocate( "/login" );
}
var user = UserService.findByEmail( ssoAuthorizationResponse.getEmail() );
// check if we have a user record for this person already
if( isNull( user ) ){
user = UserService.new();
user.setEmail( ssoAuthorizationResponse.getEmail() );
user.setFirstName( ssoAuthorizationResponse.getFirstName() );
user.setLastName( ssoAuthorizationResponse.getLastName() );
user.save();
}
// log them in - YAY SSO!
user.login();
relocate( "/dashboard" );
} public any function processAuthorizationEvent( required any event ){
// generate our SSOAuthorizationResponse which implements ISSOAuthorizationResponse
var authResponse = wirebox.getInstance( "SSOAuthorizationResponse@cbsso" );
var rawData = {
"authResponse": {},
"accessResponse": {},
"userData": {}
};
authResponse.setRawResponseData( rawData );
try {
rawData[ "authResponse" ] = event.getCollection();
// there was an error with our request or the user rejected us
if( event.getValue( "error", "" ) != "" ){
return authResponse
.setWasSuccessful( false )
.setErrorMessage( event.getValue( "error" ) );
}
// at this point we have been granted access on behalf of the user
// so we can request an access_token
var res = oAuthService.makeAccessTokenRequest(
getClientId(),
getClientSecret(),
getRedirectUri(),
getAccessTokenEndpoint(),
event.getValue( "code" )
);
var accessData = parseAccessData( res.getData().toString() );
rawData[ "accessResponse" ] = accessData;
// request information about the current user
var userDataRes = hyper
.setMethod( "GET" )
.setUrl( variables.userInfoURL )
.setHeaders( { "Authorization": "Bearer " & accessData.access_token } )
.send();
// parse the user data
var userData = deserializeJSON( userDataRes.getData() );
rawData[ "userData" ] = userData;
// transform our authorization data and user data into our interface format
return authResponse
.setWasSuccessful( true )
.setName( userData.name )
.setEmail( userData.email )
.setUserId( userData.id )
}
catch( any e ){
return authResponse
.setWasSuccessful( false )
.setErrorMessage( e.message );
}
}