-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[identity] Support Federated Identity for Service connections - new c…
…redential (#29392)
- Loading branch information
1 parent
f1b4f41
commit 63dce5e
Showing
12 changed files
with
1,046 additions
and
446 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
180 changes: 180 additions & 0 deletions
180
sdk/identity/identity/src/credentials/azurePipelinesServiceConnectionCredential.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth"; | ||
import { ClientAssertionCredential } from "./clientAssertionCredential"; | ||
import { AuthenticationError, CredentialUnavailableError } from "../errors"; | ||
import { credentialLogger } from "../util/logging"; | ||
import { checkTenantId } from "../util/tenantIdUtils"; | ||
import { | ||
createDefaultHttpClient, | ||
createHttpHeaders, | ||
createPipelineRequest, | ||
} from "@azure/core-rest-pipeline"; | ||
import { AzurePipelinesServiceConnectionCredentialOptions } from "./azurePipelinesServiceConnectionCredentialOptions"; | ||
|
||
const credentialName = "AzurePipelinesServiceConnectionCredential"; | ||
const OIDC_API_VERSION = "7.1"; | ||
const logger = credentialLogger(credentialName); | ||
|
||
/** | ||
* This credential is designed to be used in ADO Pipelines with service connections | ||
* as a setup for workload identity federation. | ||
*/ | ||
export class AzurePipelinesServiceConnectionCredential implements TokenCredential { | ||
private clientAssertionCredential: ClientAssertionCredential | undefined; | ||
private serviceConnectionId: string | undefined; | ||
|
||
/** | ||
* AzurePipelinesServiceConnectionCredential supports Federated Identity on Azure Pipelines through Service Connections. | ||
* @param tenantId - tenantId associated with the service connection | ||
* @param clientId - clientId associated with the service connection | ||
* @param serviceConnectionId - id for the service connection | ||
* @param options - The identity client options to use for authentication. | ||
*/ | ||
constructor( | ||
tenantId: string, | ||
clientId: string, | ||
serviceConnectionId: string, | ||
options?: AzurePipelinesServiceConnectionCredentialOptions, | ||
) { | ||
if (!clientId || !tenantId || !serviceConnectionId) { | ||
throw new CredentialUnavailableError( | ||
`${credentialName}: is unavailable. tenantId, clientId, and serviceConnectionId are required parameters.`, | ||
); | ||
} | ||
|
||
checkTenantId(logger, tenantId); | ||
logger.info( | ||
`Invoking AzurePipelinesServiceConnectionCredential with tenant ID: ${tenantId}, clientId: ${clientId} and service connection id: ${serviceConnectionId}`, | ||
); | ||
|
||
if (clientId && tenantId && serviceConnectionId) { | ||
this.ensurePipelinesSystemVars(); | ||
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=${OIDC_API_VERSION}&serviceConnectionId=${this.serviceConnectionId}`; | ||
const systemAccessToken = `${process.env.SYSTEM_ACCESSTOKEN}`; | ||
logger.info( | ||
`Invoking ClientAssertionCredential with tenant ID: ${tenantId}, clientId: ${clientId} and service connection id: ${serviceConnectionId}`, | ||
); | ||
this.clientAssertionCredential = new ClientAssertionCredential( | ||
tenantId, | ||
clientId, | ||
this.requestOidcToken.bind(this, oidcRequestUrl, systemAccessToken), | ||
options, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Authenticates with Microsoft Entra ID and returns an access token if successful. | ||
* If authentication fails, a {@link CredentialUnavailableError} or {@link AuthenticationError} 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 any requests this | ||
* TokenCredential implementation might make. | ||
*/ | ||
public async getToken( | ||
scopes: string | string[], | ||
options?: GetTokenOptions, | ||
): Promise<AccessToken> { | ||
if (!this.clientAssertionCredential) { | ||
const errorMessage = `${credentialName}: is unavailable. tenantId, clientId, and serviceConnectionId are required parameters. | ||
To use Federation Identity in Azure Pipelines, these are required as inputs / env variables - | ||
tenantId, | ||
clientId, | ||
serviceConnectionId, | ||
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" && | ||
"SYSTEM_TEAMPROJECTID" && | ||
"SYSTEM_PLANID" && | ||
"SYSTEM_JOBID" && | ||
"SYSTEM_ACCESSTOKEN" | ||
See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/troubleshoot`; | ||
logger.error(errorMessage); | ||
throw new CredentialUnavailableError(errorMessage); | ||
} | ||
logger.info("Invoking getToken() of Client Assertion Credential"); | ||
return this.clientAssertionCredential.getToken(scopes, options); | ||
} | ||
|
||
/** | ||
* | ||
* @param oidcRequestUrl - oidc request url | ||
* @param systemAccessToken - system access token | ||
* @returns OIDC token from Azure Pipelines | ||
*/ | ||
private async requestOidcToken( | ||
oidcRequestUrl: string, | ||
systemAccessToken: string, | ||
): Promise<string> { | ||
logger.info("Requesting OIDC token from Azure Pipelines..."); | ||
logger.info(oidcRequestUrl); | ||
|
||
const httpClient = createDefaultHttpClient(); | ||
|
||
const request = createPipelineRequest({ | ||
url: oidcRequestUrl, | ||
method: "POST", | ||
headers: createHttpHeaders({ | ||
"Content-Type": "application/json", | ||
Authorization: `Bearer ${systemAccessToken}`, | ||
}), | ||
}); | ||
|
||
const response = await httpClient.sendRequest(request); | ||
const text = response.bodyAsText; | ||
if (!text) { | ||
throw new AuthenticationError( | ||
response.status, | ||
`${credentialName}: Authenticated Failed. Received null token from OIDC request.`, | ||
); | ||
} | ||
const result = JSON.parse(text); | ||
if (result?.oidcToken) { | ||
return result.oidcToken; | ||
} else { | ||
throw new AuthenticationError( | ||
response.status, | ||
`${credentialName}: Authentication Failed. oidcToken field not detected in the response. Response = ${JSON.stringify( | ||
result, | ||
)}`, | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Ensures all system env vars are there to form the request uri for OIDC token | ||
* @returns void | ||
* @throws CredentialUnavailableError | ||
*/ | ||
private ensurePipelinesSystemVars(): void { | ||
if ( | ||
process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI && | ||
process.env.SYSTEM_TEAMPROJECTID && | ||
process.env.SYSTEM_PLANID && | ||
process.env.SYSTEM_JOBID && | ||
process.env.SYSTEM_ACCESSTOKEN | ||
) { | ||
return; | ||
} | ||
const missingEnvVars = []; | ||
let errorMessage = ""; | ||
if (!process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { | ||
missingEnvVars.push("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"); | ||
} | ||
if (!process.env.SYSTEM_TEAMPROJECTID) missingEnvVars.push("SYSTEM_TEAMPROJECTID"); | ||
if (!process.env.SYSTEM_PLANID) missingEnvVars.push("SYSTEM_PLANID"); | ||
if (!process.env.SYSTEM_JOBID) missingEnvVars.push("SYSTEM_JOBID"); | ||
if (!process.env.SYSTEM_ACCESSTOKEN) { | ||
errorMessage += | ||
"\nPlease ensure that the system access token is available in the SYSTEM_ACCESSTOKEN value; this is often most easily achieved by adding a block to the end of your pipeline yaml for the task with:\n env: \n- SYSTEM_ACCESSTOKEN: $(System.AccessToken)"; | ||
missingEnvVars.push("SYSTEM_ACCESSTOKEN"); | ||
} | ||
if (missingEnvVars.length > 0) { | ||
throw new CredentialUnavailableError( | ||
`${credentialName}: is unavailable. Ensure that you're running this task in an Azure Pipeline, so that following missing system variable(s) can be defined- ${missingEnvVars.join( | ||
", ", | ||
)}.${errorMessage}`, | ||
); | ||
} | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
sdk/identity/identity/src/credentials/azurePipelinesServiceConnectionCredentialOptions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { AuthorityValidationOptions } from "./authorityValidationOptions"; | ||
import { CredentialPersistenceOptions } from "./credentialPersistenceOptions"; | ||
import { MultiTenantTokenCredentialOptions } from "./multiTenantTokenCredentialOptions"; | ||
|
||
/** | ||
* Optional parameters for the {@link AzurePipelinesServiceConnectionCredential} class. | ||
*/ | ||
export interface AzurePipelinesServiceConnectionCredentialOptions | ||
extends MultiTenantTokenCredentialOptions, | ||
CredentialPersistenceOptions, | ||
AuthorityValidationOptions {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
sdk/identity/identity/test/public/node/azurePipelinesServiceConnection.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
import { AzurePipelinesServiceConnectionCredential } from "../../../src"; | ||
import { env, isLiveMode } from "@azure-tools/test-recorder"; | ||
import { assert } from "@azure-tools/test-utils"; | ||
|
||
describe("AzurePipelinesServiceConnectionCredential", function () { | ||
const scope = "https://vault.azure.net/.default"; | ||
const tenantId = env.IDENTITY_SP_TENANT_ID || env.AZURE_TENANT_ID!; | ||
// const clientId = env.IDENTITY_SP_CLIENT_ID || env.AZURE_CLIENT_ID!; | ||
|
||
it("authenticates with a valid service connection", async function () { | ||
if (!isLiveMode()) { | ||
this.skip(); | ||
} | ||
// this serviceConnection corresponds to the Azure SDK Test Resources - LiveTestSecrets service | ||
const existingServiceConnectionId = "0dec29c2-a766-4121-9c2e-1894f5aca5cb"; | ||
// clientId for above service connection | ||
const clientId = "203c27cb-6778-4ecc-9bfd-9f03a61f3408"; | ||
const credential = new AzurePipelinesServiceConnectionCredential( | ||
tenantId, | ||
clientId, | ||
existingServiceConnectionId, | ||
); | ||
try { | ||
const token = await credential.getToken(scope); | ||
assert.ok(token?.token); | ||
assert.isDefined(token?.expiresOnTimestamp); | ||
if (token?.expiresOnTimestamp) assert.ok(token?.expiresOnTimestamp > Date.now()); | ||
} catch (e) { | ||
console.log(e); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters