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

[Identity] Support exchanging k8s token to AAD token #16688

Merged
29 commits merged into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9d396d3
[Identity] Support exchanging k8s token to AAD token
sadasant Jul 31, 2021
684c32e
Merge remote-tracking branch 'Azure/main' into identity/fix15800
sadasant Sep 1, 2021
5dede8c
fixes after merging main
sadasant Sep 1, 2021
63c2986
feedback and tests fixed
sadasant Sep 2, 2021
8f93b29
simpler changelog entry
sadasant Sep 2, 2021
e376252
MSICache is not necessary
sadasant Sep 2, 2021
d35daf5
after feedback from Charles
sadasant Sep 2, 2021
18ee518
Merge remote-tracking branch 'Azure/main' into identity/fix15800
sadasant Sep 2, 2021
f8deac2
small improvements
sadasant Sep 2, 2021
a19e628
using the original scopes instead of the altered resource, and better…
sadasant Sep 2, 2021
3df153d
formatting
sadasant Sep 2, 2021
ad0f06f
possible CI fix
sadasant Sep 2, 2021
67f1d04
will this fix the build?
sadasant Sep 3, 2021
33cd48c
fixed AggregateAuthenticationError bug. Potentially related to CI
sadasant Sep 3, 2021
7aea1ee
changelog entry
sadasant Sep 3, 2021
2dd335c
one more CHANGELOG entry
sadasant Sep 3, 2021
ab0c572
overkill solution to ensure the environment is cleaned for this test.…
sadasant Sep 3, 2021
b87127f
this time trying with a potential bug fix in the IMDS MSI
sadasant Sep 3, 2021
2881f6c
i am running out of ideas
sadasant Sep 3, 2021
9eb6203
feedback after speaking with Scott Schaab: limit AZURE_CLIENT_ID to t…
sadasant Sep 3, 2021
385f187
Here we go again
sadasant Sep 3, 2021
02fd239
another attempt
sadasant Sep 3, 2021
0ec0125
somewhat desperate meassures: logging the error
sadasant Sep 3, 2021
9b3811b
ok found the source of the CI issue: one last non-standard error message
sadasant Sep 4, 2021
2f07e3a
small fix to mapScopesToResource after a second thought
sadasant Sep 7, 2021
3091b74
specifying that most MSIs are not available for getToken requests wit…
sadasant Sep 7, 2021
1dd2856
Update sdk/identity/identity/src/credentials/managedIdentityCredentia…
sadasant Sep 8, 2021
a2a5f89
testing that parameter client_id overrides environment client_id
sadasant Sep 8, 2021
1cf0242
variable rename thanks to Charles
sadasant Sep 8, 2021
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
5 changes: 5 additions & 0 deletions sdk/identity/identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- `ManagedIdentityCredential` now supports token exchange authentication.
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 intentionally vague.


### Breaking Changes

#### Breaking Changes from 2.0.0-beta.5
Expand All @@ -14,9 +16,12 @@

- `ClientSecretCredential`, `ClientCertificateCredential` and `UsernamePasswordCredential` now throw if the required parameters are not provided (even in JavaScript).
- Fixed a bug introduced on 2.0.0-beta.5 that caused the `ManagedIdentityCredential` to fail authenticating in Arc environments. Since our new core disables unsafe requests by default, we had to change the security settings for the first request of the Arc MSI, which retrieves the file path where the authentication value is stored since this request generally happens through an HTTP endpoint.
- Fixed bug on the `AggregateAuthenticationError`, which caused an inconsistent error message on the `ChainedTokenCredential`, `DefaultAzureCredential` and `ApplicationCredential`.

### Other Changes

- The errors thrown by the `ManagedIdentityCredential` have been improved.

## 2.0.0-beta.5 (2021-08-10)

