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

Add support for certificate based confidential client auth. acquireTokenWithClientCertificate #32

Merged
merged 11 commits into from
Dec 16, 2014
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions lib/authentication-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,30 @@ AuthenticationContext.prototype.acquireTokenWithRefreshToken = function(refreshT
});
};

util.adalInit();
/**
* Gets a new access token using via a certificate credential.
* @param {string} resource A URI that identifies the resource for which the token is valid.
* @param {string} clientId The OAuth client id of the calling application.
* @param {string} certificate A PEM encoded certificate private key.
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
* @param {AcquireTokenCallback} callback The callback function.
*/
AuthenticationContext.prototype.acquireTokenWithClientCertificate = function(resource, clientId, certificate, thumbprint, callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We abstract out client certificate using class ClientAssertionCertificate which has the Sign method (and maybe a hash method). This way, the object can be created differently on different platforms and the API still have the same OM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have another class named ClientAssertion and an AcquireToken API for it, so developer can create the assertion once and pass it multiple times.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One idea we discussed in the API review meetings was to get rid of ClientAssertionCertificate and add a factory method (or constructor) to ClientAssertion to create it using a certificate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afshins I don't think that the ClientAssertionCertificate class makes sense in Node. There is only one kind of certificate in Node and that is a PEM file. There is no need to create the cert differently on different platforms.

I can get to the version that accepts an already generated assertion later. I don't think we need to block this update on that particular version.

In general, Node is much lighter weight than C#, and generally doesn't involve a whole lot of classes. Since there is no static type checking it doesn't make as much sense to have a bunch of classes. In node strings are acceptable. I believe that it would be unnatural in node to force the developer to deal with a bunch of other classes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. We need to make OM natural to the platform, so feel free to follow the common pattern.

argument.validateCallbackType(callback);
try {
argument.validateStringParameter(resource, 'resource');
argument.validateStringParameter(certificate, 'certificate');
argument.validateStringParameter(thumbprint, 'thumbprint');
} catch(err) {
callback(err);
return;
}

this._acquireToken(callback, function() {
var tokenRequest = new TokenRequest(this._callContext, this, clientId, resource);
tokenRequest.getTokenWithCertificate(certificate, thumbprint, callback);
});
};

var exports = {
AuthenticationContext : AuthenticationContext,
Expand All @@ -347,4 +370,5 @@ var exports = {
}
};

