[Identity] Simple OnBehalfOfCredential (#17137)
* WIP * added test file and another small test-related fix * fixed test after things were up to date * accepting certificate paths * feedback from Jeff * small improvements * moved sendCertificateChain to the OnBehalfOfCredentialCertificateConfiguration * inheritance improvements, by Jeff * small fixes
This commit is contained in:
Родитель
01e60581d4
Коммит
35f3eddd60
|
@ -4,10 +4,13 @@
|
|||
|
||||
### Features Added
|
||||
|
||||
- Added the `OnBehalfOfCredential`, which allows users to authenticate through the [On-Behalf-Of authentication flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow).
|
||||
- `ManagedIdentityCredential` now supports token exchange authentication.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- `ClientCertificateCredential` now evaluates the validity of the PEM certificate path on `getToken` and not on the constructor.
|
||||
|
||||
#### Breaking Changes from 2.0.0-beta.5
|
||||
|
||||
- The property named `selectedCredential` that was added to `ChainedTokenCredential` and `DefaultAzureCredential` has been removed, since customers reported that logging was enough.
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"./dist-esm/src/credentials/usernamePasswordCredential.js": "./dist-esm/src/credentials/usernamePasswordCredential.browser.js",
|
||||
"./dist-esm/src/credentials/azurePowerShellCredential.js": "./dist-esm/src/credentials/azurePowerShellCredential.browser.js",
|
||||
"./dist-esm/src/credentials/applicationCredential.js": "./dist-esm/src/credentials/applicationCredential.browser.js",
|
||||
"./dist-esm/src/credentials/onBehalfOfCredential.js": "./dist-esm/src/credentials/onBehalfOfCredential.browser.js",
|
||||
"./dist-esm/src/util/authHostEnv.js": "./dist-esm/src/util/authHostEnv.browser.js",
|
||||
"./dist-esm/src/tokenCache/TokenCachePersistence.js": "./dist-esm/src/tokenCache/TokenCachePersistence.browser.js",
|
||||
"./dist-esm/src/extensions/consumer.js": "./dist-esm/src/extensions/consumer.browser.js",
|
||||
|
|
|
@ -248,6 +248,33 @@ export class ManagedIdentityCredential implements TokenCredential {
|
|||
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export class OnBehalfOfCredential implements TokenCredential {
|
||||
constructor(configuration: OnBehalfOfCredentialSecretConfiguration | OnBehalfOfCredentialCertificateConfiguration, options?: OnBehalfOfCredentialOptions);
|
||||
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OnBehalfOfCredentialCertificateConfiguration {
|
||||
certificatePath: string;
|
||||
clientId: string;
|
||||
sendCertificateChain?: boolean;
|
||||
tenantId: string;
|
||||
userAssertionToken: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OnBehalfOfCredentialOptions extends TokenCredentialOptions, CredentialPersistenceOptions {
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface OnBehalfOfCredentialSecretConfiguration {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
tenantId: string;
|
||||
userAssertionToken: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export enum RegionalAuthority {
|
||||
AsiaEast = "eastasia",
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { TokenCredential, AccessToken } from "@azure/core-auth";
|
||||
import { credentialLogger, formatError } from "../util/logging";
|
||||
|
||||
const credentialName = "OnBehalfOfCredential";
|
||||
const BrowserNotSupportedError = new Error(`${credentialName}: Not supported in the browser.`);
|
||||
const logger = credentialLogger(credentialName);
|
||||
|
||||
export class OnBehalfOfCredential implements TokenCredential {
|
||||
constructor() {
|
||||
logger.info(formatError("", BrowserNotSupportedError));
|
||||
throw BrowserNotSupportedError;
|
||||
}
|
||||
|
||||
public getToken(): Promise<AccessToken | null> {
|
||||
logger.getToken.info(formatError("", BrowserNotSupportedError));
|
||||
throw BrowserNotSupportedError;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth";
|
||||
|
||||
import { MsalOnBehalfOf } from "../msal/nodeFlows/msalOnBehalfOf";
|
||||
import { credentialLogger } from "../util/logging";
|
||||
import { trace } from "../util/tracing";
|
||||
import { MsalFlow } from "../msal/flows";
|
||||
import { OnBehalfOfCredentialOptions } from "./onBehalfOfCredentialOptions";
|
||||
|
||||
const credentialName = "OnBehalfOfCredential";
|
||||
const logger = credentialLogger(credentialName);
|
||||
|
||||
/**
|
||||
* Defines the configuration parameters to authenticate the {@link OnBehalfOfCredential} with a secret.
|
||||
*/
|
||||
export interface OnBehalfOfCredentialSecretConfiguration {
|
||||
/**
|
||||
* The Azure Active Directory tenant (directory) ID.
|
||||
*/
|
||||
tenantId: string;
|
||||
/**
|
||||
* The client (application) ID of an App Registration in the tenant.
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* A client secret that was generated for the App Registration.
|
||||
*/
|
||||
clientSecret: string;
|
||||
/**
|
||||
* The user assertion for the On-Behalf-Of flow.
|
||||
*/
|
||||
userAssertionToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the configuration parameters to authenticate the {@link OnBehalfOfCredential} with a certificate.
|
||||
*/
|
||||
export interface OnBehalfOfCredentialCertificateConfiguration {
|
||||
/**
|
||||
* The Azure Active Directory tenant (directory) ID.
|
||||
*/
|
||||
tenantId: string;
|
||||
/**
|
||||
* The client (application) ID of an App Registration in the tenant.
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* The path to a PEM-encoded public/private key certificate on the filesystem.
|
||||
*/
|
||||
certificatePath: string;
|
||||
/**
|
||||
* Option to include x5c header for SubjectName and Issuer name authorization.
|
||||
* Set this option to send base64 encoded public certificate in the client assertion header as an x5c claim
|
||||
*/
|
||||
sendCertificateChain?: boolean;
|
||||
/**
|
||||
* The user assertion for the On-Behalf-Of flow.
|
||||
*/
|
||||
userAssertionToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables authentication to Azure Active Directory using the [On Behalf Of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow).
|
||||
*/
|
||||
export class OnBehalfOfCredential implements TokenCredential {
|
||||
private msalFlow: MsalFlow;
|
||||
|
||||
/**
|
||||
* Creates an instance of the {@link OnBehalfOfCredential} with the details
|
||||
* needed to authenticate against Azure Active Directory with a client
|
||||
* secret or a path to a PEM certificate, and an user assertion.
|
||||
*
|
||||
* Example using the `KeyClient` from [\@azure/keyvault-keys](https://www.npmjs.com/package/\@azure/keyvault-keys):
|
||||
*
|
||||
* ```ts
|
||||
* const tokenCredential = new OnBehalfOfCredential({
|
||||
* tenantId,
|
||||
* clientId,
|
||||
* clientSecret, // or `certificatePath: "/path/to/certificate.pem"
|
||||
* userAssertionToken: "access-token"
|
||||
* });
|
||||
* const client = new KeyClient("vault-url", tokenCredential);
|
||||
*
|
||||
* await client.getKey("key-name");
|
||||
* ```
|
||||
*
|
||||
* @param configuration - Configuration specific to this credential.
|
||||
* @param options - Optional parameters, generally common across credentials.
|
||||
*/
|
||||
constructor(
|
||||
private configuration:
|
||||
| OnBehalfOfCredentialSecretConfiguration
|
||||
| OnBehalfOfCredentialCertificateConfiguration,
|
||||
private options: OnBehalfOfCredentialOptions = {}
|
||||
) {
|
||||
const { tenantId, clientId, userAssertionToken } = configuration;
|
||||
const secretConfiguration = configuration as OnBehalfOfCredentialSecretConfiguration;
|
||||
const certificateConfiguration = configuration as OnBehalfOfCredentialCertificateConfiguration;
|
||||
if (
|
||||
!tenantId ||
|
||||
!clientId ||
|
||||
!(secretConfiguration.clientSecret || certificateConfiguration.certificatePath) ||
|
||||
!userAssertionToken
|
||||
) {
|
||||
throw new Error(
|
||||
`${credentialName}: tenantId, clientId, clientSecret (or certificatePath) and userAssertionToken are required parameters.`
|
||||
);
|
||||
}
|
||||
this.msalFlow = new MsalOnBehalfOf({
|
||||
...this.options,
|
||||
...this.configuration,
|
||||
logger,
|
||||
tokenCredentialOptions: this.options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Azure Active Directory and returns an access token if successful.
|
||||
* If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure.
|
||||
*
|
||||
* @param scopes - The list of scopes for which the token will have access.
|
||||
* @param options - The options used to configure the underlying network requests.
|
||||
*/
|
||||
async getToken(scopes: string | string[], options: GetTokenOptions = {}): Promise<AccessToken> {
|
||||
return trace(`${credentialName}.getToken`, options, async (newOptions) => {
|
||||
const arrayScopes = Array.isArray(scopes) ? scopes : [scopes];
|
||||
return this.msalFlow!.getToken(arrayScopes, newOptions);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { TokenCredentialOptions } from "../client/identityClient";
|
||||
import { CredentialPersistenceOptions } from "./credentialPersistenceOptions";
|
||||
|
||||
/**
|
||||
* Optional parameters for the {@link OnBehalfOfCredential} class.
|
||||
*/
|
||||
export interface OnBehalfOfCredentialOptions
|
||||
extends TokenCredentialOptions,
|
||||
CredentialPersistenceOptions {}
|
|
@ -59,6 +59,13 @@ export {
|
|||
VisualStudioCodeCredentialOptions
|
||||
} from "./credentials/visualStudioCodeCredential";
|
||||
|
||||
export {
|
||||
OnBehalfOfCredential,
|
||||
OnBehalfOfCredentialSecretConfiguration,
|
||||
OnBehalfOfCredentialCertificateConfiguration
|
||||
} from "./credentials/onBehalfOfCredential";
|
||||
export { OnBehalfOfCredentialOptions } from "./credentials/onBehalfOfCredentialOptions";
|
||||
|
||||
export { TokenCachePersistenceOptions } from "./msal/nodeFlows/tokenCachePersistenceOptions";
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { readFile } from "fs";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import { promisify } from "util";
|
||||
import { AccessToken } from "@azure/core-auth";
|
||||
|
||||
import { MsalNodeOptions, MsalNode } from "./nodeCommon";
|
||||
import { formatError } from "../../util/logging";
|
||||
import { CredentialFlowGetTokenOptions } from "../credentials";
|
||||
|
||||
const readFileAsync = promisify(readFile);
|
||||
|
||||
/**
|
||||
* Options that can be passed to configure MSAL to handle client certificates.
|
||||
* @internal
|
||||
*/
|
||||
export interface MSALClientCertificateOptions extends MsalNodeOptions {
|
||||
/**
|
||||
* Location of the certificate.
|
||||
* Location of the PEM certificate.
|
||||
*/
|
||||
certificatePath: string;
|
||||
/**
|
||||
|
@ -45,60 +47,78 @@ interface CertificateParts {
|
|||
x5c: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to asynchronously load a certificate from the given path.
|
||||
*
|
||||
* @param certificatePath - Path to the certificate.
|
||||
* @param sendCertificateChain - Option to include x5c header for SubjectName and Issuer name authorization.
|
||||
* @returns - The certificate parts, or `undefined` if the certificate could not be loaded.
|
||||
* @internal
|
||||
*/
|
||||
export async function parseCertificate(
|
||||
certificatePath: string,
|
||||
sendCertificateChain?: boolean
|
||||
): Promise<CertificateParts> {
|
||||
const certificateParts: Partial<CertificateParts> = {};
|
||||
|
||||
certificateParts.certificateContents = await readFileAsync(certificatePath, "utf8");
|
||||
if (sendCertificateChain) {
|
||||
certificateParts.x5c = certificateParts.certificateContents;
|
||||
}
|
||||
|
||||
const certificatePattern = /(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?|\r\n?)(-+END CERTIFICATE-+)/g;
|
||||
const publicKeys: string[] = [];
|
||||
|
||||
// Match all possible certificates, in the order they are in the file. These will form the chain that is used for x5c
|
||||
let match;
|
||||
do {
|
||||
match = certificatePattern.exec(certificateParts.certificateContents);
|
||||
if (match) {
|
||||
publicKeys.push(match[3]);
|
||||
}
|
||||
} while (match);
|
||||
|
||||
if (publicKeys.length === 0) {
|
||||
throw new Error("The file at the specified path does not contain a PEM-encoded certificate.");
|
||||
}
|
||||
|
||||
certificateParts.thumbprint = createHash("sha1")
|
||||
.update(Buffer.from(publicKeys[0], "base64"))
|
||||
.digest("hex")
|
||||
.toUpperCase();
|
||||
|
||||
return certificateParts as CertificateParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSAL client certificate client. Calls to MSAL's confidential application's `acquireTokenByClientCredential` during `doGetToken`.
|
||||
* @internal
|
||||
*/
|
||||
export class MsalClientCertificate extends MsalNode {
|
||||
private certificatePath: string;
|
||||
private sendCertificateChain?: boolean;
|
||||
|
||||
constructor(options: MSALClientCertificateOptions) {
|
||||
super(options);
|
||||
this.requiresConfidential = true;
|
||||
this.certificatePath = options.certificatePath;
|
||||
this.sendCertificateChain = options.sendCertificateChain;
|
||||
|
||||
const parts = this.parseCertificate(options.certificatePath);
|
||||
this.msalConfig.auth.clientCertificate = {
|
||||
thumbprint: parts.thumbprint,
|
||||
privateKey: parts.certificateContents,
|
||||
x5c: parts.x5c
|
||||
};
|
||||
}
|
||||
|
||||
private parseCertificate(certificatePath: string): CertificateParts {
|
||||
const certificateParts: Partial<CertificateParts> = {};
|
||||
|
||||
certificateParts.certificateContents = readFileSync(certificatePath, "utf8");
|
||||
if (this.sendCertificateChain) {
|
||||
certificateParts.x5c = certificateParts.certificateContents;
|
||||
}
|
||||
|
||||
const certificatePattern = /(-+BEGIN CERTIFICATE-+)(\n\r?|\r\n?)([A-Za-z0-9+/\n\r]+=*)(\n\r?|\r\n?)(-+END CERTIFICATE-+)/g;
|
||||
const publicKeys: string[] = [];
|
||||
|
||||
// Match all possible certificates, in the order they are in the file. These will form the chain that is used for x5c
|
||||
let match;
|
||||
do {
|
||||
match = certificatePattern.exec(certificateParts.certificateContents);
|
||||
if (match) {
|
||||
publicKeys.push(match[3]);
|
||||
}
|
||||
} while (match);
|
||||
|
||||
if (publicKeys.length === 0) {
|
||||
const error = new Error(
|
||||
"The file at the specified path does not contain a PEM-encoded certificate."
|
||||
);
|
||||
// Changing the MSAL configuration asynchronously
|
||||
async init(options?: CredentialFlowGetTokenOptions): Promise<void> {
|
||||
try {
|
||||
const parts = await parseCertificate(this.certificatePath, this.sendCertificateChain);
|
||||
this.msalConfig.auth.clientCertificate = {
|
||||
thumbprint: parts.thumbprint,
|
||||
privateKey: parts.certificateContents,
|
||||
x5c: parts.x5c
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.info(formatError("", error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
certificateParts.thumbprint = createHash("sha1")
|
||||
.update(Buffer.from(publicKeys[0], "base64"))
|
||||
.digest("hex")
|
||||
.toUpperCase();
|
||||
|
||||
return certificateParts as CertificateParts;
|
||||
return super.init(options);
|
||||
}
|
||||
|
||||
protected async doGetToken(
|
||||
|
|
|
@ -11,6 +11,9 @@ import { MsalNodeOptions, MsalNode } from "./nodeCommon";
|
|||
* @internal
|
||||
*/
|
||||
export interface MSALClientSecretOptions extends MsalNodeOptions {
|
||||
/**
|
||||
* A client secret that was generated for the App Registration.
|
||||
*/
|
||||
clientSecret: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { AccessToken } from "@azure/core-auth";
|
||||
import { formatError } from "../../util/logging";
|
||||
import { CredentialFlowGetTokenOptions } from "../credentials";
|
||||
import { parseCertificate } from "./msalClientCertificate";
|
||||
import { MsalNodeOptions, MsalNode } from "./nodeCommon";
|
||||
|
||||
/**
|
||||
* Options that can be passed to configure MSAL to handle On-Behalf-Of authentication requests.
|
||||
* @internal
|
||||
*/
|
||||
export interface MSALOnBehalfOfOptions extends MsalNodeOptions {
|
||||
/**
|
||||
* A client secret that was generated for the App Registration.
|
||||
*/
|
||||
clientSecret?: string;
|
||||
/**
|
||||
* Location of the PEM certificate.
|
||||
*/
|
||||
certificatePath?: string;
|
||||
/**
|
||||
* Option to include x5c header for SubjectName and Issuer name authorization.
|
||||
* Set this option to send base64 encoded public certificate in the client assertion header as an x5c claim
|
||||
*/
|
||||
sendCertificateChain?: boolean;
|
||||
/**
|
||||
* The user assertion for the On-Behalf-Of flow.
|
||||
*/
|
||||
userAssertionToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* MSAL on behalf of flow. Calls to MSAL's confidential application's `acquireTokenOnBehalfOf` during `doGetToken`.
|
||||
* @internal
|
||||
*/
|
||||
export class MsalOnBehalfOf extends MsalNode {
|
||||
private userAssertionToken: string;
|
||||
private certificatePath?: string;
|
||||
private sendCertificateChain?: boolean;
|
||||
private clientSecret?: string;
|
||||
|
||||
constructor(options: MSALOnBehalfOfOptions) {
|
||||
super(options);
|
||||
this.logger.info("Initialized MSAL's On-Behalf-Of flow");
|
||||
this.requiresConfidential = true;
|
||||
this.userAssertionToken = options.userAssertionToken;
|
||||
this.certificatePath = options.certificatePath;
|
||||
this.sendCertificateChain = options.sendCertificateChain;
|
||||
this.clientSecret = options.clientSecret;
|
||||
}
|
||||
|
||||
// Changing the MSAL configuration asynchronously
|
||||
async init(options?: CredentialFlowGetTokenOptions): Promise<void> {
|
||||
if (this.certificatePath) {
|
||||
try {
|
||||
const parts = await parseCertificate(this.certificatePath, this.sendCertificateChain);
|
||||
this.msalConfig.auth.clientCertificate = {
|
||||
thumbprint: parts.thumbprint,
|
||||
privateKey: parts.certificateContents,
|
||||
x5c: parts.x5c
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.info(formatError("", error));
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
this.msalConfig.auth.clientSecret = this.clientSecret;
|
||||
}
|
||||
return super.init(options);
|
||||
}
|
||||
|
||||
protected async doGetToken(
|
||||
scopes: string[],
|
||||
options: CredentialFlowGetTokenOptions = {}
|
||||
): Promise<AccessToken> {
|
||||
try {
|
||||
const result = await this.confidentialApp!.acquireTokenOnBehalfOf({
|
||||
scopes,
|
||||
correlationId: options.correlationId,
|
||||
authority: options.authority,
|
||||
oboAssertion: this.userAssertionToken
|
||||
});
|
||||
return this.handleResult(scopes, this.clientId, result || undefined);
|
||||
} catch (err) {
|
||||
throw this.handleError(scopes, err, options);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,8 +114,8 @@ export async function prepareIdentityTests({
|
|||
}
|
||||
|
||||
if (replaceLogger) {
|
||||
AzureLogger.log = (args) => {
|
||||
logMessages.push(args);
|
||||
AzureLogger.log = (...args) => {
|
||||
logMessages.push(args.join(" "));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -171,23 +171,29 @@ export async function prepareIdentityTests({
|
|||
const providerObject = provider === "http" ? http : https;
|
||||
const totalOptions: http.RequestOptions[] = [];
|
||||
|
||||
sandbox.replace(
|
||||
providerObject,
|
||||
"request",
|
||||
(options: string | URL | http.RequestOptions, resolve: any) => {
|
||||
totalOptions.push(options as http.RequestOptions);
|
||||
try {
|
||||
sandbox.replace(
|
||||
providerObject,
|
||||
"request",
|
||||
(options: string | URL | http.RequestOptions, resolve: any) => {
|
||||
totalOptions.push(options as http.RequestOptions);
|
||||
|
||||
const { response, error } = responses.shift()!;
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
resolve(responseToIncomingMessage(response!));
|
||||
const { response, error } = responses.shift()!;
|
||||
if (error) {
|
||||
throw error;
|
||||
} else {
|
||||
resolve(responseToIncomingMessage(response!));
|
||||
}
|
||||
const request = createRequest();
|
||||
spies.push(sandbox.spy(request, "end"));
|
||||
return request;
|
||||
}
|
||||
const request = createRequest();
|
||||
spies.push(sandbox.spy(request, "end"));
|
||||
return request;
|
||||
}
|
||||
);
|
||||
);
|
||||
} catch (e) {
|
||||
console.debug(
|
||||
"Failed to replace the request. This might be expected if you're running multiple sendCredentialRequests() calls."
|
||||
);
|
||||
}
|
||||
|
||||
return totalOptions;
|
||||
};
|
||||
|
|
|
@ -79,14 +79,19 @@ describe("ClientCertificateCredential (internal)", function() {
|
|||
});
|
||||
});
|
||||
|
||||
it("throws when given a file that doesn't contain a PEM-formatted certificate", () => {
|
||||
assert.throws(() => {
|
||||
new ClientCertificateCredential(
|
||||
"tenant",
|
||||
"client",
|
||||
path.resolve(__dirname, "../src/index.ts")
|
||||
);
|
||||
});
|
||||
it("throws when given a file that doesn't contain a PEM-formatted certificate", async function(this: Context) {
|
||||
const fullPath = path.resolve(__dirname, "../src/index.ts");
|
||||
const credential = new ClientCertificateCredential("tenant", "client", fullPath);
|
||||
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
await credential.getToken(scope);
|
||||
} catch (_error) {
|
||||
error = _error;
|
||||
}
|
||||
|
||||
assert.ok(error);
|
||||
assert.deepEqual(error?.message, `ENOENT: no such file or directory, open '${fullPath}'`);
|
||||
});
|
||||
|
||||
it("Authenticates silently after the initial request", async function(this: Context) {
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as path from "path";
|
||||
import { assert } from "chai";
|
||||
import { isNode } from "@azure/core-util";
|
||||
import { OnBehalfOfCredential } from "../../../src";
|
||||
import {
|
||||
createResponse,
|
||||
IdentityTestContext,
|
||||
SendCredentialRequests
|
||||
} from "../../httpRequestsCommon";
|
||||
import { prepareIdentityTests, prepareMSALResponses } from "../../httpRequests";
|
||||
|
||||
describe("OnBehalfOfCredential", function() {
|
||||
let testContext: IdentityTestContext;
|
||||
let sendCredentialRequests: SendCredentialRequests;
|
||||
|
||||
beforeEach(async function() {
|
||||
testContext = await prepareIdentityTests({ replaceLogger: true, logLevel: "verbose" });
|
||||
sendCredentialRequests = testContext.sendCredentialRequests;
|
||||
});
|
||||
afterEach(async function() {
|
||||
if (isNode) {
|
||||
delete process.env.AZURE_AUTHORITY_HOST;
|
||||
}
|
||||
await testContext.restore();
|
||||
});
|
||||
|
||||
it("authenticates with a secret", async () => {
|
||||
const credential = new OnBehalfOfCredential({
|
||||
tenantId: "adfs",
|
||||
clientId: "client",
|
||||
clientSecret: "secret",
|
||||
userAssertionToken: "user-assertion"
|
||||
});
|
||||
|
||||
const newMSALClientLogs = () =>
|
||||
testContext.logMessages.filter((message) =>
|
||||
message.match("Initialized MSAL's On-Behalf-Of flow")
|
||||
).length;
|
||||
|
||||
const authDetails = await sendCredentialRequests({
|
||||
scopes: ["https://test/.default"],
|
||||
credential,
|
||||
secureResponses: [
|
||||
...prepareMSALResponses(),
|
||||
createResponse(200, {
|
||||
access_token: "token",
|
||||
expires_on: "06/20/2019 02:57:58 +00:00"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
assert.isNumber(authDetails.result?.expiresOnTimestamp);
|
||||
|
||||
// Just checking that a new MSAL client was created.
|
||||
// This kind of testing will be important as we improve the On-Behalf-Of Credential.
|
||||
assert.equal(newMSALClientLogs(), 1);
|
||||
});
|
||||
|
||||
it("authenticates with a certificate path", async () => {
|
||||
const certificatePath = path.join("assets", "fake-cert.pem");
|
||||
const credential = new OnBehalfOfCredential({
|
||||
tenantId: "adfs",
|
||||
clientId: "client",
|
||||
certificatePath,
|
||||
userAssertionToken: "user-assertion"
|
||||
});
|
||||
|
||||
const newMSALClientLogs = () =>
|
||||
testContext.logMessages.filter((message) =>
|
||||
message.match("Initialized MSAL's On-Behalf-Of flow")
|
||||
).length;
|
||||
|
||||
const authDetails = await sendCredentialRequests({
|
||||
scopes: ["https://test/.default"],
|
||||
credential,
|
||||
secureResponses: [
|
||||
...prepareMSALResponses(),
|
||||
createResponse(200, {
|
||||
access_token: "token",
|
||||
expires_on: "06/20/2019 02:57:58 +00:00"
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
assert.isNumber(authDetails.result?.expiresOnTimestamp);
|
||||
|
||||
// Just checking that a new MSAL client was created.
|
||||
// This kind of testing will be important as we improve the On-Behalf-Of Credential.
|
||||
assert.equal(newMSALClientLogs(), 1);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче