UNPKG

azure-cli

Version:

Microsoft Azure Cross Platform Command Line tool

304 lines (268 loc) 9.8 kB
/** * Copyright (c) Microsoft. 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 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; var _ = require('underscore'); var adal = require('adal-node'); var os = require('os'); var path = require('path'); var util = require('util'); var utils = require('../utils'); var defaultTokenCacheFile = path.join(utils.azureDir(), 'accessTokens.json'); var TokenStorage; if (os.platform() === 'darwin') { TokenStorage = require('./osx-token-storage'); } else if (os.platform() === 'win32') { TokenStorage = require('./win-token-storage'); } else { TokenStorage = require('./file-token-storage'); } var TokenCache = require('./token-cache'); var logging = require('../logging'); var $ = utils.getLocaleString; function turnOnLogging() { var log = adal.Logging; log.setLoggingOptions( { level : log.LOGGING_LEVEL.VERBOSE, log : function(level, message, error) { logging.info(message); if (error) { logging.error(error); } } }); } if (process.env['AZURE_ADAL_LOGGING_ENABLED']) { turnOnLogging(); } // // A list of known azure test endpoints for active directory. // Turn off authority verification if authority is one of these. // var knownTestEndpoints = [ 'https://login.windows-ppe.net', 'https://sts.login.windows-int.net' ]; function isKnownTestEndpoint(authorityUrl) { return _.some(knownTestEndpoints, function (endpoint) { return utils.ignoreCaseEquals(endpoint, authorityUrl); }); } /** * Given a user name derive the tenant id * * @param {string} username name of user * * @returns {string} tenant Id */ function tenantIdForUser(username) { var match = username.match(/@(.*)+$/); if (match === null) { throw new Error(util.format($('No tenant found in username %s'), username)); } var tenant = match[1]; if (tenant.indexOf('.') === -1) { tenant = tenant + '.onmicrosoft.com'; } return tenant; } /** * Add the '.onmicrosoft.com' suffix to the user name * if it's required and not present. * * @param {string} username The original user name * * @returns {string} the updated if necessary username */ function normalizeUserName(username) { var match = username.match(/^([^@]+@)([^.]+)$/); if (match !== null) { username = match[1] + match[2] + '.onmicrosoft.com'; } return username; } function createAuthenticationContext(authConfig) { var authorityUrl = authConfig.authorityUrl + '/' + authConfig.tenantId; var validateAuthority = !isKnownTestEndpoint(authConfig.authorityUrl); return new adal.AuthenticationContext(authorityUrl, validateAuthority, exports.tokenCache); } function AdalAccessToken(authConfig, userId) { this.authConfig = authConfig; this.userId = userId; } _.extend(AdalAccessToken.prototype, { authenticateRequest: function (authorizer) { var context = createAuthenticationContext(this.authConfig); context.acquireToken(this.authConfig.resourceId, this.userId, this.authConfig.clientId, function (err, result) { if (err) { authorizer(new Error($('Credentials have expired, please reauthenticate' + ' Detailed error message from ADAL is as follows: ' + err))); } else { authorizer(null, 'Bearer', result.accessToken); } }); }, }); // // Functions to store service principal keys in the // token cache. We're borrowing the token cache for this // because it's already using OS-specific credential // management functions. // function saveServicePrincipalKey(appId, tenantId, key, callback) { var entry = { servicePrincipalId: appId, servicePrincipalTenant: tenantId, // token cache implementations specifically look // for these two keys to store as the secret, // so we use them to store the key. accessToken: key, refreshToken: '' }; exports.tokenCache.add([entry], callback); } function loadServicePrincipalKey(appId, tenantId, callback) { var query = { servicePrincipalId: appId, servicePrincipalTenant: tenantId }; exports.tokenCache.find(query, function (err, entries) { if (err) { return callback(err); } if (entries.length === 0) { return callback(new Error('No service principal key found')); } callback(null, entries[0].accessToken); }); } function removeInvalidServicePrincipalKey(appId, tenantId, callback) { var query = { servicePrincipalId: appId, servicePrincipalTenant: tenantId }; exports.tokenCache.find(query, function (err, entries) { if (err) { return callback(err); } exports.tokenCache.remove(entries, callback); }); } function ServicePrincipalAccessToken(authConfig, appId) { this.authConfig = authConfig; this.appId = appId; } _.extend(ServicePrincipalAccessToken.prototype, { authenticateRequest: function (callback) { var self = this; loadServicePrincipalKey(self.appId, self.authConfig.tenantId, function (err, key) { if (err) { return callback(new Error(util.format($('No service key found for appid %s'), self.appId))); } var context = createAuthenticationContext(self.authConfig); context.acquireTokenWithClientCredentials(self.authConfig.resourceId, self.appId, key, function (err, result) { if (err) { //because the invalid key is cached, let us clean it up; otherwise the next logon attempt //will fail removeInvalidServicePrincipalKey(self.appId, self.authConfig.tenantId, function(errorOnRemove){ if (errorOnRemove) { //this error would be rare, but warn still, just in case. logging.warn($('Failed to clean up invalid credentils for service principal')); } return callback(util.format($('Unable to acquire token due to error of: %s'), err.message)); }); } else { return callback(null, result.tokenType, result.accessToken); } }); }); } }); /** * Call to Active Directory tenant to get a token back. * Returns accessToken object via callback. * * @param {AuthenticationConfig} authConfig Connection details for AD * * @param {string} authConfig.authorityUrl Url for AD tenant to authenticate against * @param {string} authConfig.tenantId Active directory tenant ID * @param {string} authConfig.clientId Client ID that is requesting authentication * @param {string} authConfig.resourceId Id of resoure being accessed * * @param {string} username user identifier * @param {string} password the password * @param {function} callback callback function (err, accessToken) * */ function acquireToken(authConfig, username, password, callback) { var context = createAuthenticationContext(authConfig); context.acquireTokenWithUsernamePassword(authConfig.resourceId, username, password, authConfig.clientId, function (err, response) { if (err) { return callback(err); } callback(null, new AdalAccessToken(authConfig, response.userId)); }); } /** * Call to Active Directory tenant to get a token for a service principal back. * Returns accessToken object via callback. * * @param {AuthenticationConfig} authConfig Connection details for AD * * @param {string} authConfig.authorityUrl Url for AD tenant to authenticate against * @param {string} authConfig.tenantId Active directory tenant ID or domain * @param {string} authConfig.clientId Client ID that is requesting authentication * @param {string} authConfig.resourceId Id of resoure being accessed * * @param {string} appId AppId for the service principal * @param {string} serviceKey Service Principal's secret key * @param {function} callback callback function (err, accessToken) * */ function acquireServicePrincipalToken(authConfig, appId, serviceKey, callback) { saveServicePrincipalKey(appId, authConfig.tenantId, serviceKey, function (err) { if (err) { return callback(err); } callback(null, new ServicePrincipalAccessToken(authConfig, appId, authConfig.tenantId)); }); } /** * This is the callback passed to the logoutUser method * @callback LogoutUserCallback * @param {Error} [err] Any errors that occur are passed here. */ /** * Logs out a user, deleting any cached users for that username. * * @param {string} username username to remove tokens for. * @param {TokenCache} [cache] cache to delete from, optional, uses * default cache if not given * @param {LogoutUserCallback} done completion callback */ function logoutUser(username, cache, done) { if (typeof cache === 'function') { done = cache; cache = exports.tokenCache; } cache.find({userId: username}, function (err, found) { if (err) { return done(err); } cache.remove(found, done); }); } _.extend(exports, { defaultTokenCacheFile: defaultTokenCacheFile, tokenCache: new TokenCache(new TokenStorage(defaultTokenCacheFile)), tenantIdForUser: tenantIdForUser, normalizeUserName: normalizeUserName, AdalAccessToken: AdalAccessToken, ServicePrincipalAccessToken: ServicePrincipalAccessToken, acquireToken: acquireToken, acquireServicePrincipalToken: acquireServicePrincipalToken, logoutUser: logoutUser });