### Features Added
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/identity/src/client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class AggregateAuthenticationError extends Error {

constructor(errors: any[], errorMessage?: string) {
const errorDetail = errors.join("\n");
super(`${errorMessage}\n\n${errorDetail}`);
super(`${errorMessage}\n${errorDetail}`);
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 meant a whole empty line, which I didn’t like now that I re-reviewed these errors.

this.errors = errors;

// Ensure that this type reports the correct name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export class ChainedTokenCredential implements TokenCredential {
}

if (!token && errors.length > 0) {
const err = new AggregateAuthenticationError(errors);
const err = new AggregateAuthenticationError(
errors,
"ChainedTokenCredential authentication failed."
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without this, this error had an initial segment with an undefined string. This PR adds a test to ensure this is never silently broken in the future.

span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@

import { createHttpHeaders, PipelineRequestOptions } from "@azure/core-rest-pipeline";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { IdentityClient } from "../../client/identityClient";
import { credentialLogger } from "../../util/logging";
import { MSI } from "./models";
import { msiGenericGetToken } from "./utils";
import { MSI, MSIConfiguration } from "./models";
import { mapScopesToResource, msiGenericGetToken } from "./utils";

const logger = credentialLogger("ManagedIdentityCredential - AppServiceMSI 2017");
const msiName = "ManagedIdentityCredential - AppServiceMSI 2017";
const logger = credentialLogger(msiName);

function expiresInParser(requestBody: any): number {
// Parse a date format like "06/20/2019 02:57:58 +00:00" and
// convert it into a JavaScript-formatted date
return Date.parse(requestBody.expires_on);
}

function prepareRequestOptions(resource: string, clientId?: string): PipelineRequestOptions {
function prepareRequestOptions(
scopes: string | string[],
clientId?: string
): PipelineRequestOptions {
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}

const queryParameters: any = {
resource,
"api-version": "2017-09-01"
Expand All @@ -30,10 +38,10 @@ function prepareRequestOptions(resource: string, clientId?: string): PipelineReq

// This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below.
if (!process.env.MSI_ENDPOINT) {
throw new Error("Missing environment variable: MSI_ENDPOINT");
throw new Error(`${msiName}: Missing environment variable: MSI_ENDPOINT`);
}
if (!process.env.MSI_SECRET) {
throw new Error("Missing environment variable: MSI_SECRET");
throw new Error(`${msiName}: Missing environment variable: MSI_SECRET`);
}

return {
Expand All @@ -47,27 +55,34 @@ function prepareRequestOptions(resource: string, clientId?: string): PipelineReq
}

export const appServiceMsi2017: MSI = {
async isAvailable(): Promise<boolean> {
async isAvailable(scopes): Promise<boolean> {
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
const env = process.env;
const result = Boolean(env.MSI_ENDPOINT && env.MSI_SECRET);
if (!result) {
logger.info("The Azure App Service MSI 2017 is unavailable.");
logger.info(
`${msiName}: Unavailable. The environment variables needed are: MSI_ENDPOINT and MSI_SECRET.`
);
}
return result;
},
async getToken(
identityClient: IdentityClient,
resource: string,
clientId?: string,
configuration: MSIConfiguration,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
const { identityClient, scopes, clientId } = configuration;

logger.info(
`Using the endpoint and the secret coming form the environment variables: MSI_ENDPOINT=${process.env.MSI_ENDPOINT} and MSI_SECRET=[REDACTED].`
`${msiName}: Using the endpoint and the secret coming form the environment variables: MSI_ENDPOINT=${process.env.MSI_ENDPOINT} and MSI_SECRET=[REDACTED].`
);

return msiGenericGetToken(
identityClient,
prepareRequestOptions(resource, clientId),
prepareRequestOptions(scopes, clientId),
expiresInParser,
getTokenOptions
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import {
} from "@azure/core-rest-pipeline";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { readFile } from "fs";
import { MSI } from "./models";
import { MSI, MSIConfiguration } from "./models";
import { credentialLogger } from "../../util/logging";
import { IdentityClient } from "../../client/identityClient";
import { msiGenericGetToken } from "./utils";
import { mapScopesToResource, msiGenericGetToken } from "./utils";
import { azureArcAPIVersion } from "./constants";
import { AuthenticationError } from "../../client/errors";

const logger = credentialLogger("ManagedIdentityCredential - ArcMSI");
const msiName = "ManagedIdentityCredential - Azure Arc MSI";
const logger = credentialLogger(msiName);

// Azure Arc MSI doesn't have a special expiresIn parser.
const expiresInParser = undefined;

function prepareRequestOptions(resource?: string): PipelineRequestOptions {
function prepareRequestOptions(scopes: string | string[]): PipelineRequestOptions {
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}
const queryParameters: any = {
resource,
"api-version": azureArcAPIVersion
Expand All @@ -30,7 +35,7 @@ function prepareRequestOptions(resource?: string): PipelineRequestOptions {

// This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below.
if (!process.env.IDENTITY_ENDPOINT) {
throw new Error("Missing environment variable: IDENTITY_ENDPOINT");
throw new Error(`${msiName}: Missing environment variable: IDENTITY_ENDPOINT`);
}

return createPipelineRequest({
Expand Down Expand Up @@ -69,7 +74,7 @@ async function filePathRequest(
}
throw new AuthenticationError(
response.status,
`To authenticate with Azure Arc MSI, status code 401 is expected on the first request.${message}`
`${msiName}: To authenticate with Azure Arc MSI, status code 401 is expected on the first request. ${message}`
);
}

Expand All @@ -82,39 +87,46 @@ async function filePathRequest(
}

export const arcMsi: MSI = {
async isAvailable(): Promise<boolean> {
async isAvailable(scopes): Promise<boolean> {
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
const result = Boolean(process.env.IMDS_ENDPOINT && process.env.IDENTITY_ENDPOINT);
if (!result) {
logger.info("The Azure Arc MSI is unavailable.");
logger.info(
`${msiName}: The environment variables needed are: IMDS_ENDPOINT and IDENTITY_ENDPOINT`
);
}
return result;
},
async getToken(
identityClient: IdentityClient,
resource?: string,
clientId?: string,
configuration: MSIConfiguration,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
logger.info(`Using the Azure Arc MSI to authenticate.`);
const { identityClient, scopes, clientId } = configuration;

logger.info(`${msiName}: Authenticating.`);

if (clientId) {
throw new Error(
"User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate with the system assigned identity omit the client id when constructing the ManagedIdentityCredential, or if authenticating with the DefaultAzureCredential ensure the AZURE_CLIENT_ID environment variable is not set."
`${msiName}: User assigned identity is not supported by the Azure Arc Managed Identity Endpoint. To authenticate with the system assigned identity, omit the client id when constructing the ManagedIdentityCredential, or if authenticating with the DefaultAzureCredential ensure the AZURE_CLIENT_ID environment variable is not set.`
);
}

const requestOptions = {
disableJsonStringifyOnBody: true,
deserializationMapper: undefined,
abortSignal: getTokenOptions.abortSignal,
...prepareRequestOptions(resource),
...prepareRequestOptions(scopes),
allowInsecureConnection: true
};

const filePath = await filePathRequest(identityClient, requestOptions);

if (!filePath) {
throw new Error("Azure Arc MSI failed to find the token file.");
throw new Error(`${msiName}: Failed to find the token file.`);
}

const key = await readFileAsync(filePath, { encoding: "utf-8" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,25 @@

import { createHttpHeaders, PipelineRequestOptions } from "@azure/core-rest-pipeline";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { MSI } from "./models";
import { MSI, MSIConfiguration } from "./models";
import { credentialLogger } from "../../util/logging";
import { IdentityClient } from "../../client/identityClient";
import { msiGenericGetToken } from "./utils";
import { mapScopesToResource, msiGenericGetToken } from "./utils";

const logger = credentialLogger("ManagedIdentityCredential - CloudShellMSI");
const msiName = "ManagedIdentityCredential - CloudShellMSI";
const logger = credentialLogger(msiName);

// Cloud Shell MSI doesn't have a special expiresIn parser.
const expiresInParser = undefined;

function prepareRequestOptions(resource: string, clientId?: string): PipelineRequestOptions {
function prepareRequestOptions(
scopes: string | string[],
clientId?: string
): PipelineRequestOptions {
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}

const body: any = {
resource
};
Expand All @@ -24,7 +32,7 @@ function prepareRequestOptions(resource: string, clientId?: string): PipelineReq

// This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below.
if (!process.env.MSI_ENDPOINT) {
throw new Error("Missing environment variable: MSI_ENDPOINT");
throw new Error(`${msiName}: Missing environment variable: MSI_ENDPOINT`);
}
const params = new URLSearchParams(body);
return {
Expand All @@ -40,26 +48,31 @@ function prepareRequestOptions(resource: string, clientId?: string): PipelineReq
}

export const cloudShellMsi: MSI = {
async isAvailable(): Promise<boolean> {
async isAvailable(scopes): Promise<boolean> {
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
const result = Boolean(process.env.MSI_ENDPOINT);
if (!result) {
logger.info("The Azure Cloud Shell MSI is unavailable.");
logger.info(`${msiName}: Unavailable. The environment variable MSI_ENDPOINT is needed.`);
}
return result;
},
async getToken(
identityClient: IdentityClient,
resource: string,
clientId?: string,
configuration: MSIConfiguration,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
const { identityClient, scopes, clientId } = configuration;

logger.info(
`Using the endpoint coming form the environment variable MSI_ENDPOINT=${process.env.MSI_ENDPOINT}, and using the Cloud Shell to proceed with the authentication.`
`${msiName}: Using the endpoint coming form the environment variable MSI_ENDPOINT = ${process.env.MSI_ENDPOINT}.`
);

return msiGenericGetToken(
identityClient,
prepareRequestOptions(resource, clientId),
prepareRequestOptions(scopes, clientId),
expiresInParser,
getTokenOptions
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@

import { createHttpHeaders, PipelineRequestOptions } from "@azure/core-rest-pipeline";
import { AccessToken, GetTokenOptions } from "@azure/core-auth";
import { MSI } from "./models";
import { MSI, MSIConfiguration } from "./models";
import { credentialLogger } from "../../util/logging";
import { IdentityClient } from "../../client/identityClient";
import { msiGenericGetToken } from "./utils";
import { mapScopesToResource, msiGenericGetToken } from "./utils";
import { azureFabricVersion } from "./constants";

const logger = credentialLogger("ManagedIdentityCredential - Fabric MSI");
const msiName = "ManagedIdentityCredential - Fabric MSI";
const logger = credentialLogger(msiName);

function expiresInParser(requestBody: any): number {
// Parses a string representation of the seconds since epoch into a number value
return Number(requestBody.expires_on);
}

function prepareRequestOptions(resource: string, clientId?: string): PipelineRequestOptions {
function prepareRequestOptions(
scopes: string | string[],
clientId?: string
): PipelineRequestOptions {
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}

const queryParameters: any = {
resource,
"api-version": azureFabricVersion
Expand Down Expand Up @@ -58,24 +66,32 @@ function prepareRequestOptions(resource: string, clientId?: string): PipelineReq
//

export const fabricMsi: MSI = {
async isAvailable(): Promise<boolean> {
async isAvailable(scopes): Promise<boolean> {
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
const env = process.env;
const result = Boolean(
env.IDENTITY_ENDPOINT && env.IDENTITY_HEADER && env.IDENTITY_SERVER_THUMBPRINT
);
if (!result) {
logger.info("The Azure App Service Fabric MSI is unavailable.");
logger.info(
`${msiName}: Unavailable. The environment variables needed are: IDENTITY_ENDPOINT, IDENTITY_HEADER and IDENTITY_SERVER_THUMBPRINT`
);
}
return result;
},
async getToken(
identityClient: IdentityClient,
resource: string,
clientId?: string,
configuration: MSIConfiguration,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
const { identityClient, scopes, clientId } = configuration;

logger.info(
[
`${msiName}:`,
"Using the endpoint and the secret coming from the environment variables:",
`IDENTITY_ENDPOINT=${process.env.IDENTITY_ENDPOINT},`,
"IDENTITY_HEADER=[REDACTED] and",
Expand All @@ -85,7 +101,7 @@ export const fabricMsi: MSI = {

return msiGenericGetToken(
identityClient,
prepareRequestOptions(resource, clientId),
prepareRequestOptions(scopes, clientId),
expiresInParser,
getTokenOptions
);
Expand Down
Loading