Skip to content

Commit bd12482

Browse files
committed
Merge branch 'development'
2 parents 8d744ce + 40630ce commit bd12482

File tree

8 files changed

+123
-37
lines changed

8 files changed

+123
-37
lines changed

.markdownlint.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"line-length": false,
3+
"single-h1": false,
4+
"no-hard-tabs" : false,
5+
"fenced-code-language" : false,
6+
"no-bare-urls" : false,
7+
"first-line-h1": false,
8+
"no-multiple-blanks": {
9+
"maximum": 2
10+
},
11+
"no-duplicate-header" : {
12+
"siblings_only" : true
13+
}
14+
}

ModuleConfig.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ component {
119119
"cbSecurity_onJWTInvalidClaims",
120120
"cbSecurity_onJWTExpiration",
121121
"cbSecurity_onJWTStorageRejection",
122-
"cbSecurity_onJWTValidParsing"
122+
"cbSecurity_onJWTValidParsing",
123+
"cbSecurity_onJWTInvalidateAllTokens"
123124
]
124125
};
125126

box.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name":"ColdBox Security",
3-
"version":"2.7.0",
3+
"version":"2.8.0",
44
"location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cbsecurity/@build.version@/[email protected]@.zip",
55
"author":"Ortus Solutions.com <[email protected]>",
66
"slug":"cbsecurity",

changelog.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
----
99

10+
## [2.8.0] => 2020-NOV-09
11+
12+
### Added
13+
14+
* `parseToken( token )` now accepts a token of your choice to work with in the request or it will continue to discover it if not passed.
15+
* Added new JWT Service method: `invalidateAll()` which invalidates all tokens in the token storage
16+
* Added the new event: `cbSecurity_onJWTInvalidateAllTokens` that fires once all tokens in the storage are cleared
17+
* Added storage of the authenticated user into the `prc` scope when using `attempt()` to be consistent with API calls
18+
19+
### Fixed
20+
21+
* Spelling corrections on the readme
22+
* Added full var scoping for `cbsecurity` in JWTService calls
23+
24+
----
25+
1026
## [2.7.0] => 2020-SEP-14
1127

1228
### Added

models/jwt/JwtService.cfc

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,18 @@ component accessors="true" singleton {
136136
required password,
137137
struct customClaims = {}
138138
){
139-
var oUser = cbSecurity
139+
// Authenticate via the auth service wired up
140+
// If it fails an exception is thrown
141+
var oUser = variables.cbSecurity
140142
.getAuthService()
141143
.authenticate( arguments.username, arguments.password );
142144

143-
// Create it
145+
// Store User in ColdBox data bus
146+
variables.requestService
147+
.getContext()
148+
.setPrivateValue( variables.settings.prcUserVariable, oUser );
149+
150+
// Create the token and return it
144151
return fromUser( oUser, arguments.customClaims );
145152
}
146153

