Only this pageAll pages
Powered by GitBook
1 of 18

cbSSO

Loading...

Loading...

Usage

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Providers

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

cbAuth Integration

Loading...

Introduction

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

Versioning

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

License

Apache 2 License: ​

Important Links

  • Code:

  • Issues:

Professional Open Source

The cbSSO module is a professional open-source software backed by offering services like:

  • Custom Development

  • Professional Support & Mentoring

  • Training

  • Server Tuning

HONOR GOES TO GOD ABOVE ALL

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" )
                }
            ]
        };
    }
}
Security Hardening
  • Code Reviews

  • Much More

  • Semantic Versioning
    http://www.apache.org/licenses/LICENSE-2.0
    https://github.com/coldbox-modules/cbsso
    https://github.com/coldbox-modules/cbsso/issues
    Ortus Solutions, Corp
    Ortus Solutions, Corp
    <major>.<minor>.<patch>

    Interception Points

    CBSSO publishes the following interception points.

    • CBSSOAuthorization

    • CBSSOMissingProvider

    Provider Service

    Public Methods

    • public array function getRenderableProviderData() Use this to easily render options to initiate SSO workflows. This will return an array of structs with each having a name and url key.

    • public any function get( required name ){ Get a provider by name.

    How It Works

    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.

    GoogleProvider

    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"
            }
          ]
        };  
      }
    }
    https://developers.google.com/identity/protocols/oauth2

    GitHubProvider

    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"
            }
          ]
        };  
      }
    }

    Configuration

    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.

    The enableCBSecurityIntegration Setting

    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.

    Built-in Providers

    Out of the box we have implemented the following providers for your use.

    • FacebookProvider

    • GitHubProvider

    • GoogleProvider

    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.

    Custom Providers

    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.

    Enabling Integration

    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.

    Module Settings

    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 );
    }
    MicrosoftSAMLProvider
    // 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"
            }
          ]
        };  
      }
    }
    IUserService
    .

    Necessary IUserService Additions

    Integrating cbSSO and cbAuth requires that your IUserService implement the following functions.

    Running It

    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 );

    MircosoftSAMLProvider

    The MicrosoftSAMLProvider gives you the ability to integrate with Microsoft's Entra single sign-on service. You can read more about

    Example Configuration

    Additional Server Configuration

    If you are using the MicrosoftSAMLProvider you will need to add some java libraries to your server. If using a CommandBox server.json you can do that like so

    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/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"
            }
          ]
        };  
      }
    }
    Microsoft's Entra AuthNRequest workflow here.
    ```jsonc
    {
        "app":{
            // add this line to ensure the java library is loaded at the appropriate level
            "libDirs":"modules/cbsso/lib"
        }
    }
    ```

    Quick Start

    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

    Register app with GitHub

    Navigate to https://github.com/settings/developers.

    Click the "New OAuth App" button to bring up the following form.

    The default callback URL will be /cbsso/auth/:providerName. If you set a name for your provider it will be used in the URL. If your provider name is "foo" your URL for that provider workflow will be "https://myssoapp.com/cbsso/auth/foo"

    Notice that the authorization callback URL is often case sensitive!

    Gather Client Credentials

    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!

    Configure cbSSO Module Settings

    This can be configured in either your ColdBox.cfc or in config/modules/cbSSO.cfc

    Implement CBSSOAuthorization Event

    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.

    Render SSO Options to User

    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>

    Initiating SSO

    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.

    Displaying Choices to the User

    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>

    Programmatically Starting SSO

    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.

    What Happens Next

    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 );

    FacebookProvider

    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"
            }
          ]
        };  
      }
    }
    Social technologies | Meta for DevelopersMeta for Developers
    Logo

    Handling The Identity Provider Response

    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.

    Handling The ISSOAuthorizationResponse

    Once a response has been received and parsed cbSSO fires of the CBSSOAuthorization event. One way you could handle this event would be

    Behind The Scenes

    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 );
            }        
        }