ColdBox Authentication and Authorization
Follow Along:
Utah
Ortus Solutions
qb, Quick, Hyper, lots of other modules
1 wife, 3 kids, 1 dog
Type 1 Diabetic
Theatre Nerd
GIFs ahead
Responsible for authentication usernames and passwords as well as retrieving users based on ids and usernames.
"User" is a bit of a misnomer — it can be anything that can be authenticated against.
component name="UserService" {
public boolean function isValidCredentials( email, password ){
var user = newEntity().where( "email", email ).first();
if ( isNull( user ) ) {
return false;
}
return bcrypt.checkPassword( password, user.getPassword() );
}
public User function retrieveUserByUsername( email ){
return newEntity().where( "email", email ).firstOrFail();
}
public User function retrieveUserById( id ){
return newEntity().findOrFail( id );
}
}
Handles the plumbing of setting session variables, request variables, and authenticating with the UserService
/** Uses the UserService to do these tasks */
interface {
any function getUser();
boolean function isLoggedIn();
any function authenticate( required username, required password );
function login( required user );
function logout();
}
/**
* Authentication services for your application
*/
component singleton accessors="true" {
/* *********************************************************************
** DI
********************************************************************* */
property name="wirebox" inject="wirebox";
property name="interceptorService" inject="coldbox:interceptorService";
property name="sessionStorage" inject="SessionStorage@cbauth";
property name="requestStorage" inject="RequestStorage@cbauth";
property name="userServiceClass" inject="coldbox:setting:userServiceClass@cbauth";
/* *********************************************************************
** Static Vars
********************************************************************* */
variables.USER_ID_KEY = "cbauth__userId";
variables.USER_KEY = "cbauth__user";
/**
* Constructor
*/
function init() {
return this;
}
/**
* Logout a user
*/
public void function logout( boolean quiet = false ) {
// Annouce pre logout with or without user
if ( !arguments.quiet ) {
variables.interceptorService.processState(
"preLogout",
{ user: isLoggedIn() ? getUser() : javacast( "null", "" ) }
);
}
// cleanup
variables.sessionStorage.delete( variables.USER_ID_KEY );
variables.requestStorage.delete( variables.USER_KEY );
// Announce post logout
if ( !arguments.quiet ) {
variables.interceptorService.processState( "postLogout", {} );
}
}
/**
* Logout a user without raising interceptor events.
* Useful when testing "logged in" user no longer exists.
*/
public void function quietLogout() {
arguments.quiet = true;
logout( argumentCollection = arguments );
}
/**
* Login a user into our persistent scopes
*
* @user The user object to log in
*
* @return The same user object so you can do functional goodness
*/
public any function login( required user ) {
variables.interceptorService.processState( "preLogin", { user: arguments.user } );
variables.sessionStorage.set( variables.USER_ID_KEY, arguments.user.getId() );
variables.requestStorage.set( variables.USER_KEY, arguments.user );
variables.interceptorService.processState(
"postLogin",
{
user : arguments.user,
sessionStorage: variables.sessionStorage,
requestStorage: variables.requestStorage
}
);
return arguments.user;
}
/**
* Try to authenticate a user into the system. If the authentication fails an exception is thrown, else the logged in user object is returned
*
* @username The username to test
* @password The password to test
*
* @throws InvalidCredentials
*
* @return User : The logged in user object
*/
public any function authenticate( required string username, required string password ) {
variables.interceptorService.processState(
"preAuthentication",
{
"username": arguments.username,
"password": arguments.password
}
);
if ( !getUserService().isValidCredentials( arguments.username, arguments.password ) ) {
variables.interceptorService.processState(
"onInvalidCredentials",
{
"username": arguments.username,
"password": arguments.password
}
);
throw( type = "InvalidCredentials", message = "Incorrect Credentials Entered" );
}
var user = getUserService().retrieveUserByUsername( arguments.username );
variables.interceptorService.processState(
"postAuthentication",
{
"user" : user,
"username" : arguments.username,
"password" : arguments.password,
"sessionStorage": variables.sessionStorage,
"requestStorage": variables.requestStorage
}
);
return login( user );
}
/**
* Verify if the user is logged in
*/
public boolean function isLoggedIn() {
return variables.sessionStorage.exists( variables.USER_ID_KEY );
}
/**
* Alias to the isLoggedIn function
*/
public boolean function check() {
return isLoggedIn();
}
/**
* Verify if you are NOT logged in, but a guest in the site
*/
public boolean function guest() {
return !isLoggedIn();
}
/**
* Get the currently logged in user object
*
* @throws NoUserLoggedIn : If the user is not logged in
*
* @return User
*/
public any function getUser() {
if ( !variables.requestStorage.exists( variables.USER_KEY ) ) {
try {
var userBean = getUserService().retrieveUserById( getUserId() );
} catch ( any e ) {
// if there was a problem retrieving the user,
// remove the key from the sessionStorage so we
// don't keep trying to log in the user.
variables.sessionStorage.delete( variables.USER_ID_KEY );
rethrow;
}
variables.requestStorage.set( variables.USER_KEY, userBean );
}
return variables.requestStorage.get( variables.USER_KEY );
}
/**
* Alias to `getUser()`
*/
public any function user() {
return getUser();
}
/**
* Get the currently logged in user Id
*
* @throws NoUserLoggedIn
*
* @return The user Id
*/
public any function getUserId() {
if ( !isLoggedIn() ) {
throw( type = "NoUserLoggedIn", message = "No user is currently logged in." );
}
return variables.sessionStorage.get( variables.USER_ID_KEY );
}
/**
* Get the appropriate user service configured by the settings
*
* @throws IncompleteConfiguration
*/
private any function getUserService() {
if ( !structKeyExists( variables, "userService" ) ) {
if ( variables.userServiceClass == "" ) {
throw(
type = "IncompleteConfiguration",
message = "No [userServiceClass] provided. Please set in `config/ColdBox.cfc` under `moduleSettings.cbauth.userServiceClass`."
);
}
variables.userService = variables.wirebox.getInstance( dsl = variables.userServiceClass );
}
return variables.userService;
}
}
How to process rules and annotations
*/
component singleton threadsafe {
property name="cbSecurity" inject="CBSecurity@cbSecurity";
struct function ruleValidator( required rule, required controller ){
return validateSecurity( arguments.rule.permissions );
}
struct function annotationValidator( required securedValue, required controller ){
return validateSecurity( arguments.securedValue );
}
private function validateSecurity( required permissions ){
var results = {
"allow" : false,
"type" : "authentication",
"messages" : ""
};
// Are we logged in?
if ( variables.cbSecurity.getAuthService().isLoggedIn() ) {
// Do we have any permissions?
if ( listLen( arguments.permissions ) ) {
results.allow = variables.cbSecurity.has( arguments.permissions );
results.type = "authorization";
} else {
// We are satisfied!
results.allow = true;
}
}
return results;
}
}
(Not an exhaustive list.)
Traditional username and password, usually with a users table in a database
Authentication using another website's authentication service
Tokens used for applications to talk between each other
Maybe you don't need cbSecurity?
Maybe you just need a `preProcess` interceptor.
Tokens tied to a user that can be used in place of usernames and passwords.
Stateless and signed payload, potentially self-expiring
Via Annotation
component secured {
function index( event, rc, prc ) secured="secret" {
// ...
}
}
component secured {
function index( event, rc, prc ) secured="secret,top-secret" {
// ...
}
}
hasPermission
public boolean function hasPermission( required string permission ) {
return true;
}
Check a list of Permissions
public boolean function hasPermission( required string permission ) {
for ( var p in permission.listToArray() ) {
if ( arrayContains( getPermissions(), p ) ) {
return true;
}
}
return false;
}
Check another fact about the User
public boolean function hasPermission( required string permission ) {
for ( var p in permission.listToArray() ) {
if ( p == "super-admin" && isSuperAdmin() ) {
return true;
}
if ( arrayContains( getPermissions(), p ) ) {
return true;
}
}
return false;
}
Short circuit for certain types of Users
public boolean function hasPermission( required string permission ) {
if ( isAdmin() ) {
return true;
}
for ( var p in permission.listToArray() ) {
if ( arrayContains( getPermissions(), p ) ) {
return true;
}
}
return false;
}
Check token scopes
(Remember, not all authenticatable types are Users)
public boolean function hasPermission( required string permission ) {
for ( var p in permission.listToArray() ) {
if ( arrayContains( getScopes(), p ) ) {
return true;
}
}
return false;
}
cbsecure()
component name="Posts" {
// you need to be logged in to even attempt to edit a Post
function edit( event, rc, prc ) secured {
var post = getInstance( "Post" ).findOrFail( rc.id );
cbsecure().secureWhen( ( user ) => {
return cbsecure().none( "AUTHOR_ADMIN" ) &&
!cbsecure().sameUser( post.getAuthor() );
} );
// business as usual...
}
}
secureView()
component name="Posts" {
function index( event, rc, prc ) {
prc.posts = getInstance( "Post" ).paginate( rc.page, rc.maxrows );
event.secureView( "AUTHOR_ADMIN", "posts/admin/index", "posts/index" );
}
}
Honorable Mention
component name="Registrations" {
function new( event, rc, prc ) {
// Store this in a hidden field in the form
prc.token = csrfGenerate();
event.setView( "registrations/new" );
}
function create( event, rc, prc ) {
// Verify CSFR token from form
if ( !csrfVerify( rc.token ?: '' ) {
redirectBack();
return;
}
// process and save form
}
}
There's also an `automaticTokenVerifier`!