module.exports = exports;
util.adalInit();
module.exports = exports;
2 changes: 1 addition & 1 deletion lib/authentication-parameters.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* @copyright
* Copyright © Microsoft Open Technologies, Inc.
*
Expand Down
15 changes: 14 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var Constants = {
OAuth2 : {
Parameters : {
GRANT_TYPE : 'grant_type',
CLIENT_ASSERTION : 'client_assertion',
CLIENT_ASSERTION_TYPE : 'client_assertion_type',
CLIENT_ID : 'client_id',
CLIENT_SECRET : 'client_secret',
REDIRECT_URI : 'redirect_uri',
Expand All @@ -41,6 +43,7 @@ var Constants = {
AUTHORIZATION_CODE : 'authorization_code',
REFRESH_TOKEN : 'refresh_token',
CLIENT_CREDENTIALS : 'client_credentials',
JWT_BEARER : 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
PASSWORD : 'password',
SAML1 : 'urn:ietf:params:oauth:grant-type:saml1_1-bearer',
SAML2 : 'urn:ietf:params:oauth:grant-type:saml2-bearer'
Expand Down Expand Up @@ -99,6 +102,16 @@ var Constants = {
CLOCK_BUFFER : 5 // In minutes.
},

Jwt : {
SELF_SIGNED_JWT_LIFETIME : 10, // 10 mins in mins
AUDIENCE : 'aud',
ISSUER : 'iss',
SUBJECT : 'sub',
NOT_BEFORE : 'nbf',
EXPIRES_ON : 'exp',
JWT_ID : 'jti'
},

AADConstants : {
WORLD_WIDE_AUTHORITY : 'login.windows.net',
WELL_KNOWN_AUTHORITY_HOSTS : ['login.windows.net', 'login.chinacloudapi.cn', 'login.cloudgovapi.us'],
Expand Down Expand Up @@ -156,4 +169,4 @@ var Constants = {
}
};

module.exports = Constants;
module.exports = Constants;
167 changes: 167 additions & 0 deletions lib/self-signed-jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* @copyright
* Copyright © Microsoft Open Technologies, Inc.
*
* All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http: *www.apache.org/licenses/LICENSE-2.0
*
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
* OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
* ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
* PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
*
* See the Apache License, Version 2.0 for the specific language
* governing permissions and limitations under the License.
*/
'use strict';

var jwtConstants = require('./constants').Jwt;
var Logger = require('./log').Logger;
var util = require('./util');

require('date-utils');
var jws = require('jws');
var uuid = require('node-uuid');

/**
* JavaScript dates are in milliseconds, but JWT dates are in seconds.
* This function does the conversion.
* @param {Date} date
* @return {string}
*/
function dateGetTimeInSeconds(date) {
return Math.floor(date.getTime()/1000);
}

/**
* Constructs a new SelfSignedJwt object.
* @param {object} callContext Context specific to this token request.
* @param {Authority} authority The authority to be used as the JWT audience.
* @param {string} clientId The client id of the calling app.
*/
function SelfSignedJwt(callContext, authority, clientId) {
this._log = new Logger('SelfSignedJwt', callContext._logContext);
this._callContext = callContext;

this._authority = authority;
this._tokenEndpoint = authority.tokenEndpoint;
this._clientId = clientId;
}

/**
* This wraps date creation in order to make unit testing easier.
* @return {Date}
*/
SelfSignedJwt.prototype._getDateNow = function() {
return new Date();
};

SelfSignedJwt.prototype._getNewJwtId = function() {
return uuid.v4();
};

/**
* A regular certificate thumbprint is a hex encode string of the binary certificate
* hash. For some reason teh x5t value in a JWT is a url save base64 encoded string
* instead. This function does the conversion.
* @param {string} thumbprint A hex encoded certificate thumbprint.
* @return {string} A url safe base64 encoded certificate thumbprint.
*/
SelfSignedJwt.prototype._createx5tValue = function(thumbprint) {
var hexString = thumbprint.replace(/:/g, '').replace(/ /g, '');
var base64 = (new Buffer(hexString, 'hex')).toString('base64');
return util.convertRegularToUrlSafeBase64EncodedString(base64);
};

/**
* Creates the JWT header.
* @param {string} thumbprint A hex encoded certificate thumbprint.
* @return {object}
*/
SelfSignedJwt.prototype._createHeader = function(thumbprint) {
var x5t = this._createx5tValue(thumbprint);
var header = { typ: 'JWT', alg: 'RS256', x5t : x5t };

this._log.verbose('Creating self signed JWT header. x5t: ' + x5t);

return header;
};

/**
* Creates the JWT payload.
* @return {object}
*/
SelfSignedJwt.prototype._createPayload = function() {
var now = this._getDateNow();
var expires = (new Date(now.getTime())).addMinutes(jwtConstants.SELF_SIGNED_JWT_LIFETIME);

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

var jwtPayload = {};
jwtPayload[jwtConstants.AUDIENCE] = this._tokenEndpoint;
jwtPayload[jwtConstants.ISSUER] = this._clientId;
jwtPayload[jwtConstants.SUBJECT] = this._clientId;
jwtPayload[jwtConstants.NOT_BEFORE] = dateGetTimeInSeconds(now);
jwtPayload[jwtConstants.EXPIRES_ON] = dateGetTimeInSeconds(expires);
jwtPayload[jwtConstants.JWT_ID] = this._getNewJwtId();

return jwtPayload;
};

SelfSignedJwt.prototype._throwOnInvalidJwtSignature = function(jwt) {
var jwtSegments = jwt.split('.');

if (3 > jwtSegments.length || !jwtSegments[2]) {
throw this._log.createError('Failed to sign JWT. This is most likely due to an invalid certificate.');
}

return;
};

SelfSignedJwt.prototype._signJwt = function(header, payload, certificate) {
var jwt = jws.sign({ header : header, payload : payload, secret : certificate});
this._throwOnInvalidJwtSignature(jwt);
return jwt;
};

SelfSignedJwt.prototype._reduceThumbprint = function(thumbprint) {
var canonical = thumbprint.toLowerCase().replace(/ /g, '').replace(/:/g, '');
this._throwOnInvalidThumbprint(canonical);
return canonical;
};

var numCharIn128BitHexString = 128/8*2;
var numCharIn160BitHexString = 160/8*2;
var thumbprintSizes = {};
thumbprintSizes[numCharIn128BitHexString] = true;
thumbprintSizes[numCharIn160BitHexString] = true;
var thumbprintRegExp = /^[a-f\d]*$/;

SelfSignedJwt.prototype._throwOnInvalidThumbprint = function(thumbprint) {
if (!thumbprintSizes[thumbprint.length] || !thumbprintRegExp.test(thumbprint)) {
throw this._log.createError('The thumbprint does not match a known format');
}
};

/**
* Creates a self signed JWT that can be used as a client_assertion.
* @param {string} certificate A PEM encoded certificate private key.
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
* @return {string} A self signed JWT token.
*/
SelfSignedJwt.prototype.create = function(certificate, thumbprint) {
thumbprint = this._reduceThumbprint(thumbprint);
var header = this._createHeader(thumbprint);

var payload = this._createPayload();

var jwt = this._signJwt(header, payload, certificate);
return jwt;
};

module.exports = SelfSignedJwt;
76 changes: 65 additions & 11 deletions lib/token-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var CacheDriver = require('./cache-driver');
var Logger = require('./log').Logger;
var Mex = require('./mex');
var OAuth2Client = require('./oauth2client');
var SelfSignedJwt = require('./self-signed-jwt');
var UserRealm = require('./user-realm');
var WSTrustRequest = require('./wstrust-request');

Expand Down Expand Up @@ -76,6 +77,10 @@ TokenRequest.prototype._createOAuth2Client = function() {
return new OAuth2Client(this._callContext, this._authenticationContext._authority);
};

TokenRequest.prototype._createSelfSignedJwt = function() {
return new SelfSignedJwt(this._callContext, this._authenticationContext._authority, this._clientId);
};

TokenRequest.prototype._oauthGetToken = function(oauthParameters, callback) {
var client = this._createOAuth2Client();
client.getToken(oauthParameters, callback);
Expand Down Expand Up @@ -112,13 +117,14 @@ TokenRequest.prototype._createCacheQuery = function() {
if (this._userId) {
query.userId = this._userId;
} else {
this._log.verbose('No userId passed.');
this._log.verbose('No userId passed for cache query.');
}

return query;
};

TokenRequest.prototype._getToken = function(callback, getTokenFunc) {

TokenRequest.prototype._getTokenWithCacheWrapper = function(callback, getTokenFunc) {
var self = this;
this._cacheDriver = this._createCacheDriver();
var cacheQuery = this._createCacheQuery();
Expand Down Expand Up @@ -353,24 +359,24 @@ TokenRequest.prototype.getTokenWithUsernamePassword = function(username, passwor
this._log.info('Acquiring token with username password');

this._userId = username;
this._getToken(callback, function(innerCallback) {
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
var self = this;
this._userRealm = this._createUserRealmRequest(username);
this._userRealm.discover(function(err) {
if (err) {
innerCallback(err);
getTokenCompleteCallback(err);
return;
}

switch(self._userRealm.accountType) {
case AccountType.Managed:
self._getTokenUsernamePasswordManaged(username, password, innerCallback);
self._getTokenUsernamePasswordManaged(username, password, getTokenCompleteCallback);
return;
case AccountType.Federated:
self._getTokenUsernamePasswordFederated(username, password, innerCallback);
self._getTokenUsernamePasswordFederated(username, password, getTokenCompleteCallback);
return;
default:
innerCallback(self._log.createError('Server returned an unknown AccountType: ' + self._userRealm.AccountType));
getTokenCompleteCallback(self._log.createError('Server returned an unknown AccountType: ' + self._userRealm.AccountType));
}
});
});
Expand All @@ -385,12 +391,12 @@ TokenRequest.prototype.getTokenWithUsernamePassword = function(username, passwor
TokenRequest.prototype.getTokenWithClientCredentials = function(clientSecret, callback) {
this._log.info('Getting token with client credentials.');

this._getToken(callback, function(innerCallback) {
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);

oauthParameters[OAuth2Parameters.CLIENT_SECRET] = clientSecret;

this._oauthGetToken(oauthParameters, innerCallback);
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
});
};

Expand Down Expand Up @@ -455,11 +461,59 @@ TokenRequest.prototype.getTokenFromCacheWithRefresh = function(userId, callback)
this._log.info('Getting token from cache with refresh if necessary.');

this._userId = userId;
this._getToken(callback, function(innerCallback){
this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
// If this method was called then no cached entry was found. Since
// this particular version of acquireToken can only retrieve tokens
// from the cache, return an error.
innerCallback(self._log.createError('Entry not found in cache.'));
getTokenCompleteCallback(self._log.createError('Entry not found in cache.'));
});
};

/**
* Creates a self signed jwt.
* @param {string} authorityUrl
* @param {string} certificate A PEM encoded certificate private key.
* @param {string} thumbprint
* @return {string} A self signed JWT
*/
TokenRequest.prototype._createJwt = function(authorityUrl, certificate, thumbprint) {
var jwt;
var ssj = this._createSelfSignedJwt();
jwt = ssj.create(certificate, thumbprint);
if (!jwt) {
throw this._log.createError('Failed to create JWT');
}

return jwt;
};

/**
* Obtains a token via a certificate. The certificate is used to generate a self signed
* JWT token that is passed as a client_assertion.
* @param {string} certificate A PEM encoded certificate private key.
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
* @param {AcquireTokenCallback} callback
*/
TokenRequest.prototype.getTokenWithCertificate = function(certificate, thumbprint, callback) {

this._log.info('Getting a new token from a refresh token.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log message needs to be updated here.


var authorityUrl = this._authenticationContext._authority;

var jwt;
try {
jwt = this._createJwt(authorityUrl, certificate, thumbprint);
} catch (err) {
callback(err);
return;
}

var oauthParameters = this._createOAuthParameters(OAuth2GrantType.CLIENT_CREDENTIALS);
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION_TYPE] = OAuth2GrantType.JWT_BEARER;
oauthParameters[OAuth2Parameters.CLIENT_ASSERTION] = jwt;

this._getTokenWithCacheWrapper(callback, function(getTokenCompleteCallback) {
this._oauthGetToken(oauthParameters, getTokenCompleteCallback);
});
};

Expand Down
Loading