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

Add AuthFileCredential and its unit test to identity #7031

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/* eslint-disable @typescript-eslint/no-unused-vars */

import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http";
import { TokenCredentialOptions } from "../client/identityClient";

const BrowserNotSupportedError = new Error("AuthFileCredential is not supported in the browser.");

export class AuthFileCliCredential implements TokenCredential {
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
constructor(options?: TokenCredentialOptions) {
throw BrowserNotSupportedError;
}

getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null> {
throw BrowserNotSupportedError;
}
}
112 changes: 112 additions & 0 deletions sdk/identity/identity/src/credentials/authFileCredential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http";
import { TokenCredentialOptions, IdentityClient } from "../client/identityClient";
import { createSpan } from "../util/tracing";
import { AuthenticationErrorName } from "../client/errors";
import { CanonicalCode } from "@opentelemetry/types";
import { ClientSecretCredential } from "./clientSecretCredential";
import * as fs from "fs";

/**
* Enables authentication to Azure Active Directory using client secret
* details configured in the following environment variables:
*
* - AZURE_TENANT_ID: The Azure Active Directory tenant (directory) ID.
* - AZURE_CLIENT_ID: The client (application) ID of an App Registration in the tenant.
* - AZURE_CLIENT_SECRET: A client secret that was generated for the App Registration.
*
* This credential ultimately uses a {@link ClientSecretCredential} to
* perform the authentication using these details. Please consult the
* documentation of that class for more details.
*/
export class AuthFileCredential implements TokenCredential {
private credential?: TokenCredential = undefined;
private identityClient: IdentityClient;
private filePath;
/**
* Creates an instance of the authFileCredential class.
*
* @param filePath The path to the SDK Auth file.
* @param options Options for configuring the client which makes the authentication request.
*/
constructor(filePath: string);
constructor(filePath: string, options?: TokenCredentialOptions) {
this.filePath = filePath;
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
this.identityClient = new IdentityClient(options);
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Authenticates with Azure Active Directory and returns an access token if
* successful. If authentication cannot be performed at this time, this method may
* return null.
*
* @param scopes The list of scopes for which the token will have access.
* @param options The options used to configure any requests this
* TokenCredential implementation might make.
*/
async getToken(
scopes: string | string[],
options?: GetTokenOptions
): Promise<AccessToken | null> {
await this.ensureCredential();
const { span, options: newOptions } = createSpan("authFileCredential-getToken", options);
if (this.credential) {
try {
return await this.credential.getToken(scopes, newOptions);
} catch (err) {
const code =
err.name === AuthenticationErrorName
? CanonicalCode.UNAUTHENTICATED
: CanonicalCode.UNKNOWN;
span.setStatus({
code,
message: err.message
});
throw err;
} finally {
span.end();
}
}
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved

// If by this point we don't have a credential, throw an exception so that
// the user knows the credential was not configured appropriately
span.setStatus({ code: CanonicalCode.UNAUTHENTICATED });
span.end();
}

async ensureCredential() {
try {
if (this.credential == null) {
this.credential = await this.BuildCredentialForCredentialsFile(
JSON.parse(fs.readFileSync(this.filePath).toString())
);
}
} catch (err) {
throw new Error("Error parsing SDK Auth File");
}
}

async BuildCredentialForCredentialsFile(authData: Array<any>) {
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
let clientId = authData["clientId"];
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
let clientSecret = authData["clientSecret"];
let tenantId = authData["tenantId"];
let activeDirectoryEndpointUrl = authData["activeDirectoryEndpointUrl"];

if (
clientId == null ||
clientSecret == null ||
tenantId == null ||
activeDirectoryEndpointUrl == null
) {
throw new Error(
"Malformed Azure SDK Auth file. The file should contain 'clientId', 'clientSecret', 'tenentId' and 'activeDirectoryEndpointUrl' values."
);
}

return new ClientSecretCredential(tenantId, clientId, clientSecret, {
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
authorityHost: activeDirectoryEndpointUrl
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ClientSecretCredential implements TokenCredential {
grant_type: "client_credentials",
client_id: this.clientId,
client_secret: this.clientSecret,
scope: typeof scopes === "string" ? scopes : scopes.join(" ")
scope: (typeof scopes === "string" ? scopes : scopes.join(" ")).replace(/\/.default$/, "")
zzhxiaofeng marked this conversation as resolved.
Show resolved Hide resolved
}),
headers: {
Accept: "application/json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CanonicalCode } from "@opentelemetry/types";
import { logger } from "../util/logging";
import { ClientCertificateCredential } from "./clientCertificateCredential";
import { UsernamePasswordCredential } from "./usernamePasswordCredential";
import { AuthFileCredential } from "./authFileCredential";

/**
* Contains the list of all supported environment variable names so that an
Expand Down Expand Up @@ -96,6 +97,11 @@ export class EnvironmentCredential implements TokenCredential {
options
);
}

const sdkAuthLocation = process.env.SdkAuthLocation;
if (sdkAuthLocation) {
this._credential = new AuthFileCredential(sdkAuthLocation);
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/identity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DefaultAzureCredential } from "./credentials/defaultAzureCredential";
export { ChainedTokenCredential } from "./credentials/chainedTokenCredential";
export { TokenCredentialOptions } from "./client/identityClient";
export { EnvironmentCredential } from "./credentials/environmentCredential";
export { AuthFileCredential } from "./credentials/authFileCredential";
export { ClientSecretCredential } from "./credentials/clientSecretCredential";
export { ClientCertificateCredential } from "./credentials/clientCertificateCredential";
export { InteractiveBrowserCredential } from "./credentials/interactiveBrowserCredential";
Expand Down
16 changes: 16 additions & 0 deletions sdk/identity/identity/test/node/authFileCredential.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import assert from "assert";
import { AuthFileCredential } from "../../src";

describe("throw an error when use the incorrect file path ", function() {
it("use incorrect file path will throw an error when getting token", async () => {
let credential = new AuthFileCredential("Bougs*File*Path");
try {
await credential.getToken("https://mock.scope/.default/");
} catch (error) {
assert.equal(error.message, "Error parsing SDK Auth File");
}
});
});