diff --git a/sdk/identity/identity/src/credentials/authFileCredential.browser.ts b/sdk/identity/identity/src/credentials/authFileCredential.browser.ts new file mode 100644 index 000000000000..1beddee741e7 --- /dev/null +++ b/sdk/identity/identity/src/credentials/authFileCredential.browser.ts @@ -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 AuthFileCredential implements TokenCredential { + constructor(options?: TokenCredentialOptions) { + throw BrowserNotSupportedError; + } + + getToken(scopes: string | string[], options?: GetTokenOptions): Promise { + throw BrowserNotSupportedError; + } +} diff --git a/sdk/identity/identity/src/credentials/authFileCredential.ts b/sdk/identity/identity/src/credentials/authFileCredential.ts new file mode 100644 index 000000000000..586b332954f8 --- /dev/null +++ b/sdk/identity/identity/src/credentials/authFileCredential.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http"; +import { createSpan } from "../util/tracing"; +import { AuthenticationErrorName } from "../client/errors"; +import { CanonicalCode } from "@opentelemetry/types"; +import { ClientCertificateCredential } from "./clientCertificateCredential"; +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 filePath: string; + /** + * 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) { + this.filePath = filePath; + } + + /** + * 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 { + const { span, options: newOptions } = createSpan("authFileCredential-getToken", options); + if (this.credential) { + try { + await this.ensureCredential(); + return await this.credential.getToken(scopes, newOptions); + } catch (err) { + const code = + err.name === AuthenticationErrorName + ? CanonicalCode.UNAUTHENTICATED + : CanonicalCode.UNKNOWN; + span.setStatus({ + code, + message: err.message + }); + return null; + } finally { + span.end(); + } + } + + // 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() { + if (this.credential == null) { + try { + let authData = JSON.parse(fs.readFileSync(this.filePath).toString()); + await this.buildCredentialForCredentialFile(authData); + } catch (err) { + throw new Error("error parsing SDK Auth File"); + } + } + } + + async buildCredentialForCredentialFile(authData: any) { + if (this.credential == null && typeof authData == "object") { + let clientId = authData["clientId"]; + let certificatePath = authData["certificatePath"]; + let tenantId = authData["tenantId"]; + let activeDirectoryEndpointUrl = authData["activeDirectoryEndpointUrl"]; + + if ( + clientId == undefined || + certificatePath == undefined || + tenantId == undefined || + activeDirectoryEndpointUrl == undefined + ) { + throw new Error("there was a problem building the credential."); + } + + this.credential = new ClientCertificateCredential(tenantId, clientId, certificatePath, { + authorityHost: activeDirectoryEndpointUrl + }); + } + } +} diff --git a/sdk/identity/identity/src/credentials/environmentCredential.ts b/sdk/identity/identity/src/credentials/environmentCredential.ts index e9966e1bf1bc..aedec16b2b03 100644 --- a/sdk/identity/identity/src/credentials/environmentCredential.ts +++ b/sdk/identity/identity/src/credentials/environmentCredential.ts @@ -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 @@ -96,6 +97,11 @@ export class EnvironmentCredential implements TokenCredential { options ); } + + const sdkAuthLocation = process.env.SdkAuthLocation; + if (sdkAuthLocation) { + this._credential = new AuthFileCredential(sdkAuthLocation); + } } /** diff --git a/sdk/identity/identity/src/index.ts b/sdk/identity/identity/src/index.ts index 35976a2e6a27..52425de5ba06 100644 --- a/sdk/identity/identity/src/index.ts +++ b/sdk/identity/identity/src/index.ts @@ -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"; diff --git a/sdk/identity/identity/test/node/authFileCredential.spec.ts b/sdk/identity/identity/test/node/authFileCredential.spec.ts new file mode 100644 index 000000000000..01a1283288c1 --- /dev/null +++ b/sdk/identity/identity/test/node/authFileCredential.spec.ts @@ -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"); + } + }); +});