@@ -152,14 +159,14 @@ component accessors="true" singleton {
152159
*/
153160
function logout(){
154161
invalidate( this.getToken() );
155-
cbSecurity.getAuthService().logout();
162+
variables.cbSecurity.getAuthService().logout();
156163
}
157164

158165
/**
159166
* Shortcut function to our authentication services to check if we are logged in
160167
*/
161168
boolean function isLoggedIn(){
162-
return cbSecurity.getAuthService().isLoggedIn();
169+
return variables.cbSecurity.getAuthService().isLoggedIn();
163170
}
164171

165172
/**
@@ -306,6 +313,29 @@ component accessors="true" singleton {
306313
return results;
307314
}
308315

316+
/**
317+
* Invalidates all tokens in the connected storage provider
318+
*
319+
* @async Run the clearing asynchronously or not, default is false
320+
*/
321+
JwtService function invalidateAll( boolean async=false ){
322+
if ( variables.log.canInfo() ) {
323+
variables.log.info( "Token invalidation request issued for all tokens" );
324+
}
325+
326+
// Clear all via storage
327+
getTokenStorage().clearAll( arguments.async );
328+
329+
// Announce the token invalidation
330+
variables.interceptorService.processState( "cbSecurity_onJWTInvalidateAllTokens" );
331+
332+
if ( variables.log.canInfo() ) {
333+
variables.log.info( "All tokens cleared via token storage clear all" );
334+
}
335+
336+
return this;
337+
}
338+
309339
/**
310340
* Verifies if the passed in token exists in the storage provider
311341
*
@@ -321,33 +351,34 @@ component accessors="true" singleton {
321351

322352
/**
323353
* Try's to get a jwt token from the authorization header or the custom header
324-
* defined in the configuration. If it is a valid token and it decodes we will then
325-
* continue to validat the subject it represents. Once those are satisfied, then it will
354+
* defined in the configuration or passed in by you. If it is a valid token and it decodes we will then
355+
* continue to validate the subject it represents. Once those are satisfied, then it will
326356
* store it in the `prc` as `prc.jwt_token` and the payload as `prc.jwt_payload`.
327357
*
358+
* @token The token to parse and validate, if not passed we call the discoverToken() method for you.
359+
*
328360
* @throws TokenExpiredException If the token has expired or no longer in the storage (invalidated)
329361
* @throws TokenInvalidException If the token doesn't verify decoding
330362
* @throws TokenNotFoundException If the token cannot be found in the headers
331363
*
332364
* @returns The payload for convenience
333365
*/
334-
struct function parseToken(){
335-
var jwtToken = discoverToken();
366+
struct function parseToken( string token = discoverToken() ){
336367

337368
// Did we find an incoming token
338-
if ( !len( jwtToken ) ) {
369+
if ( !len( arguments.token ) ) {
339370
if ( variables.log.canDebug() ) {
340-
variables.log.debug( "Token not found anywhere" );
371+
variables.log.debug( "Token empty or not found anywhere (headers, url, form)" );
341372
}
342373

343374
throw(
344-
message = "Token not found in authorization header or the custom header or the request collection",
375+
message = "Token not found in authorization header or the custom header or the request collection or not passed in",
345376
type = "TokenNotFoundException"
346377
);
347378
}
348379

349380
// Decode it
350-
var decodedToken = decode( jwtToken );
381+
var decodedToken = decode( arguments.token );
351382
var decodedClaims = decodedToken.keyArray();
352383

353384
// Verify the required claims
@@ -368,7 +399,7 @@ component accessors="true" singleton {
368399
variables.interceptorService.processState(
369400
"cbSecurity_onJWTInvalidClaims",
370401
{
371-
token : jwtToken,
402+
token : arguments.token,
372403
payload : decodedToken
373404
}
374405
);
@@ -391,7 +422,7 @@ component accessors="true" singleton {
391422
variables.interceptorService.processState(
392423
"cbSecurity_onJWTExpiration",
393424
{
394-
token : jwtToken,
425+
token : arguments.token,
395426
payload : decodedToken
396427
}
397428
);
@@ -409,7 +440,7 @@ component accessors="true" singleton {
409440
variables.interceptorService.processState(
410441
"cbSecurity_onJWTStorageRejection",
411442
{
412-
token : jwtToken,
443+
token : arguments.token,
413444
payload : decodedToken
414445
}
415446
);
@@ -429,22 +460,22 @@ component accessors="true" singleton {
429460
);
430461
}
431462

432-
// Store it
463+
// Store it on the PRC scope values
433464
variables.requestService
434465
.getContext()
435-
.setPrivateValue( "jwt_token", jwtToken )
466+
.setPrivateValue( "jwt_token", arguments.token )
436467
.setPrivateValue( "jwt_payload", decodedToken );
437468

438469
// Announce the valid parsing
439470
variables.interceptorService.processState(
440471
"cbSecurity_onJWTValidParsing",
441472
{
442-
token : jwtToken,
473+
token : arguments.token,
443474
payload : decodedToken
444475
}
445476
);
446477

447-
// Authenticate the payload
478+
// Authenticate the payload, because a token MUST be valid before usage
448479
authenticate();
449480

450481
// Return it
@@ -662,7 +693,12 @@ component accessors="true" singleton {
662693
/****************************** PRIVATE ******************************/
663694

664695
/**
665-
* Try to discover the jwt token from many incoming resources
696+
* Try to discover the jwt token from many incoming resources:
697+
* - The custom auth header: x-auth-token
698+
* - URL/FORM: x-auth-token
699+
* - Authorization Header
700+
*
701+
* @return The discovered token or an empty string
666702
*/
667703
private string function discoverToken(){
668704
var event = variables.requestService.getContext();

readme.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ cbauth = {
5151
cbsecurity = {
5252
// The global invalid authentication event or URI or URL to go if an invalid authentication occurs
5353
"invalidAuthenticationEvent" : "",
54-
// Default Auhtentication Action: override or redirect when a user has not logged in
54+
// Default Authentication Action: override or redirect when a user has not logged in
5555
"defaultAuthenticationAction" : "redirect",
5656
// The global invalid authorization event or URI or URL to go if an invalid authorization occurs
5757
"invalidAuthorizationEvent" : "",
@@ -134,7 +134,7 @@ Using the default configuration, this module will register the `Security` interc
134134
135135
The interceptor will intercept all calls to your application via the `preProcess()` interception point. Each request will then be validated against any registered security rules and then validated against any handler/action security annotations (if active) via a Security Validator. Also, if the request is made to a module, each module has the capability to have it's own separate validator apart from the global one.
136136

137-
> **Info** You can deactive annotation driven security via the `handlerAnnotationSecurity` setting.
137+
> **Info** You can deactivate annotation driven security via the `handlerAnnotationSecurity` setting.
138138
139139
### How does validation happen?
140140

@@ -327,7 +327,7 @@ component{
327327

328328
### Authorization Context
329329

330-
You can also give the annotation some value, which can be anything you like: A list of roles, a role, a list of permissions, metadata, etc. Whatever it is, this is the **authorization context** and the security validator must be able to not only authenticate but authorize the context or an invalid authorization will occurr.
330+
You can also give the annotation some value, which can be anything you like: A list of roles, a role, a list of permissions, metadata, etc. Whatever it is, this is the **authorization context** and the security validator must be able to not only authenticate but authorize the context or an invalid authorization will occur.
331331

332332
```js
333333
// Secure this handler
@@ -417,7 +417,7 @@ private function permissionValidator( permissions, controller, rule ){
417417

418418
## Interceptions
419419

420-
When invalid access or authorizations occurr the interceptor will announce the following events:
420+
When invalid access or authorizations occur the interceptor will announce the following events:
421421

422422
- `cbSecurity_onInvalidAuthentication`
423423
- `cbSecurity_onInvalidAuthorization`

test-harness/tests/Application.cfc

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ component {
3232

3333
// request start
3434
public boolean function onRequestStart( String targetPage ){
35-
return true;
36-
}
3735

38-
function onRequestEnd(){
39-
structDelete( application, "wirebox" );
36+
// Cleanup
37+
if( !isNull( application.cbController ) ){
38+
application.cbController.getLoaderService().processShutdown();
39+
}
4040
structDelete( application, "cbController" );
41+
structDelete( application, "wirebox" );
42+
43+
return true;
4144
}
4245

43-
}
46+
}

test-harness/tests/specs/integration/JWTSpec.cfc

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
2020
beforeEach( function(currentSpec){
2121
// Setup as a new ColdBox request for this suite, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST.
2222
setup();
23+
// Get the Service
24+
variables.jwtService = getInstance( "jwtService@cbSecurity" );
2325
} );
2426

2527
given( "no jwt token and accessing a secure api call", function(){
@@ -87,11 +89,11 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
8789
} );
8890
} );
8991

90-
given( "an valid jwt token but it is not in the storage", function(){
92+
given( "a valid jwt token but it is not in the storage", function(){
9193
then( "it should block with no authorization", function(){
92-
var thisToken = getInstance( "jwtService@cbSecurity" ).attempt( "test", "test" );
94+
var thisToken = variables.jwtService.attempt( "test", "test" );
9395

94-
getInstance( "jwtService@cbSecurity" )
96+
variables.jwtService
9597
.getTokenStorage()
9698
.clearAll();
9799
getRequestContext().setValue( "x-auth-token", thisToken );
@@ -110,7 +112,7 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
110112
var thisToken = getInvalidUserToken();
111113
getRequestContext().setValue( "x-auth-token", thisToken.token );
112114

113-
getInstance( "jwtService@cbSecurity" )
115+
variables.jwtService
114116
.getTokenStorage()
115117
.set(
116118
key = thisToken.payload.jti,
@@ -130,20 +132,34 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
130132

131133
given( "a valid jwt token in all senses", function(){
132134
then( "it should allow the call", function(){
133-
var thisToken = getInstance( "jwtService@cbSecurity" ).attempt( "test", "test" );
135+
var thisToken = variables.jwtService.attempt( "test", "test" );
134136
getRequestContext().setValue( "x-auth-token", thisToken );
135137

136138
var event = execute( route = "/api/secure", renderResults = true );
137139
expect( event.getCurrentEvent() ).toBe( "api:secure.index" );
138140
} );
139141
} );
142+
143+
144+
story( "I want to invalidate all tokens in the storage", function(){
145+
given( "a valid jwt token and a invalidate all is issued", function(){
146+
then( "the storage should be empty", function(){
147+
var thisToken = variables.jwtService.attempt( "test", "test" );
148+
expect( variables.jwtService.getTokenStorage().size() ).toBeGT( 0 );
149+
150+
variables.jwtService.invalidateAll();
151+
expect( variables.jwtService.getTokenStorage().size() ).toBe( 0 );
152+
} );
153+
} );
154+
} );
155+
140156
} );
141157
}
142158

143159
private function getInvalidUserToken(){
144160
var timestamp = now();
145161
var userId = 123;
146-
var service = getInstance( "jwtService@cbsecurity" );
162+
var service = variables.jwtService;
147163
var payload = {
148164
// Issuing authority
149165
"iss" : service.getSettings().jwt.issuer,

0 commit comments

Comments
 (0)