Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2.0.0-beta.3 (Unreleased)

- Fixed issue with the logging of success messages on the `DefaultAzureCredential` and the `ChainedTokenCredential`. These messages will now mention the internal credential that succeeded.
- The feature of persistence caching of credentials (introduced in 2.0.0-beta.1) is now supported on Node.js 15 as well.
- `AuthenticationRequiredError` (introduced in 2.0.0-beta.1) now has the same impact on `ChainedTokenCredential` as the `CredentialUnavailableError` which is to allow the next credential in the chain to be tried.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { createSpan } from "../util/tracing";
import { SpanStatusCode } from "@azure/core-tracing";
import { credentialLogger, formatSuccess, formatError } from "../util/logging";

const logger = credentialLogger("ChainedTokenCredential");
/**
* @internal
*/
export const logger = credentialLogger("ChainedTokenCredential");

/**
* Enables multiple `TokenCredential` implementations to be tried in order
Expand Down Expand Up @@ -53,13 +56,15 @@ export class ChainedTokenCredential implements TokenCredential {
*/
async getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken> {
let token = null;
let successfulCredentialName = "";
const errors = [];

const { span, updatedOptions } = createSpan("ChainedTokenCredential-getToken", options);

for (let i = 0; i < this._sources.length && token === null; i++) {
try {
token = await this._sources[i].getToken(scopes, updatedOptions);
successfulCredentialName = this._sources[i].constructor.name;
Copy link
Contributor

Choose a reason for hiding this comment

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

Imagine I made up my own credential following the TokenCredential interface but without using a class.
Then, this._sources[i].constructor would be undefined

Copy link
Contributor Author

@sadasant sadasant Apr 12, 2021

Choose a reason for hiding this comment

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

The name will be the name of the parent class, which is probably "Object" for the case you describe.

I don't think our code is thought to work at all with credentials without a proper constructor.name. There are more places where this could be an issue.

I can log an issue, what do you think?

Copy link
Contributor Author

@sadasant sadasant Apr 12, 2021

Choose a reason for hiding this comment

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

I made an issue to follow up later: #14848

} catch (err) {
if (
err.name === "CredentialUnavailableError" ||
Expand All @@ -85,7 +90,7 @@ export class ChainedTokenCredential implements TokenCredential {

span.end();

logger.getToken.info(formatSuccess(scopes));
logger.getToken.info(`Result for ${successfulCredentialName}: ${formatSuccess(scopes)}`);

if (token === null) {
throw new CredentialUnavailableError("Failed to retrieve a valid token");
Expand Down
4 changes: 3 additions & 1 deletion sdk/identity/identity/src/util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function credentialLoggerInstance(
const fullTitle = parent ? `${parent.fullTitle} ${title}` : title;

function info(message: string): void {
log.info(`${fullTitle} =>`, message);
log.info(`${fullTitle} => ${message}`);
}

return {
Expand All @@ -109,6 +109,7 @@ export function credentialLoggerInstance(
* It has all the properties of a CredentialLoggerInstance, plus other logger instances, one per method.
*/
export interface CredentialLogger extends CredentialLoggerInstance {
parent: AzureLogger;
getToken: CredentialLoggerInstance;
}

Expand All @@ -126,6 +127,7 @@ export function credentialLogger(title: string, log: AzureLogger = logger): Cred
const credLogger = credentialLoggerInstance(title, undefined, log);
return {
...credLogger,
parent: log,
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this change do? The PR description does not have anything regarding this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an internal change that allows us to test what the parent is logging.

getToken: credentialLoggerInstance("=> getToken()", credLogger, log)
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import assert from "assert";
import { ChainedTokenCredential, TokenCredential, AccessToken } from "../../../src";
import Sinon from "sinon";
import { logger as chainedTokenCredentialLogger } from "../../../src/credentials/chainedTokenCredential";

class TestMockCredential implements TokenCredential {
constructor(public returnPromise: Promise<AccessToken | null>) {}

getToken(): Promise<AccessToken | null> {
return this.returnPromise;
}
}

describe("ChainedTokenCredential", function() {
it("Logs the expected successful message", async () => {
const chainedTokenCredential = new ChainedTokenCredential(
new TestMockCredential(Promise.resolve({ token: "firstToken", expiresOnTimestamp: 0 }))
);

const infoSpy = Sinon.spy(chainedTokenCredentialLogger.parent, "info");
const getTokenInfoSpy = Sinon.spy(chainedTokenCredentialLogger.getToken, "info");

const accessToken = await chainedTokenCredential.getToken("<scope>");
assert.notStrictEqual(accessToken, null);

assert.equal(
infoSpy.getCalls()[0].args[0],
"ChainedTokenCredential => getToken() => Result for TestMockCredential: SUCCESS. Scopes: <scope>."
);
assert.equal(
getTokenInfoSpy.getCalls()[0].args[0],
"Result for TestMockCredential: SUCCESS. Scopes: <scope>."
);

infoSpy.restore();
getTokenInfoSpy.restore();
});
});