Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make token credentials compatible with @azure/core-auth's TokenCredential #66

Closed
wants to merge 12 commits into from
Closed
9 changes: 8 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Changelog

## 3.0.0 - 2019/08/02

- Token credential types are now compatible with the `TokenCredential` interface from [`@azure/core-auth`](https://github.com/Azure/azure-sdk-for-js/tree/master/sdk/core/core-auth), enabling them to be used with newer SDK libraries in `azure-sdk-for-js`.
- **Breaking change:**
- Updated min version of dependency `@azure/ms-rest-js` to `^2.0.4`.

## 2.0.4 - 2019/08/02
- Rolling back the min version of dependency `@azure/ms-rest-js` from `^2.0.3` to `^1.8.13` thereby fixing [#69](https://github.com/Azure/ms-rest-nodeauth/issues/69).

Expand All @@ -16,7 +23,7 @@

## 2.0.0 - 2019/05/20
- Added support for client_id, object_id and ms_res_id query parameters for VmMSI. Fixes [#58](https://github.com/Azure/ms-rest-nodeauth/issues/58).
- **Breaking change:**
- **Breaking change:**
- Added support to get token for a different resource like Azure Keyvault, Azure Batch, Azure Graph apart from the default Azure Resource Manager resource via `AzureCliCredentials`.
- `AzureCliCredentials.create()` now takes an optional parameter where the user can specify the subscriptionId and the resource for which the token is required.
- `AzureCliCredentials.getDefaultSubscription()` has been changed to `AzureCliCredentials.getSubscription(subscriptionIdOrName?: string)`.
Expand Down
12 changes: 9 additions & 3 deletions lib/credentials/applicationTokenCertificateCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import { readFileSync } from "fs";
import { createHash } from "crypto";
import { prepareToken } from "./coreAuthHelpers";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { ApplicationTokenCredentialsBase } from "./applicationTokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
Expand Down Expand Up @@ -52,10 +54,13 @@ export class ApplicationTokenCertificateCredentials extends ApplicationTokenCred
* 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> {
public async getToken(): Promise<TokenResponse>;
public async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
try {
const tokenResponse = await this.getTokenFromCache();
return tokenResponse;
const token = prepareToken(tokenResponse, scopes);
return token;
} catch (error) {
if (error.message.startsWith(AuthConstants.SDK_INTERNAL_ERROR)) {
return Promise.reject(error);
Expand All @@ -74,7 +79,8 @@ export class ApplicationTokenCertificateCredentials extends ApplicationTokenCred
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse as TokenResponse);
const token = prepareToken(tokenResponse as TokenResponse, scopes);
return resolve(token);
}
);
});
Expand Down
12 changes: 9 additions & 3 deletions lib/credentials/applicationTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { ApplicationTokenCredentialsBase } from "./applicationTokenCredentialsBase";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
Expand Down Expand Up @@ -42,10 +44,13 @@ export class ApplicationTokenCredentials extends ApplicationTokenCredentialsBase
* 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> {
public async getToken(): Promise<TokenResponse>;
public async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
try {
const tokenResponse = await this.getTokenFromCache();
return tokenResponse;
const token = prepareToken(tokenResponse, scopes);
return token;
} catch (error) {
if (
error.message &&
Expand All @@ -66,7 +71,8 @@ export class ApplicationTokenCredentials extends ApplicationTokenCredentialsBase
if (tokenResponse.error || tokenResponse.errorDescription) {
return reject(tokenResponse);
}
return resolve(tokenResponse as TokenResponse);
const token = prepareToken(tokenResponse as TokenResponse, scopes);
return resolve(token);
}
);
});
Expand Down
14 changes: 10 additions & 4 deletions lib/credentials/azureCliCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { TokenCredential, AccessToken, GetTokenOptions } from "@azure/core-auth";
import { Constants as MSRestConstants, WebResource } from "@azure/ms-rest-js";
import { TokenClientCredentials, TokenResponse } from "./tokenClientCredentials";
import { LinkedSubscription } from "../subscriptionManagement/subscriptionUtils";
Expand Down Expand Up @@ -76,7 +78,7 @@ export interface AccessTokenOptions {
/**
* Describes the credentials by retrieving token via Azure CLI.
*/
export class AzureCliCredentials implements TokenClientCredentials {
export class AzureCliCredentials implements TokenClientCredentials, TokenCredential {
/**
* Provides information about the default/current subscription for Azure CLI.
*/
Expand Down Expand Up @@ -120,7 +122,9 @@ export class AzureCliCredentials implements TokenClientCredentials {
* changed else uses the cached accessToken.
* @return The tokenResponse (tokenType and accessToken are the two important properties).
*/
public async getToken(): Promise<TokenResponse> {
public async getToken(): Promise<TokenResponse>;
public async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
if (this._hasTokenExpired() || this._hasSubscriptionChanged() || this._hasResourceChanged()) {
try {
// refresh the access token
Expand All @@ -143,7 +147,9 @@ export class AzureCliCredentials implements TokenClientCredentials {
expiresOn: this.tokenInfo.expiresOn,
tenantId: this.tokenInfo.tenant
};
return result;

const token = prepareToken(result, scopes);
return token;
}

/**
Expand Down Expand Up @@ -325,4 +331,4 @@ export class AzureCliCredentials implements TokenClientCredentials {
]);
return new AzureCliCredentials(subscriptinInfo, accessToken, options.resource);
}
}
}
36 changes: 36 additions & 0 deletions lib/credentials/coreAuthHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AccessToken } from "@azure/core-auth";

interface TokenResponseLike {
accessToken: string;
expiresOn?: Date | string;
}

/**
* Prepares a TokenResponse to be returned as a TokenResponse or
* a @azure/core-auth AccessToken depending on whether the 'scopes'
* parameter is null or not (the key to determining which getToken
* method has been called).
*/
export function prepareToken<T extends TokenResponseLike>(
token: T,
scopes: string | string[] | undefined): T | AccessToken {
// Scopes will contain _some_ value if a parameter was passed to getToken
if (scopes !== undefined) {
// Start with a default 'expiresOn' and then replace with
// the actual 'expiresOn' if one is given
let expiresOnTimestamp: number = Date.now() + 60 * 60 * 1000;
if (token.expiresOn) {
expiresOnTimestamp =
typeof token.expiresOn === "string"
Copy link
Contributor

Choose a reason for hiding this comment

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

This assumes that expiresOn exists on all the credential types in ms-rest-nodeauth.
Are we sure about this assumption?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I can't find expiresOn being assigned anywhere in a few of the credential implementations. @amarzavery, do you remember how users are supposed to deal with token expiration in this library? I only see AzureCliCredentials implementing any kind of token refresh logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

adal-node has an in memory token cache. It gets a new token if it is about to get expired or gives the token from the token cache.

For AzureCliCredentials we are calling the cli to authenticate and give us the new token that will be valid for the next one hour. Cli knows how to refresh the token if it is using it internally. We can't be calling the Cli for every request, as that will terribly slow down the sdk (spawning a new process and asking the Cli for creds).

Thus we try to do the same thing that adal-python would do anyways (if we are within the token refresh window (last 5 mins before the token expiry) then ask the Cli for token, it will ask adal-python which will then get the new token that will be valid for the next one hour).

Thus we need to check the expiresOn property.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've loosened the expectation of an expiresOn property on the token and put a default expiration timestamp of an hour if one is not provided. What do you think?

? Date.parse(token.expiresOn)
: token.expiresOn.getTime();
}

return {
token: token.accessToken,
expiresOnTimestamp
} as AccessToken;
} else {
return token;
}
}
13 changes: 9 additions & 4 deletions lib/credentials/deviceTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { TokenCredential, AccessToken, GetTokenOptions } from "@azure/core-auth";
import { Environment } from "@azure/ms-rest-azure-env";
import { AuthConstants, TokenAudience } from "../util/authConstants";
import { TokenResponse, TokenCache } from "adal-node";

export class DeviceTokenCredentials extends TokenCredentialsBase {
export class DeviceTokenCredentials extends TokenCredentialsBase implements TokenCredential {

readonly username: string;

Expand Down Expand Up @@ -53,8 +55,11 @@ export class DeviceTokenCredentials extends TokenCredentialsBase {
this.username = username;
}

public getToken(): Promise<TokenResponse> {
public async getToken(): Promise<TokenResponse>;
public async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing async keywords on the overloads

// For device auth, this is just getTokenFromCache.
return this.getTokenFromCache(this.username);
const token = prepareToken(await this.getTokenFromCache(this.username), scopes);
return token;
}
}
}
9 changes: 7 additions & 2 deletions lib/credentials/msiAppServiceTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { MSITokenCredentials, MSIOptions, MSITokenResponse } from "./msiTokenCredentials";
import { HttpOperationResponse, RequestPrepareOptions, WebResource } from "@azure/ms-rest-js";

Expand Down Expand Up @@ -91,7 +93,9 @@ export class MSIAppServiceTokenCredentials extends MSITokenCredentials {
* Prepares and sends a GET request to a service endpoint indicated by the app service, which responds with the access token.
* @return {Promise<MSITokenResponse>} Promise with the tokenResponse (tokenType and accessToken are the two important properties).
*/
async getToken(): Promise<MSITokenResponse> {
async getToken(): Promise<MSITokenResponse>;
async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
async getToken(scopes?: string | string[]): Promise<MSITokenResponse | AccessToken> {
const reqOptions = this.prepareRequestOptions();
let opRes: HttpOperationResponse;
let result: MSITokenResponse;
Expand All @@ -108,7 +112,8 @@ export class MSIAppServiceTokenCredentials extends MSITokenCredentials {
throw new Error(`Invalid token response, did not find accessToken. Response body is: ${opRes.bodyAsText}`);
}

return result;
const token = prepareToken(result, scopes);
return token;
}

protected prepareRequestOptions(): WebResource {
Expand Down
7 changes: 5 additions & 2 deletions lib/credentials/msiTokenCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import { Constants, WebResource, HttpClient, DefaultHttpClient } from "@azure/ms-rest-js";
import { TokenCredential, AccessToken, GetTokenOptions } from "@azure/core-auth";
import { TokenClientCredentials, TokenResponse } from "./tokenClientCredentials";
import { AuthConstants } from "../util/authConstants";

Expand Down Expand Up @@ -41,7 +42,7 @@ export interface MSITokenResponse extends TokenResponse {
* @class MSITokenCredentials - Provides information about managed service identity token credentials.
* This object can only be used to acquire token on a virtual machine provisioned in Azure with managed service identity.
*/
export abstract class MSITokenCredentials implements TokenClientCredentials {
export abstract class MSITokenCredentials implements TokenClientCredentials, TokenCredential {
/**
* Azure resource endpoints.
* - Defaults to Azure Resource Manager from environment: AzureCloud. "https://management.azure.com/"
Expand Down Expand Up @@ -133,6 +134,8 @@ export abstract class MSITokenCredentials implements TokenClientCredentials {
* @return {Promise<MSITokenResponse>} Promise with the token response.
*/
abstract async getToken(): Promise<MSITokenResponse>;
abstract async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
abstract async getToken(scopes?: string | string[]): Promise<MSITokenResponse | AccessToken>;

protected abstract prepareRequestOptions(): WebResource;

Expand All @@ -147,4 +150,4 @@ export abstract class MSITokenCredentials implements TokenClientCredentials {
webResource.headers.set(Constants.HeaderConstants.AUTHORIZATION, `${tokenResponse.tokenType} ${tokenResponse.accessToken}`);
return Promise.resolve(webResource);
}
}
}
10 changes: 7 additions & 3 deletions lib/credentials/msiVmTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { MSITokenCredentials, MSIOptions, MSITokenResponse } from "./msiTokenCredentials";
import { RequestPrepareOptions, HttpOperationResponse, WebResource, URLBuilder, HttpMethods } from "@azure/ms-rest-js";

Expand Down Expand Up @@ -91,7 +93,9 @@ export class MSIVmTokenCredentials extends MSITokenCredentials {
* Prepares and sends a POST request to a service endpoint hosted on the Azure VM, which responds with the access token.
* @return {Promise<MSITokenResponse>} Promise with the tokenResponse (tokenType and accessToken are the two important properties).
*/
async getToken(): Promise<MSITokenResponse> {
async getToken(): Promise<MSITokenResponse>;
async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
async getToken(scopes?: string | string[]): Promise<MSITokenResponse | AccessToken> {
const reqOptions = this.prepareRequestOptions();
let opRes: HttpOperationResponse;
let result: MSITokenResponse;
Expand All @@ -104,8 +108,8 @@ export class MSIVmTokenCredentials extends MSITokenCredentials {
throw new Error(`Invalid token response, did not find accessToken. Response body is: ${opRes.bodyAsText}`);
}


return result;
const token = prepareToken(result, scopes);
return token;
}

protected prepareRequestOptions(): WebResource {
Expand Down
9 changes: 6 additions & 3 deletions lib/credentials/tokenCredentialsBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import { Constants as MSRestConstants, WebResource } from "@azure/ms-rest-js";
import { TokenCredential, AccessToken, GetTokenOptions } from "@azure/core-auth";
import { Environment } from "@azure/ms-rest-azure-env";
import { TokenAudience } from "../util/authConstants";
import { TokenClientCredentials } from "./tokenClientCredentials";
import { TokenResponse, AuthenticationContext, MemoryCache, ErrorResponse, TokenCache } from "adal-node";

export abstract class TokenCredentialsBase implements TokenClientCredentials {
export abstract class TokenCredentialsBase implements TokenClientCredentials, TokenCredential {
public readonly authContext: AuthenticationContext;

public constructor(
Expand Down Expand Up @@ -72,7 +73,9 @@ export abstract class TokenCredentialsBase implements TokenClientCredentials {
* {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
* @memberof TokenCredentialsBase
*/
public async abstract getToken(): Promise<TokenResponse>;
public abstract async getToken(): Promise<TokenResponse>;
public abstract async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public abstract async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken>;

/**
* Signs a request with the Authentication header.
Expand All @@ -86,4 +89,4 @@ export abstract class TokenCredentialsBase implements TokenClientCredentials {
webResource.headers.set(MSRestConstants.HeaderConstants.AUTHORIZATION, `${tokenResponse.tokenType} ${tokenResponse.accessToken}`);
return Promise.resolve(webResource);
}
}
}
16 changes: 11 additions & 5 deletions lib/credentials/userTokenCredentials.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { prepareToken } from "./coreAuthHelpers";
import { TokenCredentialsBase } from "./tokenCredentialsBase";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { Environment } from "@azure/ms-rest-azure-env";
import { TokenAudience } from "../util/authConstants";
import { TokenResponse, ErrorResponse, TokenCache } from "adal-node";
Expand Down Expand Up @@ -69,14 +71,17 @@ export class UserTokenCredentials extends TokenCredentialsBase {
* {object} [tokenResponse] The tokenResponse (tokenType and accessToken are the two important properties).
* @memberof UserTokenCredentials
*/
public async getToken(): Promise<TokenResponse> {
public async getToken(): Promise<TokenResponse>;
public async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
public async getToken(scopes?: string | string[]): Promise<TokenResponse | AccessToken> {
try {
return await this.getTokenFromCache(this.username);
const token = prepareToken(await this.getTokenFromCache(this.username), scopes);
return token;
} catch (error) {
const self = this;
const resource = this.getActiveDirectoryResourceId();

return new Promise<TokenResponse>((resolve, reject) => {
return new Promise<TokenResponse | AccessToken>((resolve, reject) => {
self.authContext.acquireTokenWithUsernamePassword(resource, self.username, self.password, self.clientId,
(error: Error, tokenResponse: TokenResponse | ErrorResponse) => {
if (error) {
Expand All @@ -89,12 +94,13 @@ export class UserTokenCredentials extends TokenCredentialsBase {

tokenResponse = tokenResponse as TokenResponse;
if (self.crossCheckUserNameWithToken(self.username, tokenResponse.userId!)) {
return resolve((tokenResponse as TokenResponse));
const token = prepareToken(tokenResponse as TokenResponse, scopes);
return resolve(token);
} else {
return reject(`The userId "${tokenResponse.userId}" in access token doesn"t match the username "${self.username}" provided during authentication.`);
}
});
});
}
}
}
}
Loading