Implemented ServicePrincipal login with certificate (#53)

* update nyc dependency to fix security warning from npm

* Implemented ServicePrincipal login with certificate

* code review feedback

* Add samples that can be run live
This commit is contained in:
Amar Zavery 2019-05-06 13:22:16 -07:00 коммит произвёл GitHub
Родитель 0855f90c6c
Коммит 291bbb9d48
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 653 добавлений и 150 удалений

9
.gitignore поставляемый
Просмотреть файл

@ -62,5 +62,10 @@ typings/
*.js
*.js.map
# package-lock.json
package-lock.json
# package-lock.json
package-lock.json
# additional metadata
authfileWithCert.json
authfileWithSecret.json
spcert.pem

Просмотреть файл

@ -1,8 +1,38 @@
# Changelog
## 1.0.0 - 2019/05/06
- Added support for ServicePrincipal login with certificates.
- Updated dependencies to their latest versions.
## 0.9.3 - 2019/04/04
- Updated `@azure/ms-rest-js` to the latest version `^1.8.1`.
## 0.9.2 - 2019/03/26
- Updated the return types for calls using interactive login, user name/ password and service principal to return the right types with promise flavor methods.
## 0.9.1 - 2019/01/15
- Fixed issues in AppService MSI login.
- Improved documentation of `MSIAppServiceTokenCredentials.getToken()`
## 0.9.0 - 2019/01/11
- Added support for custom MSI endpoint.
## 0.8.4 - 2019/01/09
- Exported MSI login methods from the package.
## 0.8.3 - 2018/12/18
- Added a check for verifying the package.json version
- Added azure pipelines for CI.
## 0.8.2 - 2018/11/19
- Fixed incorrect path in the "main" node of package.json.
## 0.8.1 - 2018/11/19
- Added owners and issue template.
- Improved internal structure of the package.
## 0.8.0 - 2018/11/12
- Rename package to "@azure/ms-rest-nodeauth"
- Renamed package to "@azure/ms-rest-nodeauth"
## 0.6.0 - 2018/09/27

Просмотреть файл

@ -19,7 +19,7 @@ msRestNodeAuth.loginWithUsernamePasswordWithAuthResponse(username, password).the
});
```
### service-principal/secret based login
### service-principal and secret based login
```typescript
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
@ -34,6 +34,45 @@ msRestNodeAuth.loginWithServicePrincipalSecretWithAuthResponse(clientId, secret,
});
```
#### service-principal and certificate based login by providing an ABSOLUTE file path to the .pem file
```typescript
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
const clientId = process.env["CLIENT_ID"];
const tenantId = process.env["DOMAIN"];
msRestNodeAuth.loginWithServicePrincipalCertificateWithAuthResponse(clientId, "/Users/user1/foo.pem", tenantId).then((authres) => {
console.dir(authres, { depth: null })
}).catch((err) => {
console.log(err);
});
```
#### service-principal and certificate based login by providing the certificate and private key (contents of the .pem file)
```typescript
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
const clientId = process.env["CLIENT_ID"];
const tenantId = process.env["DOMAIN"];
const certificate =
`
-----BEGIN PRIVATE KEY-----
xxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxx
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
yyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyy
-----END CERTIFICATE-----
`;
msRestNodeAuth.loginWithServicePrincipalCertificateWithAuthResponse(clientId, certificate, tenantId).then((authres) => {
console.dir(authres, { depth: null })
}).catch((err) => {
console.log(err);
});
```
### interactive/device-code flow login
```typescript
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
@ -46,6 +85,19 @@ msRestNodeAuth.interactiveLoginWithAuthResponse().then((authres) => {
```
### service-principal authentication from auth file on disk
Before using this method please install az cli from https://github.com/Azure/azure-cli/releases.
Then execute `az ad sp create-for-rbac --sdk-auth > ${yourFilename.json}`.
If you want to create the sp for a different cloud/environment then please execute:
1. az cloud list
2. az cloud set –n <name of the environment>
3. az ad sp create-for-rbac --sdk-auth > auth.json // create sp with **secret**.
**OR**
az ad sp create-for-rbac --create-cert --sdk-auth > auth.json // create sp with **certificate**.
If the service principal is already created then login with service principal info:
4. az login --service-principal -u <clientId> -p <clientSecret> -t <tenantId>
5. az account show --sdk-auth > auth.json
```typescript
import * as msRestNodeAuth from "../lib/msRestNodeAuth";

Просмотреть файл

@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { readFileSync } from "fs";
import { createHash } from "crypto";
import { ApplicationTokenCredentialsBase } from "./applicationTokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
import { TokenResponse, ErrorResponse, TokenCache } from "adal-node";
import { AzureTokenCredentialsOptions } from "../login";
export class ApplicationTokenCertificateCredentials extends ApplicationTokenCredentialsBase {
readonly certificate: string;
readonly thumbprint: string;
/**
* Creates a new ApplicationTokenCredentials object.
* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net}
* for detailed instructions on creating an Azure Active Directory application.
* @constructor
* @param {string} clientId The active directory application client id.
* @param {string} domain The domain or tenant id containing this application.
* @param {string} certificate A PEM encoded certificate private key.
* @param {string} thumbprint A hex encoded thumbprint of the certificate.
* @param {string} [tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [environment] The azure environment to authenticate with.
* @param {object} [tokenCache] The token cache. Default value is the MemoryCache object from adal.
*/
public constructor(
clientId: string,
domain: string,
certificate: string,
thumbprint: string,
tokenAudience?: TokenAudience,
environment?: Environment,
tokenCache?: TokenCache
) {
if (!certificate || typeof certificate.valueOf() !== "string") {
throw new Error("certificate must be a non empty string.");
}
if (!thumbprint || typeof thumbprint.valueOf() !== "string") {
throw new Error("thumbprint must be a non empty string.");
}
super(clientId, domain, tokenAudience, environment, tokenCache);
this.certificate = certificate;
this.thumbprint = thumbprint;
}
/**
* Tries to get the token from cache initially. If that is unsuccessfull then it tries to get the token from ADAL.
* @returns {Promise<TokenResponse>} A promise that resolves to TokenResponse and rejects with an Error.
*/
public async getToken(): Promise<TokenResponse> {
try {
const tokenResponse = await this.getTokenFromCache();
return tokenResponse;
} catch (error) {
if (error.message.startsWith(AuthConstants.SDK_INTERNAL_ERROR)) {
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
const resource = this.getActiveDirectoryResourceId();
this.authContext.acquireTokenWithClientCertificate(
resource,
this.clientId,
this.certificate,
this.thumbprint,
(error: any, tokenResponse: TokenResponse | ErrorResponse) => {
if (error) {
return reject(error);
}
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse as TokenResponse);
}
);
});
}
}
/**
* Creates a new instance of ApplicationTokenCertificateCredentials.
*
* @param clientId The active directory application client id also known as the SPN (ServicePrincipal Name).
* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net}
* for an example.
* @param {string} certificateStringOrFilePath A PEM encoded certificate and private key OR an absolute filepath to the .pem file containing that information. For example:
* - CertificateString: "-----BEGIN PRIVATE KEY-----\n<xxxxx>\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\n<yyyyy>\n-----END CERTIFICATE-----\n"
* - CertificateFilePath: **Absolute** file path of the .pem file.
* @param domain The domain or tenant id containing this application.
* @param options AzureTokenCredentialsOptions - Object representing optional parameters.
*
* @returns ApplicationTokenCertificateCredentials
*/
public static create(
clientId: string,
certificateStringOrFilePath: string,
domain: string,
options: AzureTokenCredentialsOptions
): ApplicationTokenCertificateCredentials {
if (
!certificateStringOrFilePath ||
typeof certificateStringOrFilePath.valueOf() !== "string"
) {
throw new Error(
"'certificateStringOrFilePath' must be a non empty string."
);
}
if (!certificateStringOrFilePath.startsWith("-----BEGIN")) {
certificateStringOrFilePath = readFileSync(
certificateStringOrFilePath,
"utf8"
);
}
const certificatePattern = /(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9\+\/\n\r]+\=*)(\n\r?|\r\n?)(-+END CERTIFICATE-+)/;
const matchCert = certificateStringOrFilePath.match(certificatePattern);
const rawCertificate = matchCert ? matchCert[3] : "";
if (!rawCertificate) {
throw new Error(
"Unable to correctly parse the certificate from the value provided in 'certificateStringOrFilePath' "
);
}
const thumbprint = createHash("sha1")
.update(Buffer.from(rawCertificate, "base64"))
.digest("hex");
return new ApplicationTokenCertificateCredentials(
clientId,
domain,
certificateStringOrFilePath,
thumbprint,
options.tokenAudience,
options.environment,
options.tokenCache
);
}
}

