Skip to content
This repository was archived by the owner on Aug 7, 2021. It is now read-only.

Commit 4a697cb

Browse files
committed
Get basic cert auth happy path implemented.
1 parent 52d47f6 commit 4a697cb

7 files changed

+114
-32
lines changed

lib/authentication-context.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,15 @@ AuthenticationContext.prototype.acquireTokenWithRefreshToken = function(refreshT
335335
});
336336
};
337337

338-
339-
AuthenticationContext.prototype.acquireTokenWithCertificate = function(resource, clientId, certificate, callback) {
338+
/**
339+
* Gets a new access token using via a certificate credential.
340+
* @param {string} resource A URI that identifies the resource for which the token is valid.
341+
* @param {string} clientId The OAuth client id of the calling application.
342+
* @param {string} certificate A PEM encoded certificate private key.
343+
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
344+
* @param {AcquireTokenCallback} callback The callback function.
345+
*/
346+
AuthenticationContext.prototype.acquireTokenWithCertificate = function(resource, clientId, certificate, thumbprint, callback) {
340347
argument.validateCallbackType(callback);
341348
try {
342349
argument.validateStringParameter(resource, 'resource');
@@ -348,7 +355,7 @@ AuthenticationContext.prototype.acquireTokenWithCertificate = function(resource,
348355

349356
this._acquireToken(callback, function() {
350357
var tokenRequest = new TokenRequest(this._callContext, this, clientId, resource);
351-
tokenRequest.getTokenWithCertificate(certificate);
358+
tokenRequest.getTokenWithCertificate(certificate, thumbprint, callback);
352359
});
353360
};
354361

lib/constants.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ var Constants = {
2424
OAuth2 : {
2525
Parameters : {
2626
GRANT_TYPE : 'grant_type',
27+
CLIENT_ASSERTION : 'client_assertion',
28+
CLIENT_ASSERTION_TYPE : 'client_assertion_type',
2729
CLIENT_ID : 'client_id',
2830
CLIENT_SECRET : 'client_secret',
2931
REDIRECT_URI : 'redirect_uri',
@@ -41,6 +43,7 @@ var Constants = {
4143
AUTHORIZATION_CODE : 'authorization_code',
4244
REFRESH_TOKEN : 'refresh_token',
4345
CLIENT_CREDENTIALS : 'client_credentials',
46+
JWT_BEARER : 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
4447
PASSWORD : 'password',
4548
SAML1 : 'urn:ietf:params:oauth:grant-type:saml1_1-bearer',
4649
SAML2 : 'urn:ietf:params:oauth:grant-type:saml2-bearer'
@@ -101,12 +104,12 @@ var Constants = {
101104

102105
Jwt : {
103106
SELF_SIGNED_JWT_LIFETIME : 10, // 10 mins in mins
104-
ACTOR_TOKEN : 'actortoken',
105107
AUDIENCE : 'aud',
106108
ISSUER : 'iss',
107109
SUBJECT : 'sub',
108110
NOT_BEFORE : 'nbf',
109-
EXPIRES_ON : 'exp'
111+
EXPIRES_ON : 'exp',
112+
JWT_ID : 'jti'
110113
},
111114

112115
AADConstants : {

lib/self-signed-jwt.js

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,54 +24,96 @@ var jwtConstants = require('./constants').Jwt;
2424
var Logger = require('./log').Logger;
2525
var util = require('./util');
2626

27-
var crypto = require('crypto');
2827
require('date-utils');
28+
var jws = require('jws');
2929
var uuid = require('node-uuid');
3030

31+
/**
32+
* JavaScript dates are in milliseconds, but JWT dates are in seconds.
33+
* This function does the conversion.
34+
* @param {Date} date
35+
* @return {string}
36+
*/
37+
function dateGetTimeInSeconds(date) {
38+
return Math.floor(date.getTime()/1000);
39+
}
40+
41+
/**
42+
* Constructs a new SelfSignedJwt object.
43+
* @param {object} callContext Context specific to this token request.
44+
* @param {Authority} authority The authority to be used as the JWT audience.
45+
* @param {string} clientId The client id of the calling app.
46+
*/
3147
function SelfSignedJwt(callContext, authority, clientId) {
3248
this._log = new Logger('SelfSignedJwt', callContext._logContext);
3349
this._callContext = callContext;
3450

51+
this._authority = authority;
3552
this._tokenEndpoint = authority.tokenEndpoint;
3653
this._clientId = clientId;
3754
}
55+
/**
56+
* A regular certificate thumbprint is a hex encode string of the binary certificate
57+
* hash. For some reason teh x5t value in a JWT is a url save base64 encoded string
58+
* instead. This function does the conversion.
59+
* @param {string} thumbprint A hex encoded certificate thumbprint.
60+
* @return {string} A url safe base64 encoded certificate thumbprint.
61+
*/
62+
SelfSignedJwt.prototype._createx5tValue = function(thumbprint) {
63+
var hexString = thumbprint.replace(/:/g, '').replace(/ /g, '');
64+
var base64 = (new Buffer(hexString, 'hex')).toString('base64');
65+
return util.convertRegularToUrlSafeBase64EncodedString(base64);
66+
};
3867

68+
/**
69+
* Creates the JWT header.
70+
* @param {string} thumbprint A hex encoded certificate thumbprint.
71+
* @return {object}
72+
*/
3973
SelfSignedJwt.prototype._createHeader = function(thumbprint) {
40-
var header = { typ: 'JWT', alg: 'RS256', x5t : thumbprint };
74+
var x5t = this.createx5tValue(thumbprint);
75+
var header = { typ: 'JWT', alg: 'RS256', x5t : x5t };
4176

42-
this._log.verbose('Creating self signed JWT header. Thumbprint: ' + thumbprint);
77+
this._log.verbose('Creating self signed JWT header. x5t: ' + x5t);
4378

44-
return header;
79+
return header;
4580
};
4681

82+
/**
83+
* Creates the JWT payload.
84+
* @return {object}
85+
*/
4786
SelfSignedJwt.prototype._createPayload = function() {
4887
var now = new Date();
4988
var expires = (new Date()).addMinutes(jwtConstants.SELF_SIGNED_JWT_LIFETIME);
5089

5190
this._log.verbose('Creating self signed JWT payload. Expires: ' + expires + ' NotBefore: ' + now);
5291

5392
var jwtPayload = {};
54-
jwtPayload[jwtConstants.AUDIENCE] = this._authority;
93+
jwtPayload[jwtConstants.AUDIENCE] = this._tokenEndpoint;
5594
jwtPayload[jwtConstants.ISSUER] = this._clientId;
5695
jwtPayload[jwtConstants.SUBJECT] = this._clientId;
57-
jwtPayload[jwtConstants.NOT_BEFORE] = now.getTime();
58-
jwtPayload[jwtConstants.EXPIRES_ON] = expires.getTime();
96+
jwtPayload[jwtConstants.NOT_BEFORE] = dateGetTimeInSeconds(now);
97+
jwtPayload[jwtConstants.EXPIRES_ON] = dateGetTimeInSeconds(expires);
5998
jwtPayload[jwtConstants.JWT_ID] = uuid.v4();
6099

61100
return jwtPayload;
62101
};
63102

103+
/**
104+
* Creates a self signed JWT that can be used as a client_assertion.
105+
* @param {string} certificate A PEM encoded certificate private key.
106+
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
107+
* @return {string} A self signed JWT token.
108+
*/
64109
SelfSignedJwt.prototype.create = function(certificate, thumbprint) {
65110
var header = this._createHeader(thumbprint);
66-
var payload = this._createPayload();
67111

68-
var headerString = util.base64EncodeStringUrlSafe(JSON.stringify(header));
69-
var payloadString = util.base64EncodeStringUrlSafe(JSON.stringify(payload));
70-
var stringToSign = headerString + '.' + payloadString;
112+
var payload = this._createPayload();
71113

72-
var signature = util.base64EncodeStringUrlSafe(crypto.createSign('RSA-SHA256').update(stringToSign).sign(certificate, 'base64'));
114+
var jwt = jws.sign({ header : header, payload : payload, secret : certificate});
73115

74-
return stringToSign + '.' + signature;
116+
return jwt;
75117
};
76118

77119
module.exports.SelfSignedJwt = SelfSignedJwt;

lib/token-request.js

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var CacheDriver = require('./cache-driver');
2525
var Logger = require('./log').Logger;
2626
var Mex = require('./mex');
2727
var OAuth2Client = require('./oauth2client');
28+
var SelfSignedJwt = require('./self-signed-jwt').SelfSignedJwt;
2829
var UserRealm = require('./user-realm');
2930
var WSTrustRequest = require('./wstrust-request');
3031

@@ -118,7 +119,8 @@ TokenRequest.prototype._createCacheQuery = function() {
118119
return query;
119120
};
120121

121-
TokenRequest.prototype._getToken = function(callback, getTokenFunc) {
122+
123+
TokenRequest.prototype._getTokenWithCacheWrapper = function(callback, getTokenFunc) {
122124
var self = this;
123125
this._cacheDriver = this._createCacheDriver();
124126
var cacheQuery = this._createCacheQuery();
@@ -353,24 +355,24 @@ TokenRequest.prototype.getTokenWithUsernamePassword = function(username, passwor
353355
this._log.info('Acquiring token with username password');
354356

355357
this._userId = username;
356-
this._getToken(callback, function(innerCallback) {
358+
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
357359
var self = this;
358360
this._userRealm = this._createUserRealmRequest(username);
359361
this._userRealm.discover(function(err) {
360362
if (err) {
361-
innerCallback(err);
363+
getTokenCompleteCallback(err);
362364
return;
363365
}
364366

365367
switch(self._userRealm.accountType) {
366368
case AccountType.Managed:
367-
self._getTokenUsernamePasswordManaged(username, password, innerCallback);
369+
self._getTokenUsernamePasswordManaged(username, password, getTokenCompleteCallback);
368370
return;
369371
case AccountType.Federated:
370-
self._getTokenUsernamePasswordFederated(username, password, innerCallback);
372+
self._getTokenUsernamePasswordFederated(username, password, getTokenCompleteCallback);
371373
return;
372374
default:
373-
innerCallback(self._log.createError('Server returned an unknown AccountType: ' + self._userRealm.AccountType));
375+
getTokenCompleteCallback(self._log.createError('Server returned an unknown AccountType: ' + self._userRealm.AccountType));
374376
}
375377
});
376378
});
@@ -385,12 +387,12 @@ TokenRequest.prototype.getTokenWithUsernamePassword = function(username, passwor
385387
TokenRequest.prototype.getTokenWithClientCredentials = function(clientSecret, callback) {
386388
this._log.info('Getting token with client credentials.');
387389

388-
this._getToken(callback, function(innerCallback) {
390+
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
389391
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);
390392

391393
oauthParameters[OAuth2Parameters.CLIENT_SECRET] = clientSecret;
392394

393-
this._oauthGetToken(oauthParameters, innerCallback);
395+
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
394396
});
395397
};
396398

@@ -455,11 +457,33 @@ TokenRequest.prototype.getTokenFromCacheWithRefresh = function(userId, callback)
455457
this._log.info('Getting token from cache with refresh if necessary.');
456458

457459
this._userId = userId;
458-
this._getToken(callback, function(innerCallback){
460+
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
459461
// If this method was called then no cached entry was found. Since
460462
// this particular version of acquireToken can only retrieve tokens
461463
// from the cache, return an error.
462-
innerCallback(self._log.createError('Entry not found in cache.'));
464+
getTokenCompleteCallback(self._log.createError('Entry not found in cache.'));
465+
});
466+
};
467+
468+
/**
469+
* Obtains a token via a certificate. The certificate is used to generate a self signed
470+
* JWT token that is passed as a client_assertion.
471+
* @param {string} certificate A PEM encoded certificate private key.
472+
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
473+
* @param {AcquireTokenCallback} callback
474+
*/
475+
TokenRequest.prototype.getTokenWithCertificate = function(certificate, thumbprint, callback) {
476+
this._log.info('Getting a new token from a refresh token.');
477+
478+
var authorityUrl = this._authenticationContext._authority;
479+
var jwt = new SelfSignedJwt(this._callContext, authorityUrl, this._clientId, certificate);
480+
481+
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);
482+
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION_TYPE] = OAuth2GrantType.JWT_BEARER;
483+
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION] = jwt.create(certificate, thumbprint); // TODO: Check error!
484+
485+
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
486+
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
463487
});
464488
};
465489

lib/util.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ function copyUrl(urlSource) {
163163
}
164164

165165
function convertUrlSafeToRegularBase64EncodedString(str) {
166-
return str.replace('-', '+').replace('_', '/');
166+
return str.replace(/-/g, '+').replace(/_/g, '/');
167167
}
168168

169169
function convertRegularToUrlSafeBase64EncodedString(str) {
170-
return str.replace('+', '-').replace('/', '_').replace('=', '');
170+
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
171171
}
172172

173173
function base64DecodeStringUrlSafe(str) {
@@ -176,7 +176,9 @@ function base64DecodeStringUrlSafe(str) {
176176
}
177177

178178
function base64EncodeStringUrlSafe(str) {
179-
return convertRegularToUrlSafeBase64EncodedString((new Buffer(str).toString('base64')));
179+
var base64 = (new Buffer(str, 'utf8').toString('base64'));
180+
var converted = convertRegularToUrlSafeBase64EncodedString(base64);
181+
return converted;
180182
}
181183

182184
module.exports.adalInit = adalInit;
@@ -185,4 +187,5 @@ module.exports.createRequestHandler = createRequestHandler;
185187
module.exports.createRequestOptions = createRequestOptions;
186188
module.exports.copyUrl = copyUrl;
187189
module.exports.base64DecodeStringUrlSafe = base64DecodeStringUrlSafe;
188-
module.exports.base64EncodeStringUrlSafe = base64EncodeStringUrlSafe;
190+
module.exports.base64EncodeStringUrlSafe = base64EncodeStringUrlSafe;
191+
module.exports.convertRegularToUrlSafeBase64EncodedString = convertRegularToUrlSafeBase64EncodedString;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"licenses": [ { "type": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0" } ],
2424
"dependencies": {
2525
"date-utils": "*",
26+
"jws": "1.x.x",
2627
"node-uuid": "1.4.1",
2728
"request": ">= 2.9.203",
2829
"underscore": ">= 1.3.1",

test/username-password.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,8 @@ suite('username-password', function() {
677677
});
678678

679679
test('bad-id-token-base64-in-response', function(done) {
680+
console.log('HEY');
681+
util.turnOnLogging();
680682
var foundWarning = false;
681683
var preRequests = util.setupExpectedUserRealmResponseCommon(false);
682684
var response = util.createResponse();

0 commit comments

Comments
 (0)