cbsecurity

ColdBox Authentication and Authorization

Follow Along:

What This Talk Is

  • Overview of Authentication and Authorization
  • Why cbSecurity?
  • Examples, examples, examples
    • MVC Applications
    • LDAP Authentication
    • Token Authentication
    • JSON Web Tokens (JWT)
    • Single Page App (SPA) Authentication
    • Hybrid Authentication
    • Authorization Checks
    • And probably more...

Who Am I?

Utah

Ortus Solutions

qb, Quick, Hyper, lots of other modules

1 wife, 3 kids, 1 dog

Type 1 Diabetic

Theatre Nerd

WARNING!!

GIFs ahead

Why CBSecurity?

Why CBSecurity?

  • Consistent API regardless of authentication method
  • Protects routes and events (Authorization)

definitions

Authentication

who Am I?

AUTHORIZATION

WHAT AM I ALLOWED TO DO?

User Service

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

}

Authentication service

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

}

cbauth

/**
 * 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;
	}

}

VAlidators

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

}

Authentication Types

(Not an exhaustive list.)

MVC Applications

Traditional username and password, usually with a users table in a database

  • Make sure to hash the password

OAuth

Authentication using another website's authentication service

Client Tokens

Tokens used for applications to talk between each other

  • Make sure to hash the token

Client Tokens

Maybe you don't need cbSecurity?

Maybe you just need a `preProcess` interceptor.

Personal Access Tokens

Tokens tied to a user that can be used in place of usernames and passwords.

  • Make sure to hash the token

JSON Web Tokens

(JWT)

Stateless and signed payload, potentially self-expiring

  • Token is public a decodable by anyone. It is NOT encrypted. Don't store sensitive data.
  • JWT are validatable without the server.
  • At the end of the day, it's still a token and a token-based auth flow.

Authorization

Authorization

Via Annotation

component secured {
  
  
   function index( event, rc, prc ) secured="secret" {
       // ...
   }
  
  
}
component secured {
  
  
   function index( event, rc, prc ) secured="secret,top-secret" {
       // ...
   }
  
  
}

Authorization

hasPermission

public boolean function hasPermission( required string permission ) {
	return true;
}

Authorization

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

Authorization

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

Authorization

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

Authorization

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

Authorization

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...
    }
  
}

Authorization

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

CSRF

Honorable Mention

CSRF

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

CSRF

There's also an `automaticTokenVerifier`!

cbSecurity Demo Gallery

Thanks!