Просмотреть файл

@ -1,13 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { ApplicationTokenCredentialsBase } from "./applicationTokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
import { TokenResponse, ErrorResponse } from "adal-node";
export class ApplicationTokenCredentials extends TokenCredentialsBase {
import { TokenResponse, ErrorResponse, TokenCache } from "adal-node";
export class ApplicationTokenCredentials extends ApplicationTokenCredentialsBase {
readonly secret: string;
/**
@ -29,12 +28,12 @@ export class ApplicationTokenCredentials extends TokenCredentialsBase {
secret: string,
tokenAudience?: TokenAudience,
environment?: Environment,
tokenCache?: any) {
if (!Boolean(secret) || typeof secret.valueOf() !== "string") {
tokenCache?: TokenCache
) {
if (!secret || typeof secret.valueOf() !== "string") {
throw new Error("secret must be a non empty string.");
}
super(clientId, domain, tokenAudience, environment as any, tokenCache);
super(clientId, domain, tokenAudience, environment, tokenCache);
this.secret = secret;
}
@ -43,84 +42,34 @@ export class ApplicationTokenCredentials extends TokenCredentialsBase {
* Tries to get the token from cache initially. If that is unsuccessfull then it tries to get the token from ADAL.
* @returns {Promise<TokenResponse>} A promise that resolves to TokenResponse and rejects with an Error.
*/
public getToken(): Promise<TokenResponse> {
return this.getTokenFromCache()
.then((tokenResponse) => tokenResponse)
.catch((error) => {
if (error.message.startsWith(AuthConstants.SDK_INTERNAL_ERROR)) {
return Promise.reject(error);
}
const resource = this.getActiveDirectoryResourceId();
return new Promise((resolve, reject) => {
this.authContext.acquireTokenWithClientCredentials(resource, this.clientId, this.secret,
(error: any, tokenResponse: TokenResponse | ErrorResponse) => {
if (error) {
return reject(error);
}
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse);
});
});
public async getToken(): Promise<TokenResponse> {
try {
const tokenResponse = await this.getTokenFromCache();
return tokenResponse;
} catch (error) {
if (
error.message &&
error.message.startsWith(AuthConstants.SDK_INTERNAL_ERROR)
) {
return Promise.reject(error);
}
const resource = this.getActiveDirectoryResourceId();
return new Promise((resolve, reject) => {
this.authContext.acquireTokenWithClientCredentials(
resource,
this.clientId,
this.secret,
(error: any, tokenResponse: TokenResponse | ErrorResponse) => {
if (error) {
return reject(error);
}
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse as TokenResponse);
}
);
});
}
}
protected getTokenFromCache(): Promise<any> {
const self = this;
// a thin wrapper over the base implementation. try get token from cache, additionaly clean up cache if required.
return super.getTokenFromCache(undefined).then((tokenResponse: TokenResponse) => {
return Promise.resolve(tokenResponse);
}).catch((error: any) => {
// Remove the stale token from the tokencache. ADAL gives the same error message "Entry not found in cache."
// for entry not being present in the cache and for accessToken being expired in the cache. We do not want the token cache
// to contain the expired token, we clean it up here.
return self.removeInvalidItemsFromCache({ _clientId: self.clientId }).then((status) => {
if (status.result) {
return Promise.reject(error);
}
const msg = status && status.details && status.details.message ? status.details.message : status.details;
return Promise.reject(new Error(AuthConstants.SDK_INTERNAL_ERROR + " : "
+ "critical failure while removing expired token for service principal from token cache. "
+ msg));
});
});
}
/**
* Removes invalid items from token cache. This method is different. Here we never reject in case of error.
* Rather we resolve with an object that says the result is false and error information is provided in
* the details property of the resolved object. This is done to do better error handling in the above function
* where removeInvalidItemsFromCache() is called.
* @param {object} query The query to be used for finding the token for service principal from the cache
* @returns {result: boolean, details?: Error} resultObject with more info.
*/
private removeInvalidItemsFromCache(query: object): Promise<{ result: boolean, details?: Error }> {
const self = this;
return new Promise<{ result: boolean, details?: Error }>((resolve) => {
self.tokenCache.find(query, (error: Error, entries: any[]) => {
if (error) {
return resolve({ result: false, details: error });
}
if (entries && entries.length > 0) {
// return resolve(self.tokenCache.remove(entries, () => resolve({ result: true })));
return new Promise((resolve) => {
return self.tokenCache.remove(entries, (err: Error) => {
if (err) {
return resolve({ result: false, details: err });
}
return resolve({ result: true });
});
});
} else {
return resolve({ result: true });
}
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
import { TokenCache, TokenResponse } from "adal-node";
export abstract class ApplicationTokenCredentialsBase extends TokenCredentialsBase {
/**
* Creates a new ApplicationTokenCredentials object.
* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net}
* for detailed instructions on creating an Azure Active Directory application.
* @constructor
* @param {string} clientId The active directory application client id.
* @param {string} domain The domain or tenant id containing this application.
* @param {string} [tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [environment] The azure environment to authenticate with.
* @param {object} [tokenCache] The token cache. Default value is the MemoryCache object from adal.
*/
public constructor(
clientId: string,
domain: string,
tokenAudience?: TokenAudience,
environment?: Environment,
tokenCache?: TokenCache
) {
super(clientId, domain, tokenAudience, environment, tokenCache);
}
protected async getTokenFromCache(): Promise<TokenResponse> {
const self = this;
// a thin wrapper over the base implementation. try get token from cache, additionaly clean up cache if required.
try {
const tokenResponse = await super.getTokenFromCache(undefined);
return Promise.resolve(tokenResponse);
} catch (error) {
// Remove the stale token from the tokencache. ADAL gives the same error message "Entry not found in cache."
// for entry not being present in the cache and for accessToken being expired in the cache. We do not want the token cache
// to contain the expired token, we clean it up here.
const status = await self.removeInvalidItemsFromCache({
_clientId: self.clientId
});
if (status.result) {
return Promise.reject(error);
}
const message =
status && status.details && status.details.message
? status.details.message
: status.details;
return Promise.reject(
new Error(
AuthConstants.SDK_INTERNAL_ERROR +
" : " +
"critical failure while removing expired token for service principal from token cache. " +
message
)
);
}
}
/**
* Removes invalid items from token cache. This method is different. Here we never reject in case of error.
* Rather we resolve with an object that says the result is false and error information is provided in
* the details property of the resolved object. This is done to do better error handling in the above function
* where removeInvalidItemsFromCache() is called.
* @param {object} query The query to be used for finding the token for service principal from the cache
* @returns {result: boolean, details?: Error} resultObject with more info.
*/
private removeInvalidItemsFromCache(
query: object
): Promise<{ result: boolean; details?: Error }> {
const self = this;
return new Promise<{ result: boolean; details?: Error }>(resolve => {
self.tokenCache.find(query, (error: Error, entries: any[]) => {
if (error) {
return resolve({ result: false, details: error });
}
if (entries && entries.length > 0) {
return new Promise(resolve => {
return self.tokenCache.remove(entries, (err: Error) => {
if (err) {
return resolve({ result: false, details: err });
}
return resolve({ result: true });
});
});
} else {
return resolve({ result: true });
}
});
});
}
}

Просмотреть файл

@ -4,7 +4,7 @@
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
import { TokenResponse } from "adal-node";
import { TokenResponse, TokenCache } from "adal-node";
export class DeviceTokenCredentials extends TokenCredentialsBase {
@ -34,7 +34,7 @@ export class DeviceTokenCredentials extends TokenCredentialsBase {
username?: string,
tokenAudience?: TokenAudience,
environment?: Environment,
tokenCache?: any) {
tokenCache?: TokenCache) {
if (!username) {
username = "user@example.com";
@ -48,7 +48,7 @@ export class DeviceTokenCredentials extends TokenCredentialsBase {
clientId = AuthConstants.DEFAULT_ADAL_CLIENT_ID;
}
super(clientId, domain, tokenAudience, environment as any, tokenCache);
super(clientId, domain, tokenAudience, environment, tokenCache);
this.username = username;
}

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
import { ApplicationTokenCredentials } from "./applicationTokenCredentials";
import { ApplicationTokenCertificateCredentials } from "./applicationTokenCertificateCredentials";
import { DeviceTokenCredentials } from "./deviceTokenCredentials";
import { MSIAppServiceTokenCredentials } from "./msiAppServiceTokenCredentials";
import { MSITokenCredentials } from "./msiTokenCredentials";
@ -62,6 +63,9 @@ function _createAuthenticatorMapper(credentials: MSITokenCredentials): Authentic
if (credentials instanceof ApplicationTokenCredentials) {
return context.acquireTokenWithClientCredentials(
challenge.resource, credentials.clientId, credentials.secret, _formAuthorizationValue);
} else if (credentials instanceof ApplicationTokenCertificateCredentials) {
return context.acquireTokenWithClientCertificate(
challenge.resource, credentials.clientId, credentials.certificate, credentials.thumbprint, _formAuthorizationValue);
} else if (credentials instanceof UserTokenCredentials) {
return context.acquireTokenWithUsernamePassword(
challenge.resource, credentials.username, credentials.password, credentials.clientId, _formAuthorizationValue);

Просмотреть файл

@ -5,7 +5,7 @@ import { Constants as MSRestConstants, WebResource } from "@azure/ms-rest-js";
import { Environment } from "@azure/ms-rest-azure-env";
import { TokenAudience } from "../util/authConstants";
import { TokenClientCredentials } from "./tokenClientCredentials";
import { TokenResponse, AuthenticationContext, MemoryCache, ErrorResponse } from "adal-node";
import { TokenResponse, AuthenticationContext, MemoryCache, ErrorResponse, TokenCache } from "adal-node";
export abstract class TokenCredentialsBase implements TokenClientCredentials {
public readonly authContext: AuthenticationContext;
@ -14,14 +14,14 @@ export abstract class TokenCredentialsBase implements TokenClientCredentials {
public readonly clientId: string,
public domain: string,
public readonly tokenAudience?: TokenAudience,
public readonly environment = Environment.AzureCloud,
public tokenCache: any = new MemoryCache()) {
public readonly environment: Environment = Environment.AzureCloud,
public tokenCache: TokenCache = new MemoryCache()) {
if (!Boolean(clientId) || typeof clientId.valueOf() !== "string") {
if (!clientId || typeof clientId.valueOf() !== "string") {
throw new Error("clientId must be a non empty string.");
}
if (!Boolean(domain) || typeof domain.valueOf() !== "string") {
if (!domain || typeof domain.valueOf() !== "string") {
throw new Error("domain must be a non empty string.");
}
@ -39,9 +39,9 @@ export abstract class TokenCredentialsBase implements TokenClientCredentials {
if (this.tokenAudience) {
resource = this.tokenAudience;
if (this.tokenAudience.toLowerCase() === "graph") {
resource = this.environment.activeDirectoryGraphResourceId;
resource = this.environment.activeDirectoryGraphResourceId as string;
} else if (this.tokenAudience.toLowerCase() === "batch") {
resource = this.environment.batchResourceId;
resource = this.environment.batchResourceId as string;
}
}
return resource;

Просмотреть файл

@ -4,7 +4,7 @@
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { TokenAudience } from "../util/authConstants";
import { TokenResponse, ErrorResponse } from "adal-node";
import { TokenResponse, ErrorResponse, TokenCache } from "adal-node";
export class UserTokenCredentials extends TokenCredentialsBase {
@ -33,25 +33,25 @@ export class UserTokenCredentials extends TokenCredentialsBase {
password: string,
tokenAudience?: TokenAudience,
environment?: Environment,
tokenCache?: any) {
tokenCache?: TokenCache) {
if (!Boolean(clientId) || typeof clientId.valueOf() !== "string") {
if (!clientId || typeof clientId.valueOf() !== "string") {
throw new Error("clientId must be a non empty string.");
}
if (!Boolean(domain) || typeof domain.valueOf() !== "string") {
if (!domain || typeof domain.valueOf() !== "string") {
throw new Error("domain must be a non empty string.");
}
if (!Boolean(username) || typeof username.valueOf() !== "string") {
if (!username || typeof username.valueOf() !== "string") {
throw new Error("username must be a non empty string.");
}
if (!Boolean(password) || typeof password.valueOf() !== "string") {
if (!password || typeof password.valueOf() !== "string") {
throw new Error("password must be a non empty string.");
}
super(clientId, domain, tokenAudience, environment as any, tokenCache);
super(clientId, domain, tokenAudience, environment, tokenCache);
this.username = username;
this.password = password;

Просмотреть файл

@ -2,11 +2,12 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
import * as adal from "adal-node";
import * as fs from "fs";
import * as msRest from "@azure/ms-rest-js";
import { readFileSync } from "fs";
import { Environment } from "@azure/ms-rest-azure-env";
import { TokenCredentialsBase } from "./credentials/tokenCredentialsBase";
import { ApplicationTokenCredentials } from "./credentials/applicationTokenCredentials";
import { ApplicationTokenCertificateCredentials } from "./credentials/applicationTokenCertificateCredentials";
import { DeviceTokenCredentials } from "./credentials/deviceTokenCredentials";
import { UserTokenCredentials } from "./credentials/userTokenCredentials";
import { AuthConstants, TokenAudience } from "./util/authConstants";
@ -48,9 +49,9 @@ export interface AzureTokenCredentialsOptions {
*/
environment?: Environment;
/**
* @property {any} [tokenCache] - The token cache. Default value is MemoryCache from adal.
* @property {TokenCache} [tokenCache] - The token cache. Default value is MemoryCache from adal.
*/
tokenCache?: any;
tokenCache?: adal.TokenCache;
}
/**
@ -179,7 +180,8 @@ export async function withUsernamePasswordWithAuthResponse(username: string, pas
* @param {string} secret The application secret for the service principal.
* @param {string} domain The domain or tenant id containing this application.
* @param {object} [options] Object representing optional parameters.
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid value is "graph".
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [options.environment] The azure environment to authenticate with.
* @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal.
*
@ -207,6 +209,46 @@ export async function withServicePrincipalSecretWithAuthResponse(clientId: strin
return Promise.resolve({ credentials: creds, subscriptions: subscriptionList });
}
/**
* Provides an ApplicationTokenCertificateCredentials object and the list of subscriptions associated with that servicePrinicpalId/clientId across all the applicable tenants.
*
* @param {string} clientId The active directory application client id also known as the SPN (ServicePrincipal Name).
* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net}
* for an example.
* @param {string} certificateStringOrFilePath A PEM encoded certificate and private key OR an absolute filepath to the .pem file containing that information. For example:
* - CertificateString: "-----BEGIN PRIVATE KEY-----\n<xxxxx>\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\n<yyyyy>\n-----END CERTIFICATE-----\n"
* - CertificateFilePath: **Absolute** file path of the .pem file.
* @param {string} domain The domain or tenant id containing this application.
* @param {object} [options] Object representing optional parameters.
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [options.environment] The azure environment to authenticate with.
* @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal.
*
* @returns {Promise<AuthResponse>} A Promise that resolves to AuthResponse that contains "credentials" and optional "subscriptions" array and rejects with an Error.
*/
export async function withServicePrincipalCertificateWithAuthResponse(clientId: string, certificateStringOrFilePath: string, domain: string, options?: AzureTokenCredentialsOptions): Promise<AuthResponse> {
if (!options) {
options = {};
}
if (!options.environment) {
options.environment = Environment.AzureCloud;
}
let creds: ApplicationTokenCertificateCredentials;
let subscriptionList: LinkedSubscription[] = [];
try {
creds = ApplicationTokenCertificateCredentials.create(clientId, certificateStringOrFilePath, domain, options);
await creds.getToken();
// We only need to get the subscriptionList if the tokenAudience is for a management client.
if (options.tokenAudience && options.tokenAudience === options.environment.activeDirectoryResourceId) {
subscriptionList = await getSubscriptionsFromTenants(creds, [domain]);
}
} catch (err) {
return Promise.reject(err);
}
return Promise.resolve({ credentials: creds, subscriptions: subscriptionList });
}
function validateAuthFileContent(credsObj: any, filePath: string) {
if (!credsObj) {
throw new Error("Please provide a credsObj to validate.");
@ -217,8 +259,8 @@ function validateAuthFileContent(credsObj: any, filePath: string) {
if (!credsObj.clientId) {
throw new Error(`"clientId" is missing from the auth file: ${filePath}.`);
}
if (!credsObj.clientSecret) {
throw new Error(`"clientSecret" is missing from the auth file: ${filePath}.`);
if (!credsObj.clientSecret && !credsObj.clientCertificate) {
throw new Error(`Either "clientSecret" or "clientCertificate" must be present in the auth file: ${filePath}.`);
}
if (!credsObj.subscriptionId) {
throw new Error(`"subscriptionId" is missing from the auth file: ${filePath}.`);
@ -259,11 +301,12 @@ function foundManagementEndpointUrl(authFileUrl: string, envUrl: string): boolea
* If you want to create the sp for a different cloud/environment then please execute:
* 1. az cloud list
* 2. az cloud set n <name of the environment>
* 3. az ad sp create-for-rbac --sdk-auth > auth.json
*
* 3. az ad sp create-for-rbac --sdk-auth > auth.json // create sp with secret
* **OR**
* 3. az ad sp create-for-rbac --create-cert --sdk-auth > auth.json // create sp with certificate
* If the service principal is already created then login with service principal info:
* 3. az login --service-principal -u <clientId> -p <clientSecret> -t <tenantId>
* 4. az account show --sdk-auth > auth.json
* 4. az login --service-principal -u <clientId> -p <clientSecret> -t <tenantId>
* 5. az account show --sdk-auth > auth.json
*
* Authenticates using the service principal information provided in the auth file. This method will set
* the subscriptionId from the auth file to the user provided environment variable in the options
@ -287,9 +330,9 @@ export async function withAuthFileWithAuthResponse(options?: LoginWithAuthFileOp
return Promise.reject(new Error(msg));
}
let content: string, credsObj: any = {};
const optionsForSpSecret: any = {};
const optionsForSp: any = {};
try {
content = fs.readFileSync(filePath, { encoding: "utf8" });
content = readFileSync(filePath, { encoding: "utf8" });
credsObj = JSON.parse(content);
validateAuthFileContent(credsObj, filePath);
} catch (err) {
@ -317,7 +360,7 @@ export async function withAuthFileWithAuthResponse(options?: LoginWithAuthFileOp
}
}
if (envFound.name) {
optionsForSpSecret.environment = (Environment as any)[envFound.name];
optionsForSp.environment = (Environment as any)[envFound.name];
} else {
// create a new environment with provided info.
const envParams: any = {
@ -327,7 +370,7 @@ export async function withAuthFileWithAuthResponse(options?: LoginWithAuthFileOp
const keys = Object.keys(credsObj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key.match(/^(clientId|clientSecret|subscriptionId|tenantId)$/ig) === null) {
if (key.match(/^(clientId|clientSecret|clientCertificate|subscriptionId|tenantId)$/ig) === null) {
if (key === "activeDirectoryEndpointUrl" && !key.endsWith("/")) {
envParams[key] = credsObj[key] + "/";
} else {
@ -341,9 +384,13 @@ export async function withAuthFileWithAuthResponse(options?: LoginWithAuthFileOp
if (!envParams.portalUrl) {
envParams.portalUrl = "https://portal.azure.com";
}
optionsForSpSecret.environment = Environment.add(envParams);
optionsForSp.environment = Environment.add(envParams);
}
return withServicePrincipalSecretWithAuthResponse(credsObj.clientId, credsObj.clientSecret, credsObj.tenantId, optionsForSpSecret);
if (credsObj.clientSecret) {
return withServicePrincipalSecretWithAuthResponse(credsObj.clientId, credsObj.clientSecret, credsObj.tenantId, optionsForSp);
}
return withServicePrincipalCertificateWithAuthResponse(credsObj.clientId, credsObj.clientCertificate, credsObj.tenantId, optionsForSp);
}
@ -414,13 +461,13 @@ export async function withInteractiveWithAuthResponse(options?: InteractiveLogin
interactiveOptions.language = options.language;
interactiveOptions.userCodeResponseLogger = options.userCodeResponseLogger;
const authorityUrl: string = interactiveOptions.environment.activeDirectoryEndpointUrl + interactiveOptions.domain;
const authContext: any = new adal.AuthenticationContext(authorityUrl, interactiveOptions.environment.validateAuthority, interactiveOptions.tokenCache);
const authContext = new adal.AuthenticationContext(authorityUrl, interactiveOptions.environment.validateAuthority, interactiveOptions.tokenCache);
interactiveOptions.context = authContext;
let userCodeResponse: any;
let creds: DeviceTokenCredentials;
function tryAcquireToken(interactiveOptions: InteractiveLoginOptions, resolve: any, reject: any) {
authContext.acquireUserCode(interactiveOptions.tokenAudience, interactiveOptions.clientId, interactiveOptions.language, (err: any, userCodeRes: any) => {
authContext.acquireUserCode(interactiveOptions.tokenAudience!, interactiveOptions.clientId!, interactiveOptions.language!, (err: any, userCodeRes: adal.UserCodeInfo) => {
if (err) {
if (err.error === "authorization_pending") {
setTimeout(() => {
@ -482,11 +529,12 @@ export async function withInteractiveWithAuthResponse(options?: InteractiveLogin
* If you want to create the sp for a different cloud/environment then please execute:
* 1. az cloud list
* 2. az cloud set n <name of the environment>
* 3. az ad sp create-for-rbac --sdk-auth > auth.json
*
* 3. az ad sp create-for-rbac --sdk-auth > auth.json // create sp with secret
* **OR**
* 3. az ad sp create-for-rbac --create-cert --sdk-auth > auth.json // create sp with certificate
* If the service principal is already created then login with service principal info:
* 3. az login --service-principal -u <clientId> -p <clientSecret> -t <tenantId>
* 4. az account show --sdk-auth > auth.json
* 4. az login --service-principal -u <clientId> -p <clientSecret> -t <tenantId>
* 5. az account show --sdk-auth > auth.json
*
* Authenticates using the service principal information provided in the auth file. This method will set
* the subscriptionId from the auth file to the user provided environment variable in the options
@ -598,7 +646,8 @@ export function interactive(options?: InteractiveLoginOptions, callback?: { (err
* @param {string} secret The application secret for the service principal.
* @param {string} domain The domain or tenant id containing this application.
* @param {object} [options] Object representing optional parameters.
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid value is "graph".
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [options.environment] The azure environment to authenticate with.
* @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal.
* @param {function} [optionalCallback] The optional callback.
@ -639,6 +688,59 @@ export function withServicePrincipalSecret(clientId: string, secret: string, dom
}
}
/**
* Provides an ApplicationTokenCertificateCredentials object and the list of subscriptions associated with that servicePrinicpalId/clientId across all the applicable tenants.
*
* @param {string} clientId The active directory application client id also known as the SPN (ServicePrincipal Name).
* See {@link https://azure.microsoft.com/en-us/documentation/articles/active-directory-devquickstarts-dotnet/ Active Directory Quickstart for .Net}
* for an example.
* @param {string} certificateStringOrFilePath A PEM encoded certificate and private key OR an absolute filepath to the .pem file containing that information. For example:
* - CertificateString: "-----BEGIN PRIVATE KEY-----\n<xxxxx>\n-----END PRIVATE KEY-----\n-----BEGIN CERTIFICATE-----\n<yyyyy>\n-----END CERTIFICATE-----\n"
* - CertificateFilePath: **Absolute** file path of the .pem file.
* @param {string} domain The domain or tenant id containing this application.
* @param {object} [options] Object representing optional parameters.
* @param {string} [options.tokenAudience] The audience for which the token is requested. Valid values are 'graph', 'batch', or any other resource like 'https://vault.azure.com/'.
* If tokenAudience is 'graph' then domain should also be provided and its value should not be the default 'common' tenant. It must be a string (preferrably in a guid format).
* @param {Environment} [options.environment] The azure environment to authenticate with.
* @param {object} [options.tokenCache] The token cache. Default value is the MemoryCache object from adal.
* @param {function} [optionalCallback] The optional callback.
*
* @returns {function | Promise} If a callback was passed as the last parameter then it returns the callback else returns a Promise.
*
* {function} optionalCallback(err, credentials)
* {Error} [err] - The Error object if an error occurred, null otherwise.
* {ApplicationTokenCertificateCredentials} [credentials] - The ApplicationTokenCertificateCredentials object.
* {Array} [subscriptions] - List of associated subscriptions across all the applicable tenants.
* {Promise} A promise is returned.
* @resolve {ApplicationTokenCertificateCredentials} The ApplicationTokenCertificateCredentials object.
* @reject {Error} - The error object.
*/
export function withServicePrincipalCertificate(clientId: string, certificateStringOrFilePath: string, domain: string): Promise<ApplicationTokenCertificateCredentials>;
export function withServicePrincipalCertificate(clientId: string, certificateStringOrFilePath: string, domain: string, options: AzureTokenCredentialsOptions): Promise<ApplicationTokenCredentials>;
export function withServicePrincipalCertificate(clientId: string, certificateStringOrFilePath: string, domain: string, options: AzureTokenCredentialsOptions, callback: { (err: Error, credentials: ApplicationTokenCertificateCredentials, subscriptions: Array<LinkedSubscription>): void }): void;
export function withServicePrincipalCertificate(clientId: string, certificateStringOrFilePath: string, domain: string, callback: any): void;
export function withServicePrincipalCertificate(clientId: string, certificateStringOrFilePath: string, domain: string, options?: AzureTokenCredentialsOptions, callback?: { (err: Error, credentials: ApplicationTokenCertificateCredentials, subscriptions: Array<LinkedSubscription>): void }): any {
if (!callback && typeof options === "function") {
callback = options;
options = undefined;
}
const cb = callback as Function;
if (!callback) {
return withServicePrincipalCertificateWithAuthResponse(clientId, certificateStringOrFilePath, domain, options).then((authRes) => {
return Promise.resolve(authRes.credentials);
}).catch((err) => {
return Promise.reject(err);
});
} else {
msRest.promiseToCallback(withServicePrincipalCertificateWithAuthResponse(clientId, certificateStringOrFilePath, domain, options))((err: Error, authRes: AuthResponse) => {
if (err) {
return cb(err);
}
return cb(undefined, authRes.credentials, authRes.subscriptions);
});
}
}
/**
* Provides a UserTokenCredentials object and the list of subscriptions associated with that userId across all the applicable tenants.
* This method is applicable only for organizational ids that are not 2FA enabled otherwise please use interactive login.

Просмотреть файл

@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
export { ApplicationTokenCredentials } from "./credentials/applicationTokenCredentials";
export { ApplicationTokenCertificateCredentials } from "./credentials/applicationTokenCertificateCredentials";
export { DeviceTokenCredentials } from "./credentials/deviceTokenCredentials";
export { createAuthenticator } from "./credentials/keyVaultFactory";
export { MSIAppServiceOptions, MSIAppServiceTokenCredentials } from "./credentials/msiAppServiceTokenCredentials";
@ -24,4 +25,6 @@ export {
withAuthFileWithAuthResponse as loginWithAuthFileWithAuthResponse,
loginWithVmMSI,
loginWithAppServiceMSI,
withServicePrincipalCertificate as loginWithServicePrincipalCertificate,
withServicePrincipalCertificateWithAuthResponse as loginWithServicePrincipalCertificateWithAuthResponse
} from "./login";

Просмотреть файл

@ -3,7 +3,7 @@
import * as msRest from "@azure/ms-rest-js";
import { TokenCredentialsBase } from "../credentials/tokenCredentialsBase";
import { ApplicationTokenCredentials } from "../credentials/applicationTokenCredentials";
import { ApplicationTokenCredentialsBase } from "../credentials/applicationTokenCredentialsBase";
import { AuthConstants } from "../util/authConstants";
/**
@ -107,7 +107,7 @@ export async function getSubscriptionsFromTenants(credentials: TokenCredentialsB
let userType = "user";
let username: string;
const originalDomain = credentials.domain;
if (credentials instanceof ApplicationTokenCredentials) {
if (credentials instanceof ApplicationTokenCredentialsBase) {
userType = "servicePrincipal";
username = credentials.clientId;
} else {

Просмотреть файл

@ -5,21 +5,10 @@
"email": "azsdkteam@microsoft.com",
"url": "https://github.com/Azure/ms-rest-nodeauth"
},
"version": "0.9.3",
"version": "1.0.0",
"description": "Azure Authentication library in node.js with type definitions.",
"tags": [
"node",
"isomorphic",
"browser",
"azure",
"autorest",
"authentication",
"environment"
],
"keywords": [
"node",
"isomorphic",
"browser",
"azure",
"autorest",
"authentication",
@ -38,21 +27,23 @@
"tsconfig.json"
],
"dependencies": {
"@azure/ms-rest-azure-env": "^1.1.0",
"@azure/ms-rest-js": "^1.8.1",
"adal-node": "^0.1.22"
"@azure/ms-rest-azure-env": "^1.1.2",
"@azure/ms-rest-js": "^1.8.2",
"adal-node": "^0.1.28"
},
"license": "MIT",
"devDependencies": {
"@ts-common/azure-js-dev-tools": "^0.4.9",
"@types/chai": "^4.1.6",
"@types/dotenv": "^6.1.1",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.0",
"chai": "^4.2.0",
"dotenv": "^8.0.0",
"mocha": "^5.2.0",
"nock": "^10.0.1",
"npm-run-all": "^4.1.3",
"nyc": "^13.1.0",
"nyc": "^14.1.0",
"rollup": "^0.67.1",
"rollup-plugin-sourcemaps": "^0.4.2",
"ts-node": "^7.0.1",

10
sample.env Normal file
Просмотреть файл

@ -0,0 +1,10 @@
# copy the content of this file to a file named ".env". It should be stored at the root of the repo.
CLIENT_ID=
DOMAIN=
APPLICATION_SECRET=
AZURE_USERNAME=
AZURE_PASSWORD=
# Absolute file paths.
CERTIFICATE_FILE_PATH=
AUTH_FILE_CERT_PATH=
AUTH_FILE_SECRET_PATH=

Просмотреть файл

@ -0,0 +1,19 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
const authFilepath = process.env.AUTH_FILE_CERT_PATH || "";
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/authFileWithSpCert.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.loginWithAuthFileWithAuthResponse({
filePath: authFilepath
});
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

Просмотреть файл

@ -0,0 +1,19 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
const authFilepath = process.env.AUTH_FILE_SECRET_PATH || "";
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/authFileWithSpSecret.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.loginWithAuthFileWithAuthResponse({
filePath: authFilepath
});
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

15
samples/interactive.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/interactive.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.interactiveLogin();
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

Просмотреть файл

@ -0,0 +1,23 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
const clientId = process.env.CLIENT_ID || "";
const tenantId = process.env.DOMAIN || "";
const certificateFilepath = process.env.CERTIFICATE_FILE_PATH || "";
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/servicePrincipalSecret.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.loginWithServicePrincipalCertificateWithAuthResponse(
clientId,
certificateFilepath,
tenantId
);
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

Просмотреть файл

@ -0,0 +1,23 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
const clientId = process.env.CLIENT_ID || "";
const tenantId = process.env.DOMAIN || "";
const secret = process.env.APPLICATION_SECRET || "";
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/servicePrincipalSecret.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.loginWithServicePrincipalSecretWithAuthResponse(
clientId,
secret,
tenantId
);
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

Просмотреть файл

@ -0,0 +1,21 @@
import * as msRestNodeAuth from "../lib/msRestNodeAuth";
import * as dotenv from "dotenv";
dotenv.config();
const username = process.env.AZURE_USERNAME || "";
const password = process.env.AZURE_PASSWORD || "";
// copy the content of "sample.env" to a file named ".env". It should be stored at the root of the repo.
// then from the root of the cloned repo run, ts-node ./samples/usernamePassword.ts
async function main(): Promise<void> {
try {
const authres = await msRestNodeAuth.loginWithUsernamePasswordWithAuthResponse(
username,
password
);
console.log(authres);
} catch (err) {
console.log(err);
}
}
main();

Просмотреть файл

@ -35,6 +35,7 @@
],
"include": [
"./lib/**/*.ts",
"./test/**/*.ts"
"./test/**/*.ts",
"./samples/*.ts"
]
}