keycloak-auth-utils
Advanced tools
Comparing version 0.0.5 to 0.0.6
@@ -1,2 +0,2 @@ | ||
/* | ||
/*! | ||
* Copyright 2014 Red Hat, Inc. | ||
@@ -22,2 +22,14 @@ * | ||
/** | ||
* Construct a configuration object. | ||
* | ||
* A configuration object may be constructed with either | ||
* a path to a `keycloak.json` file (which defaults to | ||
* `$PWD/keycloak.json` if not present, or with a configuration | ||
* object akin to what parsing `keycloak.json` provides. | ||
* | ||
* @param {String|Object} config Configuration path or details. | ||
* | ||
* @constructor | ||
*/ | ||
function Config(config) { | ||
@@ -35,2 +47,7 @@ if ( ! config ) { | ||
/** | ||
* Load configuration from a path. | ||
* | ||
* @param {String} configPath Path to a `keycloak.json` configuration. | ||
*/ | ||
Config.prototype.loadConfiguration = function(configPath) { | ||
@@ -42,13 +59,71 @@ var json = fs.readFileSync( configPath ); | ||
/** | ||
* Configure this `Config` object. | ||
* | ||
* This will set the internal configuration details. The details | ||
* may come from a `keycloak.json` formatted object (with names such | ||
* as `auth-server-url`) or from an existing `Config` object (using | ||
* names such as `authServerUrl`). | ||
* | ||
* @param {Object} config The configuration to instill. | ||
*/ | ||
Config.prototype.configure = function(config) { | ||
this.authServerUrl = config['auth-server-url'] || config.authServerUrl; | ||
/** | ||
* Realm ID | ||
* @type {String} | ||
*/ | ||
this.realm = config['realm'] || config.realm; | ||
/** | ||
* Client/Application ID | ||
* @type {String} | ||
*/ | ||
this.clientId = config['resource'] || config.clientId; | ||
/** | ||
* Client/Application secret | ||
* @type {String} | ||
*/ | ||
this.secret = (config['credentials'] || {}).secret || config.secret; | ||
/** | ||
* If this is a public application or confidential. | ||
* @type {String} | ||
*/ | ||
this.public = config['public-client'] || config.public || false; | ||
/** | ||
* Authentication server URL | ||
* @type {String} | ||
*/ | ||
this.authServerUrl = config['auth-server-url'] || config.authServerUrl; | ||
/** | ||
* Root realm URL. | ||
* @type {String} | ||
*/ | ||
this.realmUrl = this.authServerUrl + '/realms/' + this.realm; | ||
/** | ||
* Root realm admin URL. | ||
* @type {String} */ | ||
this.realmAdminUrl = this.authServerUrl + '/admin/realms/' + this.realm; | ||
var plainKey = config['realm-public-key']; | ||
/** | ||
* Formatted public-key. | ||
* @type {String} | ||
*/ | ||
this.publicKey = "-----BEGIN PUBLIC KEY-----\n"; | ||
for ( i = 0 ; i < plainKey.length ; i = i + 64 ) { | ||
this.publicKey += plainKey.substring( i, i + 64 ); | ||
this.publicKey += "\n"; | ||
} | ||
this.publicKey += "-----END PUBLIC KEY-----\n"; | ||
}; | ||
module.exports = Config; |
@@ -1,2 +0,2 @@ | ||
/* | ||
/*! | ||
* Copyright 2014 Red Hat, Inc. | ||
@@ -21,17 +21,40 @@ * | ||
var URL = require('url'); | ||
var http = require('http'); | ||
var URL = require('url'); | ||
var http = require('http'); | ||
var crypto = require('crypto'); | ||
var Form = require('./form'); | ||
var Grant = require('./grant'); | ||
var Token = require('./token'); | ||
/** | ||
* Construct a grant manager. | ||
* | ||
* @param {Config} config Config object. | ||
* | ||
* @constructor | ||
*/ | ||
function GrantManager(config) { | ||
this.realmUrl = config.realmUrl; | ||
this.clientId = config.clientId; | ||
this.secret = config.secret; | ||
this.realmUrl = config.realmUrl; | ||
this.clientId = config.clientId; | ||
this.secret = config.secret; | ||
this.publicKey = config.publicKey; | ||
this.notBefore = 0; | ||
} | ||
/** | ||
* Use the direct grant API to obtain a grant from Keycloak. | ||
* | ||
* The direct grant API must be enabled for the configured realm | ||
* for this method to work. This function ostensibly provides a | ||
* non-interactive, programatic way to login to a Keycloak realm. | ||
* | ||
* This method can either accept a callback as the last parameter | ||
* or return a promise. | ||
* | ||
* @param {String} username The username. | ||
* @param {String} password The cleartext password. | ||
* @param {Function} callback Optional callback, if not using promises. | ||
*/ | ||
GrantManager.prototype.obtainDirectly = function(username, password, callback) { | ||
var deferred = Q.defer(); | ||
@@ -50,3 +73,3 @@ | ||
var params = new Form( { | ||
var params = new Form({ | ||
username: username, | ||
@@ -72,6 +95,5 @@ password: password, | ||
try { | ||
var grant = JSON.parse( json ); | ||
deferred.resolve( new Grant( grant ) ); | ||
return deferred.resolve( self.createGrant( json ) ); | ||
} catch (err) { | ||
deferred.reject( err ); | ||
return deferred.reject( err ); | ||
} | ||
@@ -88,8 +110,83 @@ }); | ||
/** | ||
* Obtain a grant from a previous interactive login which results in a code. | ||
* | ||
* This is typically used by servers which receive the code through a | ||
* redirect_uri when sending a user to Keycloak for an interactive login. | ||
* | ||
* An optional session ID and host may be provided if there is desire for | ||
* Keycloak to be aware of this information. They may be used by Keycloak | ||
* when session invalidation is triggered from the Keycloak console itself | ||
* during its postbacks to `/k_logout` on the server. | ||
* | ||
* This method returns or promise or may optionally take a callback function. | ||
* | ||
* @param {String} code The code from a successful login redirected from Keycloak. | ||
* @param {String} sessionId Optional opaque session-id. | ||
* @param {String} sessionHost Optional session host for targetted Keycloak console post-backs. | ||
* @param {Function} callback Optional callback, if not using promises. | ||
*/ | ||
GrantManager.prototype.obtainFromCode = function(code, sessionId, sessionHost, callback) { | ||
var deferred = Q.defer(); | ||
var self = this; | ||
var params = 'code=' + code + '&application_session_state=' + sessionId + '&application_session_host=' + sessionHost; | ||
var options = URL.parse( this.realmUrl + '/tokens/access/codes' ); | ||
options.method = 'POST'; | ||
options.agent = false; | ||
options.headers = { | ||
'Content-Length': params.length, | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'Authorization': 'Basic ' + new Buffer( this.clientId + ':' + this.secret ).toString('base64' ), | ||
}; | ||
var request = http.request( options, function(response) { | ||
var json = ''; | ||
response.on('data', function(d) { | ||
json += d.toString(); | ||
}); | ||
response.on( 'end', function() { | ||
try { | ||
return deferred.resolve( self.createGrant( json ) ); | ||
} catch (err) { | ||
return deferred.reject( err ); | ||
} | ||
}); | ||
} ); | ||
request.write( params ); | ||
request.end(); | ||
return deferred.promise.nodeify( callback ); | ||
}; | ||
/** | ||
* Ensure that a grant is *fresh*, refreshing if required & possible. | ||
* | ||
* If the access_token is not expired, the grant is left untouched. | ||
* | ||
* If the access_token is expired, and a refresh_token is available, | ||
* the grant is refreshed, in place (no new object is created), | ||
* and returned. | ||
* | ||
* If the access_token is expired and no refresh_token is available, | ||
* an error is provided. | ||
* | ||
* The method may either return a promise or take an optional callback. | ||
* | ||
* @param {Grant} grant The grant object to ensure freshness of. | ||
* @param {Function} callback Optional callback if promises are not used. | ||
*/ | ||
GrantManager.prototype.ensureFreshness = function(grant, callback) { | ||
if ( ! grant.expired() ) { | ||
if ( ! grant.isExpired() ) { | ||
return Q(grant).nodeify( callback ); | ||
} | ||
if ( ! grant.refresh_token ) { | ||
return Q.reject( new Error( "Unable to refresh without a refresh token" )).nodeify( callback ); | ||
} | ||
var self = this; | ||
@@ -108,5 +205,5 @@ var deferred = Q.defer(); | ||
var params = new Form( { | ||
var params = new Form({ | ||
grant_type: 'refresh_token', | ||
refresh_token: grant.refresh_token, | ||
refresh_token: grant.refresh_token.token, | ||
}); | ||
@@ -120,5 +217,8 @@ | ||
response.on( 'end', function() { | ||
var data = JSON.parse( json ); | ||
grant.update( data ); | ||
deferred.resolve(grant); | ||
try { | ||
grant.update( self.createGrant( json ) ); | ||
return deferred.resolve(grant); | ||
} catch (err) { | ||
return deferred.reject( err ); | ||
} | ||
}); | ||
@@ -134,2 +234,200 @@ | ||
/** | ||
* Perform live validation of an `access_token` against the Keycloak server. | ||
* | ||
* @param {Token|String} token The token to validate. | ||
* @param {Function} callback Callback function if not using promises. | ||
* | ||
* @return {boolean} `false` if the token is invalid, or the same token if valid. | ||
*/ | ||
GrantManager.prototype.validateAccessToken = function(token, callback) { | ||
var deferred = Q.defer(); | ||
var self = this; | ||
var url = this.realmUrl + '/tokens/validate'; | ||
var options = URL.parse( url ); | ||
options.method = 'GET'; | ||
var t; | ||
if ( typeof token == 'string' ) { | ||
t = token; | ||
} else { | ||
t = token.token; | ||
} | ||
var params = new Form({ | ||
access_token: t, | ||
}); | ||
options.path = options.path + '?' + params.encode(); | ||
var req = http.request( options, function(response) { | ||
var json = ''; | ||
response.on('data', function(d) { | ||
json += d.toString(); | ||
}); | ||
response.on( 'end', function() { | ||
var data = JSON.parse( json ); | ||
if ( data.error ) { | ||
return deferred.resolve( false ); | ||
} | ||
return deferred.resolve( token ); | ||
}); | ||
}); | ||
req.end(); | ||
return deferred.promise.nodeify( callback ); | ||
}; | ||
/** | ||
* Create a `Grant` object from a string of JSON data. | ||
* | ||
* This method creates the `Grant` object, including | ||
* the `access_token`, `refresh_token` and `id_token` | ||
* if available, and validates each for expiration and | ||
* against the known public-key of the server. | ||
* | ||
* @param {String} rawData The raw JSON string received from the Keycloak server or from a client. | ||
* @return {Grant} A validated Grant. | ||
*/ | ||
GrantManager.prototype.createGrant = function(rawData) { | ||
var grantData = JSON.parse( rawData ); | ||
var access_token; | ||
var refresh_token; | ||
var id_token; | ||
if ( grantData.access_token ) { | ||
access_token = new Token( grantData.access_token, this.clientId ); | ||
} | ||
if ( grantData.refresh_token ) { | ||
refresh_token = new Token( grantData.refresh_token ); | ||
} | ||
if ( grantData.id_token ) { | ||
id_token = new Token( grantData.id_token ); | ||
} | ||
var grant = new Grant( { | ||
access_token: access_token, | ||
refresh_token: refresh_token, | ||
id_token: id_token, | ||
expires_in: grantData.expires_in, | ||
token_type: grantData.token_type, | ||
}); | ||
grant.__raw = rawData; | ||
return this.validateGrant( grant ); | ||
}; | ||
/** | ||
* Validate the grant and all tokens contained therein. | ||
* | ||
* This method filters a grant (in place), by nulling out | ||
* any invalid tokens. After this method returns, the | ||
* passed in grant will only contain valid tokens. | ||
* | ||
* @param {Grant} The grant to validate. | ||
*/ | ||
GrantManager.prototype.validateGrant = function(grant) { | ||
grant.access_token = this.validateToken( grant.access_token ); | ||
grant.refresh_token = this.validateToken( grant.refresh_token ); | ||
grant.id_token = this.validateToken( grant.id_token ); | ||
return grant; | ||
}; | ||
/** | ||
* Validate a token. | ||
* | ||
* This method accepts a token, and either returns the | ||
* same token object, if valid, else, it returns `undefined` | ||
* if any of the following errors are seen: | ||
* | ||
* - The token was undefined in the first place. | ||
* - The token is expired. | ||
* - The token is not expired, but issued before the current *not before* timestamp. | ||
* - The token signature does not verify against the known realm public-key. | ||
* | ||
* @return {Token} The same token passed in, or `undefined` | ||
*/ | ||
GrantManager.prototype.validateToken = function(token) { | ||
if ( ! token ) { | ||
return; | ||
} | ||
if ( token.isExpired() ) { | ||
return; | ||
} | ||
if ( token.content.issuedAt < this.notBefore ) { | ||
return; | ||
} | ||
var verify = crypto.createVerify('RSA-SHA256'); | ||
verify.update( token.signed ); | ||
if ( ! verify.verify( this.publicKey, token.signature, 'base64' ) ) { | ||
return; | ||
} | ||
return token; | ||
}; | ||
GrantManager.prototype.getAccount = function(token, callback) { | ||
var deferred = Q.defer(); | ||
var self = this; | ||
var url = this.realmUrl + '/account'; | ||
var options = URL.parse( url ); | ||
options.method = 'GET'; | ||
var t; | ||
if ( typeof token == 'string' ) { | ||
t = token; | ||
} else { | ||
t = token.token; | ||
} | ||
options.headers = { | ||
'Authorization': 'Bearer ' + t, | ||
'Accept': 'application/json', | ||
}; | ||
var req = http.request( options, function(response) { | ||
console.log( "RESPONSE", response.statusCode ); | ||
if ( response.statusCode < 200 || response.statusCode >= 300 ) { | ||
return deferred.reject( "Error fetching account" ); | ||
} | ||
var json = ''; | ||
response.on('data', function(d) { | ||
json += d.toString(); | ||
}); | ||
response.on( 'end', function() { | ||
var data = JSON.parse( json ); | ||
if ( data.error ) { | ||
return deferred.reject( data ); | ||
} | ||
console.log( "ACCOUNT", data ); | ||
return deferred.resolve( data ); | ||
}); | ||
}); | ||
req.end(); | ||
return deferred.promise.nodeify( callback ); | ||
}; | ||
module.exports = GrantManager; |
63
grant.js
@@ -1,2 +0,2 @@ | ||
/* | ||
/*! | ||
* Copyright 2014 Red Hat, Inc. | ||
@@ -17,9 +17,18 @@ * | ||
var Form = require('./form'); | ||
var Q = require('q'); | ||
var http = require('http'); | ||
var URL = require('url'); | ||
/** | ||
* Construct a new grant. | ||
* | ||
* The passed in argument may be another `Grant`, or any object with | ||
* at least `access_token`, and optionally `refresh_token` and `id_token`, | ||
* `token_type`, and `expires_in`. Each token should be an instance of | ||
* `Token` if present. | ||
* | ||
* If the passed in object contains a field named `__raw` that is also stashed | ||
* away as the verbatim raw `String` data of the grant. | ||
* | ||
* @param {Object} grant The `Grant` to copy, or a simple `Object` with similar fields. | ||
* | ||
* @constructor | ||
*/ | ||
function Grant(grant) { | ||
@@ -29,6 +38,8 @@ this.update( grant ); | ||
Grant.prototype.dump = function(token) { | ||
console.log( JSON.parse( new Buffer( token.split('.')[1], 'base64' ) ) ); | ||
}; | ||
/** | ||
* Update this grant in-place given data in another grant. | ||
* | ||
* This is used to avoid making client perform extra-bookkeeping | ||
* to maintain the up-to-date/refreshed grant-set. | ||
*/ | ||
Grant.prototype.update = function(grant) { | ||
@@ -38,13 +49,39 @@ // intentional naming with under_scores instead of | ||
// and to allow new Grant(new Grant(kc)) copy-ctor | ||
this.access_token = grant.access_token; | ||
this.refresh_token = grant.refresh_token; | ||
this.id_token = grant.id_token; | ||
this.token_type = grant.token_type; | ||
this.expires_in = grant.expires_in; | ||
this.__raw = grant.__raw; | ||
}; | ||
Grant.prototype.expired = function() { | ||
return true; | ||
/** | ||
* Returns the raw String of the grant, if available. | ||
* | ||
* If the raw string is unavailable (due to programatic construction) | ||
* then `undefined` is returned. | ||
*/ | ||
Grant.prototype.toString = function() { | ||
return this.__raw; | ||
}; | ||
/** | ||
* Determine if this grant is expired/out-of-date. | ||
* | ||
* Determination is made based upon the expiration status of the `access_token`. | ||
* | ||
* An expired grant *may* be possible to refresh, if a valid | ||
* `refresh_token` is available. | ||
* | ||
* @return {boolean} `true` if expired, otherwise `false`. | ||
*/ | ||
Grant.prototype.isExpired = function() { | ||
if ( ! this.access_token ) { | ||
return true; | ||
} | ||
return this.access_token.isExpired(); | ||
}; | ||
module.exports = Grant; |
@@ -7,13 +7,23 @@ | ||
pkg: grunt.file.readJSON('package.json'), | ||
docco: { | ||
debug: { | ||
src: ['index.js', 'token-refresher.js', 'grant.js'], | ||
doxx: { | ||
all: { | ||
src: '.', | ||
target: 'doc', | ||
options: { | ||
output: 'doc/', | ||
layout: 'classic' | ||
ignore: 'Gruntfile.js,form.js,spec,node_modules,.git', | ||
} | ||
} | ||
}, | ||
touch: { | ||
src: [ 'doc/.nojekyll' ] | ||
}, | ||
jshint: { | ||
all: ['Gruntfile.js', '*.js', 'test/**/*.js'] | ||
}, | ||
'gh-pages': { | ||
options: { | ||
base: 'doc', | ||
dotfiles: true, | ||
}, | ||
src: ['**'] | ||
} | ||
@@ -23,8 +33,10 @@ }); | ||
grunt.loadNpmTasks('grunt-contrib-jshint'); | ||
grunt.loadNpmTasks('grunt-docco'); | ||
grunt.loadNpmTasks('grunt-doxx'); | ||
grunt.loadNpmTasks('grunt-gh-pages'); | ||
grunt.loadNpmTasks('grunt-touch'); | ||
// Default task(s). | ||
grunt.registerTask('default', ['jshint', 'docco']); | ||
grunt.registerTask('default', ['jshint', 'doxx', 'touch']); | ||
}; | ||
@@ -1,2 +0,2 @@ | ||
/* | ||
/*! | ||
* Copyright 2014 Red Hat, Inc. | ||
@@ -18,3 +18,4 @@ * | ||
/** Export all useful things. | ||
*/ | ||
module.exports = { | ||
@@ -24,4 +25,3 @@ Config: require('./config'), | ||
Grant: require('./grant' ), | ||
Form: require('./form'), | ||
}; | ||
{ | ||
"name": "keycloak-auth-utils", | ||
"version": "0.0.5", | ||
"version": "0.0.6", | ||
"description": "General Keycloak Utilities", | ||
@@ -13,3 +13,2 @@ "main": "index.js", | ||
"devDependencies": { | ||
"jasmine": "^2.1.1", | ||
"grunt": "^0.4.5", | ||
@@ -19,3 +18,7 @@ "grunt-cli": "^0.1.13", | ||
"grunt-contrib-uglify": "^0.6.0", | ||
"grunt-docco": "^0.3.3" | ||
"grunt-docco": "^0.3.3", | ||
"grunt-doxx": "^0.1.2", | ||
"grunt-gh-pages": "^0.9.1", | ||
"grunt-touch": "^0.1.0", | ||
"jasmine": "^2.1.1" | ||
}, | ||
@@ -27,5 +30,5 @@ "dependencies": { | ||
"type": "git", | ||
"url": "http://github.com/bobmcwhirter/keycloak-auth-utils.git" | ||
"url": "http://github.com/keycloak/keycloak-nodejs.git" | ||
}, | ||
"bugs": "http://github.com/bobmcwhirter/keycloak-auth-utils/issues" | ||
"bugs": "http://github.com/keycloak/keycloak-nodejs/issues" | ||
} |
@@ -8,4 +8,5 @@ | ||
* Can obtain a grant through the direct API | ||
* Can renew any token using a `refresh_token` | ||
* Can obtain a grant through the direct API using name/password. | ||
* Can renew any token using a `refresh_token`. | ||
* Validates grants/tokens. | ||
@@ -16,1 +17,10 @@ ## `Grant` | ||
## `Token` | ||
Embodies JSON Web Token bits and checking for roles. | ||
# Resources | ||
* [GitHub](http://github.com/bobmcwhirter/keycloak-auth-utils) | ||
* [Documentation](http://bobmcwhirter.github.io/keycloak-auth-utils) | ||
@@ -38,3 +38,3 @@ | ||
expect( grant.access_token ).not.toBe( undefined ); | ||
expect( grant.access_token ).not.toBe( originalAccessToken ); | ||
expect( grant.access_token.token ).not.toBe( originalAccessToken.token ); | ||
}) | ||
@@ -44,2 +44,59 @@ .done( done ); | ||
it( 'should be able to validate a valid token', function(done) { | ||
var originalAccessToken; | ||
manager.obtainDirectly( 'lucy', 'lucy' ) | ||
.then( function(grant) { | ||
originalAccessToken = grant.access_token; | ||
return manager.validateAccessToken( grant.access_token ) | ||
}) | ||
.then( function(token) { | ||
expect( token ).not.toBe( undefined ); | ||
expect( token ).toBe( originalAccessToken ); | ||
}) | ||
.done( done ); | ||
}) | ||
it( 'should be able to validate an invalid token', function(done) { | ||
var originalAccessToken; | ||
manager.obtainDirectly( 'lucy', 'lucy' ) | ||
.delay(3000) | ||
.then( function(grant) { | ||
originalAccessToken = grant.access_token; | ||
return manager.validateAccessToken( grant.access_token ) | ||
}) | ||
.then( function(result) { | ||
expect( result ).toBe( false ); | ||
}) | ||
.done( done ); | ||
}) | ||
it( 'should be able to validate a valid token string', function(done) { | ||
var originalAccessToken; | ||
manager.obtainDirectly( 'lucy', 'lucy' ) | ||
.then( function(grant) { | ||
originalAccessToken = grant.access_token.token; | ||
return manager.validateAccessToken( grant.access_token.token ) | ||
}) | ||
.then( function(token) { | ||
expect( token ).not.toBe( undefined ); | ||
expect( token ).toBe( originalAccessToken ); | ||
}) | ||
.done( done ); | ||
}) | ||
it( 'should be able to validate an invalid token string', function(done) { | ||
var originalAccessToken; | ||
manager.obtainDirectly( 'lucy', 'lucy' ) | ||
.delay(3000) | ||
.then( function(grant) { | ||
originalAccessToken = grant.access_token.token; | ||
return manager.validateAccessToken( grant.access_token.token ) | ||
}) | ||
.then( function(result) { | ||
expect( result ).toBe( false ); | ||
}) | ||
.done( done ); | ||
}) | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
311990
39
861
25
9