diff --git a/sdk/identity/identity/CHANGELOG.md b/sdk/identity/identity/CHANGELOG.md index f0d07915434c..4c9294764638 100644 --- a/sdk/identity/identity/CHANGELOG.md +++ b/sdk/identity/identity/CHANGELOG.md @@ -1,10 +1,12 @@ # Release History -## 2.0.0-beta.5 (Unreleased) +## 2.0.0-beta.5 (2021-08-10) ### Features Added - `ChainedTokenCredential` and `DefaultAzureCredential` now expose a property named `selectedCredential`, which will store the selected credential once any of the available credentials succeeds. +- Implementation of `ApplicationCredential` for use by applications which call into Microsoft Graph APIs and which have issues using `DefaultAzureCredential`. This credential is based on `EnvironmentCredential` and `ManagedIdentityCredential`. + ### Breaking Changes ### Bugs Fixed @@ -16,6 +18,7 @@ ## 2.0.0-beta.4 (2021-07-07) ### Features Added + - With the dropping of support for Node.js versions that are no longer in LTS, the dependency on `@types/node` has been updated to version 12. Read our [support policy](https://github.com/Azure/azure-sdk-for-js/blob/main/SUPPORT.md) for more details. - Introduced an extension API through a top-level method `useIdentityExtension`. The function accepts an "extension" as an argument, which is a function accepting a `context`. The extension context is an internal part of the Azure Identity API, so it has an `unknown` type. Two new packages are designed to be used with this API: - `@azure/identity-vscode`, which provides the dependencies of `VisualStudioCodeCredential` and enables it (see more below). diff --git a/sdk/identity/identity/package.json b/sdk/identity/identity/package.json index b31b01eb623e..310494712817 100644 --- a/sdk/identity/identity/package.json +++ b/sdk/identity/identity/package.json @@ -21,6 +21,7 @@ "./dist-esm/src/credentials/visualStudioCodeCredential.js": "./dist-esm/src/credentials/visualStudioCodeCredential.browser.js", "./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/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", diff --git a/sdk/identity/identity/recordings/node/applicationcredential/recording_authenticates_with_a_client_secret_on_the_environment_variables.js b/sdk/identity/identity/recordings/node/applicationcredential/recording_authenticates_with_a_client_secret_on_the_environment_variables.js new file mode 100644 index 000000000000..4a7df189d2dd --- /dev/null +++ b/sdk/identity/identity/recordings/node/applicationcredential/recording_authenticates_with_a_client_secret_on_the_environment_variables.js @@ -0,0 +1,111 @@ +let nock = require('nock'); + +module.exports.hash = "3a3f3ba54882469a6c079e922fe83278"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/common/discovery/instance') + .query(true) + .reply(200, {"tenant_discovery_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '54656735-48e1-4eb1-bfc3-42b302a05b00', + 'x-ms-ests-server', + '2.1.11935.12 - WUS2 ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:12 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '980' +]); + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration') + .reply(200, {"token_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/token","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"jwks_uri":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/discovery/v2.0/keys","response_modes_supported":["query","fragment","form_post"],"subject_types_supported":["pairwise"],"id_token_signing_alg_values_supported":["RS256"],"response_types_supported":["code","id_token","code id_token","id_token token"],"scopes_supported":["openid","profile","email","offline_access"],"issuer":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0","request_uri_parameter_supported":false,"userinfo_endpoint":"https://graph.microsoft.com/oidc/userinfo","authorization_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/authorize","device_authorization_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/devicecode","http_logout_supported":true,"frontchannel_logout_supported":true,"end_session_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/logout","claims_supported":["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","nonce","preferred_username","name","tid","ver","at_hash","c_hash","email"],"kerberos_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/kerberos","tenant_region_scope":"WW","cloud_instance_name":"microsoftonline.com","cloud_graph_host_name":"graph.windows.net","msgraph_host":"graph.microsoft.com","rbac_url":"https://pas.windows.net"}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '30d8ef8c-f820-4adc-96c8-853b32ff3800', + 'x-ms-ests-server', + '2.1.11935.12 - WUS2 ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:12 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '1753' +]); + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/12345678-1234-1234-1234-123456789012/oauth2/v2.0/token', "client_id=azure_client_id&scope=https%3A%2F%2Fsanitized%2F&grant_type=client_credentials&x-client-SKU=msal.js.node&x-client-VER=1.2.0&x-client-OS=win32&x-client-CPU=x64&x-ms-lib-capability=retry-after, h429&x-client-current-telemetry=2|771,0|,&x-client-last-telemetry=2|0|||0,0&client-request-id=client-request-id&client_secret=azure_client_secret") + .reply(200, {"token_type":"Bearer","expires_in":86399,"ext_expires_in":86399,"access_token":"access_token"}, [ + 'Cache-Control', + 'no-store, no-cache', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '5d25849e-e99b-4f2e-b3ec-7f4efa843b00', + 'x-ms-ests-server', + '2.1.11935.12 - SCUS ProdSlices', + 'x-ms-clitelem', + '1,0,0,,', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '1315' +]); diff --git a/sdk/identity/identity/recordings/node/applicationcredential/recording_supports_tracing_with_environment_client_secret.js b/sdk/identity/identity/recordings/node/applicationcredential/recording_supports_tracing_with_environment_client_secret.js new file mode 100644 index 000000000000..f734a36f1443 --- /dev/null +++ b/sdk/identity/identity/recordings/node/applicationcredential/recording_supports_tracing_with_environment_client_secret.js @@ -0,0 +1,111 @@ +let nock = require('nock'); + +module.exports.hash = "b5a4d41bd6e386f72f5a1e905188a949"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/common/discovery/instance') + .query(true) + .reply(200, {"tenant_discovery_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '8e585fff-4996-4728-83e0-518e7345de01', + 'x-ms-ests-server', + '2.1.11898.12 - NCUS ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '980' +]); + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration') + .reply(200, {"token_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/token","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"jwks_uri":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/discovery/v2.0/keys","response_modes_supported":["query","fragment","form_post"],"subject_types_supported":["pairwise"],"id_token_signing_alg_values_supported":["RS256"],"response_types_supported":["code","id_token","code id_token","id_token token"],"scopes_supported":["openid","profile","email","offline_access"],"issuer":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0","request_uri_parameter_supported":false,"userinfo_endpoint":"https://graph.microsoft.com/oidc/userinfo","authorization_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/authorize","device_authorization_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/devicecode","http_logout_supported":true,"frontchannel_logout_supported":true,"end_session_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/oauth2/v2.0/logout","claims_supported":["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","nonce","preferred_username","name","tid","ver","at_hash","c_hash","email"],"kerberos_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/kerberos","tenant_region_scope":"WW","cloud_instance_name":"microsoftonline.com","cloud_graph_host_name":"graph.windows.net","msgraph_host":"graph.microsoft.com","rbac_url":"https://pas.windows.net"}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '1bd469dd-c998-4c70-88ad-188983123a00', + 'x-ms-ests-server', + '2.1.11935.12 - EUS ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '1753' +]); + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .post('/12345678-1234-1234-1234-123456789012/oauth2/v2.0/token', "client_id=azure_client_id&scope=https%3A%2F%2Fsanitized%2F&grant_type=client_credentials&x-client-SKU=msal.js.node&x-client-VER=1.2.0&x-client-OS=win32&x-client-CPU=x64&x-ms-lib-capability=retry-after, h429&x-client-current-telemetry=2|771,0|,&x-client-last-telemetry=2|0|||0,0&client-request-id=client-request-id&client_secret=azure_client_secret") + .reply(200, {"token_type":"Bearer","expires_in":86399,"ext_expires_in":86399,"access_token":"access_token"}, [ + 'Cache-Control', + 'no-store, no-cache', + 'Pragma', + 'no-cache', + 'Content-Type', + 'application/json; charset=utf-8', + 'Expires', + '-1', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '20a1a05c-5822-474d-b862-ec1d8c8b3c00', + 'x-ms-ests-server', + '2.1.11935.12 - EUS ProdSlices', + 'x-ms-clitelem', + '1,0,0,,', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:12 GMT', + 'Content-Length', + '1315' +]); diff --git a/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_aggregateauthenticationerror_when_gettoken_is_called_and_no_credential_was_configured.js b/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_aggregateauthenticationerror_when_gettoken_is_called_and_no_credential_was_configured.js new file mode 100644 index 000000000000..d5acef5667c8 --- /dev/null +++ b/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_aggregateauthenticationerror_when_gettoken_is_called_and_no_credential_was_configured.js @@ -0,0 +1,5 @@ +let nock = require('nock'); + +module.exports.hash = "f27429ae778247301f35d4567e0e1af5"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} diff --git a/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_authenticationerror_when_gettoken_is_called_and_applicationcredential_authentication_failed.js b/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_authenticationerror_when_gettoken_is_called_and_applicationcredential_authentication_failed.js new file mode 100644 index 000000000000..dd0ab0f7ca5b --- /dev/null +++ b/sdk/identity/identity/recordings/node/applicationcredential/recording_throws_an_authenticationerror_when_gettoken_is_called_and_applicationcredential_authentication_failed.js @@ -0,0 +1,76 @@ +let nock = require('nock'); + +module.exports.hash = "c0899b85b1280258c71e361d391d13d1"; + +module.exports.testInfo = {"uniqueName":{},"newDate":{}} + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/common/discovery/instance') + .query(true) + .reply(200, {"12345678-1234-1234-1234-123456789012_discovery_endpoint":"https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + '87378bfa-4cc9-4279-998f-d187ff277201', + 'x-ms-ests-server', + '2.1.11898.12 - EUS ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:13 GMT', + 'Content-Length', + '950' +]); + +nock('https://login.microsoftonline.com:443', {"encodedQueryParams":true}) + .get('/12345678-1234-1234-1234-123456789012/v2.0/.well-known/openid-configuration') + .reply(400, {"error":"invalid_12345678-1234-1234-1234-123456789012","error_description":"AADSTS90002: Tenant '12345678-1234-1234-1234-123456789012' not found. This may happen if there are no active subscriptions for the 12345678-1234-1234-1234-123456789012. Check to make sure you have the correct 12345678-1234-1234-1234-123456789012 ID. Check with your subscription administrator.\r\nTrace ID: c95ed377-3195-434d-8cac-0f5460a11f02\r\nCorrelation ID: 5bf6f73d-9031-4e3f-b44e-7b7d1ebe9864\r\nTimestamp: 2021-08-05 23:14:13Z","error_codes":[90002],"timestamp":"2021-08-05 23:14:13Z","trace_id":"c95ed377-3195-434d-8cac-0f5460a11f02","correlation_id":"5bf6f73d-9031-4e3f-b44e-7b7d1ebe9864","error_uri":"https://login.microsoftonline.com/error?code=90002"}, [ + 'Cache-Control', + 'max-age=86400, private', + 'Content-Type', + 'application/json; charset=utf-8', + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains', + 'X-Content-Type-Options', + 'nosniff', + 'Access-Control-Allow-Origin', + '*', + 'Access-Control-Allow-Methods', + 'GET, OPTIONS', + 'P3P', + 'CP="DSP CUR OTPi IND OTRi ONL FIN"', + 'x-ms-request-id', + 'c95ed377-3195-434d-8cac-0f5460a11f02', + 'x-ms-ests-server', + '2.1.11898.12 - NCUS ProdSlices', + 'Set-Cookie', + 'fpc=fpc;; expires=Sat, 04-Sep-2021 23:14:13 GMT; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'esctx=esctx; domain=.login.microsoftonline.com; path=/; secure; HttpOnly; SameSite=None', + 'Set-Cookie', + 'x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly', + 'Set-Cookie', + 'stsservicecookie=estsfd; path=/; secure; samesite=none; httponly', + 'Date', + 'Thu, 05 Aug 2021 23:14:13 GMT', + 'Content-Length', + '621' +]); diff --git a/sdk/identity/identity/review/identity.api.md b/sdk/identity/identity/review/identity.api.md index 1a7a12a29596..c4462db4fa91 100644 --- a/sdk/identity/identity/review/identity.api.md +++ b/sdk/identity/identity/review/identity.api.md @@ -21,6 +21,16 @@ export class AggregateAuthenticationError extends Error { // @public export const AggregateAuthenticationErrorName = "AggregateAuthenticationError"; +// @public +export class ApplicationCredential extends ChainedTokenCredential { + constructor(options?: ApplicationCredentialOptions); +} + +// @public +export interface ApplicationCredentialOptions extends TokenCredentialOptions, CredentialPersistenceOptions { + managedIdentityClientId?: string; +} + // @public export class AuthenticationError extends Error { constructor(statusCode: number, errorBody: object | string | undefined | null); diff --git a/sdk/identity/identity/src/credentials/applicationCredential.browser.ts b/sdk/identity/identity/src/credentials/applicationCredential.browser.ts new file mode 100644 index 000000000000..15ec08e380ca --- /dev/null +++ b/sdk/identity/identity/src/credentials/applicationCredential.browser.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { AccessToken } from "@azure/core-auth"; + +import { TokenCredentialOptions } from "../client/identityClient"; +import { credentialLogger, formatError } from "../util/logging"; +import { ChainedTokenCredential } from "./chainedTokenCredential"; + +const BrowserNotSupportedError = new Error( + "ApplicationCredential is not supported in the browser. Use InteractiveBrowserCredential instead." +); +const logger = credentialLogger("ApplicationCredential"); + +/** + * Provides a default {@link ChainedTokenCredential} configuration for + * applications that will be deployed to Azure. + * + * Only available in NodeJS. + */ +export class ApplicationCredential extends ChainedTokenCredential { + /** + * Creates an instance of the ApplicationCredential class. + * + * @param options - Options for configuring the client which makes the authentication request. + */ + constructor(_tokenCredentialOptions?: TokenCredentialOptions) { + super(); + logger.info(formatError("", BrowserNotSupportedError)); + throw BrowserNotSupportedError; + } + + public getToken(): Promise { + logger.getToken.info(formatError("", BrowserNotSupportedError)); + throw BrowserNotSupportedError; + } +} diff --git a/sdk/identity/identity/src/credentials/applicationCredential.ts b/sdk/identity/identity/src/credentials/applicationCredential.ts new file mode 100644 index 000000000000..1f36a3d2c17a --- /dev/null +++ b/sdk/identity/identity/src/credentials/applicationCredential.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TokenCredential } from "@azure/core-auth"; +import { TokenCredentialOptions } from "../client/identityClient"; +import { ChainedTokenCredential } from "./chainedTokenCredential"; +import { EnvironmentCredential } from "./environmentCredential"; +import { CredentialPersistenceOptions } from "./credentialPersistenceOptions"; +import { DefaultManagedIdentityCredential } from "./defaultAzureCredential"; + +/** + * Provides options to configure the {@link ApplicationCredential} class. + */ +export interface ApplicationCredentialOptions + extends TokenCredentialOptions, + CredentialPersistenceOptions { + /** + * Optionally pass in a user assigned client ID to be used by the {@link ManagedIdentityCredential}. + * This client ID can also be passed through to the {@link ManagedIdentityCredential} through the environment variable: AZURE_CLIENT_ID. + */ + managedIdentityClientId?: string; +} + +/** + * The type of a class that implements TokenCredential and accepts + * `ApplicationCredentialOptions`. + */ +interface ApplicationCredentialConstructor { + new (options?: ApplicationCredentialOptions): TokenCredential; +} + +export const ApplicationCredentials: ApplicationCredentialConstructor[] = [ + EnvironmentCredential, + DefaultManagedIdentityCredential +]; + +/** + * Provides a default {@link ChainedTokenCredential} configuration that should + * work for most applications that use the Azure SDK. The following credential + * types will be tried, in order: + * + * - {@link EnvironmentCredential} + * - {@link ManagedIdentityCredential} + + * + * Consult the documentation of these credential types for more information + * on how they attempt authentication. + * + * Azure Identity extensions may add credential types to the default credential + * stack. + */ +export class ApplicationCredential extends ChainedTokenCredential { + /** + * Creates an instance of the ApplicationCredential class. + * + * @param options - Optional parameters. See {@link ApplicationCredentialOptions}. + */ + constructor(options?: ApplicationCredentialOptions) { + super(...ApplicationCredentials.map((ctor) => new ctor(options))); + this.UnavailableMessage = + "ApplicationCredential => failed to retrieve a token from the included credentials"; + } +} diff --git a/sdk/identity/identity/src/credentials/defaultAzureCredential.ts b/sdk/identity/identity/src/credentials/defaultAzureCredential.ts index 1bbbda2020b5..e67d383c6e0b 100644 --- a/sdk/identity/identity/src/credentials/defaultAzureCredential.ts +++ b/sdk/identity/identity/src/credentials/defaultAzureCredential.ts @@ -46,7 +46,7 @@ interface DefaultCredentialConstructor { * * @internal */ -class DefaultManagedIdentityCredential extends ManagedIdentityCredential { +export class DefaultManagedIdentityCredential extends ManagedIdentityCredential { constructor(options?: DefaultAzureCredentialOptions) { const managedIdentityClientId = options?.managedIdentityClientId ?? process.env.AZURE_CLIENT_ID; if (managedIdentityClientId !== undefined) { diff --git a/sdk/identity/identity/src/index.ts b/sdk/identity/identity/src/index.ts index 55ff135f90e7..1fdf0046f06c 100644 --- a/sdk/identity/identity/src/index.ts +++ b/sdk/identity/identity/src/index.ts @@ -49,6 +49,10 @@ export { UsernamePasswordCredentialOptions } from "./credentials/usernamePasswor export { AuthorizationCodeCredential } from "./credentials/authorizationCodeCredential"; export { AzurePowerShellCredential } from "./credentials/azurePowerShellCredential"; export { AzurePowerShellCredentialOptions } from "./credentials/azurePowerShellCredentialOptions"; +export { + ApplicationCredential, + ApplicationCredentialOptions +} from "./credentials/applicationCredential"; export { VisualStudioCodeCredential, diff --git a/sdk/identity/identity/test/internal/node/applicationCredential.spec.ts b/sdk/identity/identity/test/internal/node/applicationCredential.spec.ts new file mode 100644 index 000000000000..9059c4d2f512 --- /dev/null +++ b/sdk/identity/identity/test/internal/node/applicationCredential.spec.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { RestError } from "@azure/core-rest-pipeline"; +import { ApplicationCredential } from "../../../src"; +import { prepareIdentityTests } from "../../httpRequests"; +import { + createResponse, + IdentityTestContext, + SendCredentialRequests +} from "../../httpRequestsCommon"; + +describe("ApplicationCredential testing Managed Identity (internal)", function() { + let envCopy: string = ""; + let testContext: IdentityTestContext; + let sendCredentialRequests: SendCredentialRequests; + + beforeEach(async () => { + envCopy = JSON.stringify(process.env); + delete process.env.MSI_ENDPOINT; + delete process.env.MSI_SECRET; + delete process.env.AZURE_CLIENT_SECRET; + delete process.env.AZURE_TENANT_ID; + testContext = await prepareIdentityTests({}); + sendCredentialRequests = testContext.sendCredentialRequests; + }); + afterEach(async () => { + const env = JSON.parse(envCopy); + process.env.MSI_ENDPOINT = env.MSI_ENDPOINT; + process.env.MSI_SECRET = env.MSI_SECRET; + process.env.AZURE_CLIENT_SECRET = env.AZURE_CLIENT_SECRET; + process.env.AZURE_TENANT_ID = env.AZURE_TENANT_ID; + await testContext.restore(); + }); + + it("returns error when no MSI is available", async function() { + process.env.AZURE_CLIENT_ID = "errclient"; + + const { error } = await sendCredentialRequests({ + scopes: ["scopes"], + credential: new ApplicationCredential(), + insecureResponses: [ + { + error: new RestError("Request Timeout", { code: "REQUEST_SEND_ERROR", statusCode: 408 }) + } + ] + }); + assert.ok( + error!.message!.indexOf("No MSI credential available") > -1, + "Failed to match the expected error" + ); + }); + + it("an unexpected error bubbles all the way up", async function() { + process.env.AZURE_CLIENT_ID = "errclient"; + + const errorMessage = "ManagedIdentityCredential authentication failed."; + + const { error } = await sendCredentialRequests({ + scopes: ["scopes"], + credential: new ApplicationCredential(), + insecureResponses: [ + createResponse(200), // IMDS Endpoint ping + { error: new RestError(errorMessage, { statusCode: 500 }) } + ] + }); + assert.ok(error?.message.startsWith(errorMessage)); + }); + + it("returns expected error when the network was unreachable", async function() { + process.env.AZURE_CLIENT_ID = "errclient"; + + const netError: RestError = new RestError("Request Timeout", { + code: "ENETUNREACH", + statusCode: 408 + }); + + const { error } = await sendCredentialRequests({ + scopes: ["scopes"], + credential: new ApplicationCredential(), + insecureResponses: [ + createResponse(200), // IMDS Endpoint ping + { error: netError } + ] + }); + assert.ok(error!.message!.indexOf("Network unreachable.") > -1); + }); + + it("sends an authorization request correctly in an App Service environment", async () => { + // Trigger App Service behavior by setting environment variables + process.env.AZURE_CLIENT_ID = "client"; + process.env.MSI_ENDPOINT = "https://endpoint"; + process.env.MSI_SECRET = "secret"; + + const authDetails = await sendCredentialRequests({ + scopes: ["https://service/.default"], + credential: new ApplicationCredential(), + secureResponses: [ + createResponse(200, { + access_token: "token", + expires_on: "06/20/2019 02:57:58 +00:00" + }) + ] + }); + + const authRequest = authDetails.requests[0]; + const query = new URLSearchParams(authRequest.url.split("?")[1]); + + assert.equal(authRequest.method, "GET"); + assert.equal(query.get("clientid"), "client"); + assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); + assert.ok( + authRequest.url.startsWith(process.env.MSI_ENDPOINT), + "URL does not start with expected host and path" + ); + assert.equal(authRequest.headers.secret, process.env.MSI_SECRET); + assert.ok( + authRequest.url.indexOf(`api-version=2017-09-01`) > -1, + "URL does not have expected version" + ); + if (authDetails.result?.token) { + assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000); + } else { + assert.fail("No token was returned!"); + } + }); +}); diff --git a/sdk/identity/identity/test/public/node/applicationCredential.spec.ts b/sdk/identity/identity/test/public/node/applicationCredential.spec.ts new file mode 100644 index 000000000000..32372668fee2 --- /dev/null +++ b/sdk/identity/identity/test/public/node/applicationCredential.spec.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { ApplicationCredential } from "../../../src"; +import { MsalTestCleanup, msalNodeTestSetup, testTracing } from "../../msalTestUtils"; +import { getError } from "../../authTestUtils"; +import { Context } from "mocha"; + +describe("ApplicationCredential", function() { + let cleanup: MsalTestCleanup; + const environmentVariableNames = ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"]; + const cachedValues: Record = {}; + + beforeEach(function(this: Context) { + const setup = msalNodeTestSetup(this); + cleanup = setup.cleanup; + environmentVariableNames.forEach((name) => { + cachedValues[name] = process.env[name]; + delete process.env[name]; + }); + }); + afterEach(async function() { + await cleanup(); + environmentVariableNames.forEach((name) => { + process.env[name] = cachedValues[name]; + }); + }); + + const scope = "https://vault.azure.net/.default"; + + it("authenticates with a client secret on the environment variables", async function() { + // The following environment variables must be set for this to work. + // On TEST_MODE="playback", the recorder automatically fills them with stubbed values. + process.env.AZURE_TENANT_ID = cachedValues.AZURE_TENANT_ID; + process.env.AZURE_CLIENT_ID = cachedValues.AZURE_CLIENT_ID; + process.env.AZURE_CLIENT_SECRET = cachedValues.AZURE_CLIENT_SECRET; + + const credential = new ApplicationCredential(); + + const token = await credential.getToken(scope); + assert.ok(token?.token); + assert.ok(token?.expiresOnTimestamp > Date.now()); + }); + + it( + "supports tracing with environment client secret", + testTracing({ + test: async (tracingOptions) => { + // The following environment variables must be set for this to work. + // On TEST_MODE="playback", the recorder automatically fills them with stubbed values. + process.env.AZURE_TENANT_ID = cachedValues.AZURE_TENANT_ID; + process.env.AZURE_CLIENT_ID = cachedValues.AZURE_CLIENT_ID; + process.env.AZURE_CLIENT_SECRET = cachedValues.AZURE_CLIENT_SECRET; + + const credential = new ApplicationCredential(); + + await credential.getToken(scope, { + tracingOptions + }); + }, + children: [ + { + name: "Azure.Identity.ChainedTokenCredential-getToken", + children: [ + { + name: "Azure.Identity.EnvironmentCredential.getToken", + children: [ + { + name: "Azure.Identity.ClientSecretCredential.getToken", + children: [] + } + ] + } + ] + } + ] + }) + ); + + it("throws an AggregateAuthenticationError when getToken is called and no credential was configured", async () => { + const credential = new ApplicationCredential(); + const error = await getError(credential.getToken(scope)); + assert.equal(error.name, "AggregateAuthenticationError"); + console.log(`${error.message}`); + assert.ok( + error.message.indexOf( + `CredentialUnavailableError: EnvironmentCredential is unavailable. No underlying credential could be used.\nCredentialUnavailableError: ManagedIdentityCredential authentication failed.` + ) > -1 + ); + }); + + it("throws an AuthenticationError when getToken is called and ApplicationCredential authentication failed", async () => { + process.env.AZURE_TENANT_ID = "tenant"; + process.env.AZURE_CLIENT_ID = "client"; + process.env.AZURE_CLIENT_SECRET = "secret"; + + const credential = new ApplicationCredential(); + + const error = await getError(credential.getToken(scope)); + assert.equal(error.name, "AuthenticationError"); + assert.ok(error.message.indexOf("EnvironmentCredential authentication failed.") > -1); + }); +});