SSO Integration in ColdBox
Overview
Single Sign-On (SSO) lets users authenticate via external identity providers (Google, Azure AD, Okta, GitHub, etc.) using OAuth2 / OpenID Connect. The cbsso module provides the OAuth2 flow and provider configuration.
Language Mode Reference
Examples use BoxLang (.bx) syntax by default. Adapt for your target language:
| Concept | BoxLang (.bx) |
CFML (.cfc) |
|---|---|---|
| Class declaration | class [extends="..."] { |
component [extends="..."] { |
| DI annotation | @inject above property name="svc"; |
property name="svc" inject="svc"; |
| View templates | .bxm suffix |
.cfm / .cfml suffix |
| Tag prefix | <bx:if>, <bx:output>, <bx:set> |
<cfif>, <cfoutput>, <cfset> |
CFML Compat Mode: With BoxLang + CFML Compat module,
.bxand.cfcfiles coexist freely. BoxLang-native classes useclass {}(.bxfiles); CFML-compat classes usecomponent {}(.cfcfiles).
Installation
install cbsso
install cbauth
Module Configuration
// config/ColdBox.cfc
moduleSettings = {
cbsso: {
defaultProvider: "google",
providers: {
google: {
clientID: getSystemSetting( "GOOGLE_CLIENT_ID", "" ),
clientSecret: getSystemSetting( "GOOGLE_CLIENT_SECRET", "" ),
redirectURI: "https://app.example.com/sso/callback/google",
scope: "openid email profile"
},
azure: {
clientID: getSystemSetting( "AZURE_CLIENT_ID", "" ),
clientSecret: getSystemSetting( "AZURE_CLIENT_SECRET", "" ),
tenantID: getSystemSetting( "AZURE_TENANT_ID", "common" ),
redirectURI: "https://app.example.com/sso/callback/azure",
scope: "openid email profile"
},
okta: {
clientID: getSystemSetting( "OKTA_CLIENT_ID", "" ),
clientSecret: getSystemSetting( "OKTA_CLIENT_SECRET", "" ),
domain: getSystemSetting( "OKTA_DOMAIN", "" ),
redirectURI: "https://app.example.com/sso/callback/okta",
scope: "openid email profile"
}
}
}
}
SSO Handler
/**
* handlers/SSO.cfc
*/
class extends="coldbox.system.EventHandler" {
property name="ssoService" inject="SSOService" // cbsso
property name="authService" inject="AuthService"
property name="userService" inject="UserService"
property name="cbauth" inject="@cbauth"
/**
* Redirect user to provider's login page.
* GET /sso/login/:provider
*/
function login( event, rc, prc ) {
var provider = rc.provider ?: "google"
var authURL = ssoService.buildAuthorizationURL( provider )
relocate( authURL )
}
/**
* OAuth2 callback — exchange code, load/create user, log them in.
* GET /sso/callback/:provider
*/
function callback( event, rc, prc ) {
var provider = rc.provider ?: "google"
// Exchange authorization code for tokens + identity
try {
var identity = ssoService.exchangeCode(
provider = provider,
code = rc.code,
state = rc.state ?: ""
)
} catch ( any e ) {
flash.put( "error", "SSO authentication failed: " & e.message )
return relocate( "auth.login" )
}
// Find or create local user
var user = userService.findByEmail( identity.email )
if ( isNull( user ) ) {
user = userService.createFromSSO( {
email: identity.email,
name: identity.name,
avatar: identity.picture ?: "",
provider: provider,
sub: identity.sub // external user ID
} )
} else {
// Update SSO metadata
userService.updateSSOMetadata( user.getID(), provider, identity.sub )
}
// Log in via cbauth
cbauth.login( user )
flash.put( "success", "Welcome, " & user.getName() & "!" )
relocate( "dashboard" )
}
/**
* SSO logout — local + optional provider logout.
* GET /sso/logout
*/
function logout( event, rc, prc ) {
cbauth.logout()
relocate( "auth.login" )
}
}
CFML (.cfc):
/**
* handlers/SSO.cfc
*/
component extends="coldbox.system.EventHandler" {
property name="ssoService" inject="SSOService" // cbsso
property name="authService" inject="AuthService"
property name="userService" inject="UserService"
property name="cbauth" inject="@cbauth"
/**
* Redirect user to provider's login page.
* GET /sso/login/:provider
*/
function login( event, rc, prc ) {
var provider = rc.provider ?: "google"
var authURL = ssoService.buildAuthorizationURL( provider )
relocate( authURL )
}
/**
* OAuth2 callback — exchange code, load/create user, log them in.
* GET /sso/callback/:provider
*/
function callback( event, rc, prc ) {
var provider = rc.provider ?: "google"
// Exchange authorization code for tokens + identity
try {
var identity = ssoService.exchangeCode(
provider = provider,
code = rc.code,
state = rc.state ?: ""
)
} catch ( any e ) {
flash.put( "error", "SSO authentication failed: " & e.message )
return relocate( "auth.login" )
}
// Find or create local user
var user = userService.findByEmail( identity.email )
if ( isNull( user ) ) {
user = userService.createFromSSO( {
email: identity.email,
name: identity.name,
avatar: identity.picture ?: "",
provider: provider,
sub: identity.sub // external user ID
} )
} else {
// Update SSO metadata
userService.updateSSOMetadata( user.getID(), provider, identity.sub )
}
// Log in via cbauth
cbauth.login( user )
flash.put( "success", "Welcome, " & user.getName() & "!" )
relocate( "dashboard" )
}
/**
* SSO logout — local + optional provider logout.
* GET /sso/logout
*/
function logout( event, rc, prc ) {
cbauth.logout()
relocate( "auth.login" )
}
}
User Service SSO Methods
/**
* Additional methods on UserService for SSO account creation and linking.
*/
function createFromSSO( required struct identity ) {
var userID = createUUID()
queryExecute(
"INSERT INTO users (id, email, name, avatar, provider, external_id, email_verified)
VALUES (:id, :email, :name, :avatar, :provider, :sub, 1)",
{
id: userID,
email: arguments.identity.email,
name: arguments.identity.name,
avatar: arguments.identity.avatar,
provider: arguments.identity.provider,
sub: arguments.identity.sub
}
)
// Auto-assign default role
roleService.assignRole( userID, roleService.findByName( "member" ).id )
return findById( userID )
}
function updateSSOMetadata( required userID, required provider, required sub ) {
queryExecute(
"UPDATE users SET provider = :provider, external_id = :sub, last_login = NOW()
WHERE id = :id",
{ provider: arguments.provider, sub: arguments.sub, id: arguments.userID }
)
}
function findByExternalID( required provider, required sub ) {
var qry = queryExecute(
"SELECT * FROM users WHERE provider = :provider AND external_id = :sub",
{ provider: arguments.provider, sub: arguments.sub }
)
if ( !qry.recordCount ) return javaCast( "null", "" )
return entityToUser( qry )
}
View — Social Login Buttons
<!-- Login page with SSO buttons -->
<div class="social-login">
<a href="#event.buildLink('sso.login')#?provider=google"
class="btn btn-google">
<img src="/assets/img/google-icon.svg" alt="Google"> Sign in with Google
</a>
<a href="#event.buildLink('sso.login')#?provider=azure"
class="btn btn-azure">
<img src="/assets/img/microsoft-icon.svg" alt="Microsoft"> Sign in with Microsoft
</a>
<a href="#event.buildLink('sso.login')#?provider=okta"
class="btn btn-okta">
Sign in with Okta
</a>
</div>
Route Configuration
// config/Router.cfc
route( "/sso/login/:provider" ).to( "sso.login" )
route( "/sso/callback/:provider" ).to( "sso.callback" )
route( "/sso/logout" ).to( "sso.logout" )
SSO Provider Reference
| Provider | Required Config | Notes |
|---|---|---|
| clientID, clientSecret | Create at console.cloud.google.com | |
| Azure AD | clientID, clientSecret, tenantID | tenantID = your-tenant or "common" |
| Okta | clientID, clientSecret, domain | domain = yourorg.okta.com |
| GitHub | clientID, clientSecret | Scope: "read:user user:email" |
| Auth0 | clientID, clientSecret, domain | Most flexible option |
Security Notes
- Store OAuth2 secrets in environment variables, not source code
- Validate
stateparameter to prevent CSRF on callback - Always verify email_verified claim from provider
- Use HTTPS for all redirect URIs
- Log SSO login events for audit trail