@azure/identity
Advanced tools
Comparing version 1.0.0-preview.3 to 1.0.0-preview.4
# Changelog | ||
## 1.0.0-preview.4 - 2019-10-07 | ||
- Introduced the [`AuthorizationCodeCredential`](https://azure.github.io/azure-sdk-for-js/identity/classes/authorizationcodecredential.html) for performing the [authorization code flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) with AAD ([PR #5356](https://github.com/Azure/azure-sdk-for-js/pull/5356)) | ||
- Fixed an issue preventing the `ManagedIdentityCredential` from working inside of Azure Function Apps ([PR #5144](https://github.com/Azure/azure-sdk-for-js/pull/5144)) | ||
- Added tracing to `IdentityClient` and credential implementations ([PR #5283](https://github.com/Azure/azure-sdk-for-js/pull/5283)) | ||
- Improved the exception message for `AggregateAuthenticationError` so that errors thrown from `DefaultAzureCredential` are now more actionable ([PR #5409](https://github.com/Azure/azure-sdk-for-js/pull/5409)) | ||
## 1.0.0-preview.3 - 2019-09-09 | ||
@@ -4,0 +11,0 @@ |
@@ -18,2 +18,6 @@ /** | ||
/** | ||
* The Error.name value of an AuthenticationError | ||
*/ | ||
export declare const AuthenticationErrorName = "AuthenticationError"; | ||
/** | ||
* Provides details about a failure to authenticate with Azure Active | ||
@@ -29,2 +33,6 @@ * Directory. The `errorResponse` field contains more details about | ||
/** | ||
* The Error.name value of an AggregateAuthenticationError | ||
*/ | ||
export declare const AggregateAuthenticationErrorName = "AggregateAuthenticationError"; | ||
/** | ||
* Provides an `errors` array containing {@link AuthenticationError} instance | ||
@@ -31,0 +39,0 @@ * for authentication failures from credentials in a {@link ChainedTokenCredential}. |
@@ -9,2 +9,6 @@ // Copyright (c) Microsoft Corporation. | ||
/** | ||
* The Error.name value of an AuthenticationError | ||
*/ | ||
export const AuthenticationErrorName = "AuthenticationError"; | ||
/** | ||
* Provides details about a failure to authenticate with Azure Active | ||
@@ -54,6 +58,10 @@ * Directory. The `errorResponse` field contains more details about | ||
// Ensure that this type reports the correct name | ||
this.name = "AuthenticationError"; | ||
this.name = AuthenticationErrorName; | ||
} | ||
} | ||
/** | ||
* The Error.name value of an AggregateAuthenticationError | ||
*/ | ||
export const AggregateAuthenticationErrorName = "AggregateAuthenticationError"; | ||
/** | ||
* Provides an `errors` array containing {@link AuthenticationError} instance | ||
@@ -64,8 +72,8 @@ * for authentication failures from credentials in a {@link ChainedTokenCredential}. | ||
constructor(errors) { | ||
super("Authentication failed to complete due to errors"); | ||
super(`Authentication failed to complete due to the following errors:\n\n${errors.join("\n\n")}`); | ||
this.errors = errors; | ||
// Ensure that this type reports the correct name | ||
this.name = "AggregateAuthenticationError"; | ||
this.name = AggregateAuthenticationErrorName; | ||
} | ||
} | ||
//# sourceMappingURL=errors.js.map |
@@ -5,4 +5,6 @@ // Copyright (c) Microsoft Corporation. | ||
import qs from "qs"; | ||
import { ServiceClient, WebResource } from "@azure/core-http"; | ||
import { AuthenticationError } from "./errors"; | ||
import { ServiceClient, WebResource, tracingPolicy } from "@azure/core-http"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
import { AuthenticationError, AuthenticationErrorName } from "./errors"; | ||
import { createSpan } from "../util/tracing"; | ||
const DefaultAuthorityHost = "https://login.microsoftonline.com"; | ||
@@ -50,2 +52,3 @@ export class IdentityClient extends ServiceClient { | ||
} | ||
const { span, options: newOptions } = createSpan("IdentityClient-refreshAccessToken", options); | ||
const refreshParams = { | ||
@@ -60,19 +63,21 @@ grant_type: "refresh_token", | ||
} | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
try { | ||
return yield this.sendTokenRequest(webResource, expiresOnParser); | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
spanOptions: newOptions.spanOptions, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const response = yield this.sendTokenRequest(webResource, expiresOnParser); | ||
return response; | ||
} | ||
catch (err) { | ||
if (err instanceof AuthenticationError && | ||
if (err.name === AuthenticationErrorName && | ||
err.errorResponse.error === "interaction_required") { | ||
@@ -82,8 +87,19 @@ // It's likely that the refresh token has expired, so | ||
// initiate the authentication flow again. | ||
span.setStatus({ | ||
code: CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
return null; | ||
} | ||
else { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -93,3 +109,6 @@ } | ||
return { | ||
authorityHost: DefaultAuthorityHost | ||
authorityHost: DefaultAuthorityHost, | ||
requestPolicyFactories: (factories) => { | ||
return [tracingPolicy(), ...factories]; | ||
} | ||
}; | ||
@@ -96,0 +115,0 @@ } |
@@ -5,2 +5,4 @@ // Copyright (c) Microsoft Corporation. | ||
import { AggregateAuthenticationError } from "../client/errors"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -29,5 +31,6 @@ * Enables multiple {@link TokenCredential} implementations to be tried in order | ||
const errors = []; | ||
const { span, options: newOptions } = createSpan("ChainedTokenCredential-getToken", options); | ||
for (let i = 0; i < this._sources.length && token === null; i++) { | ||
try { | ||
token = yield this._sources[i].getToken(scopes, options); | ||
token = yield this._sources[i].getToken(scopes, newOptions); | ||
} | ||
@@ -39,4 +42,10 @@ catch (err) { | ||
if (!token && errors.length > 0) { | ||
throw new AggregateAuthenticationError(errors); | ||
const err = new AggregateAuthenticationError(errors); | ||
span.setStatus({ | ||
code: CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
span.end(); | ||
return token; | ||
@@ -43,0 +52,0 @@ }); |
@@ -10,2 +10,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
const SelfSignedJwtLifetimeMins = 10; | ||
@@ -66,43 +69,60 @@ function timestampInSeconds(date) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("ClientCertificateCredential-getToken", options); | ||
try { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -109,0 +129,0 @@ } |
@@ -6,2 +6,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -44,22 +47,39 @@ * Enables authentication to Azure Active Directory using a client secret | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("ClientSecretCredential-getToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -66,0 +86,0 @@ } |
@@ -7,3 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { AuthenticationError } from "../client/errors"; | ||
import { AuthenticationError, AuthenticationErrorName } from "../client/errors"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -33,22 +35,39 @@ * Enables authentication to Azure Active Directory using a device code | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const response = yield this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-sendDeviceCodeRequest", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const response = yield this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
} | ||
return response.parsedBody; | ||
} | ||
return response.parsedBody; | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -59,46 +78,65 @@ } | ||
let tokenResponse = null; | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
while (tokenResponse === null) { | ||
try { | ||
yield delay(deviceCodeResponse.interval * 1000); | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-pollForToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
while (tokenResponse === null) { | ||
try { | ||
yield delay(deviceCodeResponse.interval * 1000); | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
} | ||
tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
} | ||
tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
} | ||
catch (err) { | ||
if (err instanceof AuthenticationError) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
catch (err) { | ||
if (err.name === AuthenticationErrorName) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
default: // Any other error should be rethrown | ||
throw err; | ||
} | ||
} | ||
else { | ||
throw err; | ||
} | ||
} | ||
else { | ||
throw err; | ||
} | ||
} | ||
return tokenResponse; | ||
} | ||
return tokenResponse; | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -118,23 +156,39 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
let tokenResponse = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-getToken", options); | ||
try { | ||
let tokenResponse = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = yield this.identityClient.refreshAccessToken(this.tenantId, this.clientId, scopeString, this.lastTokenResponse.refreshToken, undefined, // clientSecret not needed for device code auth | ||
undefined, newOptions); | ||
} | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = yield this.sendDeviceCodeRequest(scopeString, newOptions); | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
}); | ||
tokenResponse = yield this.pollForToken(deviceCodeResponse, newOptions); | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = yield this.identityClient.refreshAccessToken(this.tenantId, this.clientId, scopeString, this.lastTokenResponse.refreshToken, undefined, // clientSecret not needed for device code auth | ||
undefined, options); | ||
} | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = yield this.sendDeviceCodeRequest(scopeString, options); | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
tokenResponse = yield this.pollForToken(deviceCodeResponse, options); | ||
throw err; | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -141,0 +195,0 @@ } |
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
import { ClientSecretCredential } from "./clientSecretCredential"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -43,5 +46,23 @@ * Enables authentication to Azure Active Directory using client secret | ||
getToken(scopes, options) { | ||
const { span, options: newOptions } = createSpan("EnvironmentCredential-getToken", options); | ||
if (this._credential) { | ||
return this._credential.getToken(scopes, options); | ||
try { | ||
return this._credential.getToken(scopes, newOptions); | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
} | ||
span.setStatus({ code: CanonicalCode.UNAUTHENTICATED }); | ||
span.end(); | ||
return Promise.resolve(null); | ||
@@ -48,0 +69,0 @@ } |
@@ -6,2 +6,4 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -92,19 +94,31 @@ * Enables authentication to Azure Active Directory inside of the web browser | ||
*/ | ||
getToken(scopes, options // eslint-disable-line @typescript-eslint/no-unused-vars | ||
) { | ||
getToken(scopes, options) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
if (!this.msalObject.getAccount()) { | ||
yield this.login(); | ||
const { span } = createSpan("InteractiveBrowserCredential-getToken", options); | ||
try { | ||
if (!this.msalObject.getAccount()) { | ||
yield this.login(); | ||
} | ||
const authResponse = yield this.acquireToken({ | ||
scopes: Array.isArray(scopes) ? scopes : scopes.split(",") | ||
}); | ||
if (authResponse) { | ||
return { | ||
token: authResponse.accessToken, | ||
expiresOnTimestamp: authResponse.expiresOn.getTime() | ||
}; | ||
} | ||
else { | ||
return null; | ||
} | ||
} | ||
const authResponse = yield this.acquireToken({ | ||
scopes: Array.isArray(scopes) ? scopes : scopes.split(",") | ||
}); | ||
if (authResponse) { | ||
return { | ||
token: authResponse.accessToken, | ||
expiresOnTimestamp: authResponse.expiresOn.getTime() | ||
}; | ||
catch (err) { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
else { | ||
return null; | ||
finally { | ||
span.end(); | ||
} | ||
@@ -111,0 +125,0 @@ }); |
@@ -7,2 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
const DefaultScopeSuffix = "/.default"; | ||
@@ -97,4 +100,5 @@ export const ImdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"; | ||
} | ||
pingImdsEndpoint(resource, clientId, timeout) { | ||
pingImdsEndpoint(resource, clientId, getTokenOptions) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const { span, options } = createSpan("ManagedIdentityCredential-pingImdsEndpoint", getTokenOptions); | ||
const request = this.createImdsAuthRequest(resource, clientId); | ||
@@ -107,24 +111,37 @@ // This will always be populated, but let's make TypeScript happy | ||
} | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
if (timeout) { | ||
webResource.timeout = timeout; | ||
} | ||
else { | ||
webResource.timeout = 500; | ||
} | ||
request.spanOptions = options.spanOptions; | ||
try { | ||
yield this.identityClient.sendRequest(webResource); | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
webResource.timeout = options.timeout || 500; | ||
try { | ||
yield this.identityClient.sendRequest(webResource); | ||
} | ||
catch (err) { | ||
if (err instanceof RestError && | ||
(err.code === RestError.REQUEST_SEND_ERROR || | ||
err.code === RestError.REQUEST_ABORTED_ERROR)) { | ||
// Either request failed or IMDS endpoint isn't available | ||
span.setStatus({ | ||
code: CanonicalCode.UNAVAILABLE, | ||
message: err.message | ||
}); | ||
return false; | ||
} | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
} | ||
catch (err) { | ||
if (err instanceof RestError && | ||
(err.code === RestError.REQUEST_SEND_ERROR || err.code === RestError.REQUEST_ABORTED_ERROR)) { | ||
// Either request failed or IMDS endpoint isn't available | ||
return false; | ||
} | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -137,35 +154,50 @@ } | ||
let expiresInParser; | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody) => { | ||
// Parse a date format like "06/20/2019 02:57:58 +00:00" and | ||
// convert it into a JavaScript-formatted date | ||
const m = requestBody.expires_on.match(/(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d) (\+|-)(\d\d):(\d\d)/); | ||
return Date.parse(`${m[3]}-${m[1]}-${m[2]}T${m[4]}:${m[5]}:${m[6]}${m[7]}${m[8]}:${m[9]}`); | ||
}; | ||
const { span, options } = createSpan("ManagedIdentityCredential-authenticateManagedIdentity", getTokenOptions); | ||
try { | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody) => { | ||
// 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); | ||
}; | ||
} | ||
else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
} | ||
} | ||
else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
// Ping the IMDS endpoint to see if it's available | ||
if (!checkIfImdsEndpointAvailable || | ||
(yield this.pingImdsEndpoint(resource, clientId, options))) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} | ||
else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
} | ||
const webResource = this.identityClient.createWebResource(Object.assign({ disableJsonStringifyOnBody: true, deserializationMapper: undefined, abortSignal: options.abortSignal, spanOptions: options.spanOptions }, authRequestOptions)); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource, expiresInParser); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
else { | ||
// Ping the IMDS endpoint to see if it's available | ||
if (!checkIfImdsEndpointAvailable || | ||
(yield this.pingImdsEndpoint(resource, clientId, getTokenOptions ? getTokenOptions.timeout : undefined))) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} | ||
else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
const webResource = this.identityClient.createWebResource(Object.assign({ disableJsonStringifyOnBody: true, deserializationMapper: undefined, abortSignal: getTokenOptions && getTokenOptions.abortSignal }, authRequestOptions)); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource, expiresInParser); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -186,13 +218,26 @@ } | ||
let result = null; | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = yield this.authenticateManagedIdentity(scopes, this.isEndpointUnavailable === null, this.clientId, options); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
const { span, options: newOptions } = createSpan("ManagedIdentityCredential-getToken", options); | ||
try { | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = yield this.authenticateManagedIdentity(scopes, this.isEndpointUnavailable === null, this.clientId, newOptions); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
} | ||
return result; | ||
} | ||
return result; | ||
catch (err) { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -199,0 +244,0 @@ } |
@@ -6,2 +6,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
/** | ||
@@ -44,23 +47,40 @@ * Enables authentication to Azure Active Directory with a user's | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("UsernamePasswordCredential-getToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -67,0 +87,0 @@ } |
@@ -13,5 +13,6 @@ import { TokenCredential } from "@azure/core-http"; | ||
export { UsernamePasswordCredential } from "./credentials/usernamePasswordCredential"; | ||
export { AuthenticationError, AggregateAuthenticationError } from "./client/errors"; | ||
export { AuthorizationCodeCredential } from "./credentials/authorizationCodeCredential"; | ||
export { AuthenticationError, AggregateAuthenticationError, AuthenticationErrorName, AggregateAuthenticationErrorName } from "./client/errors"; | ||
export { TokenCredential, GetTokenOptions, AccessToken } from "@azure/core-http"; | ||
export declare function getDefaultAzureCredential(): TokenCredential; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -13,3 +13,4 @@ // Copyright (c) Microsoft Corporation. | ||
export { UsernamePasswordCredential } from "./credentials/usernamePasswordCredential"; | ||
export { AuthenticationError, AggregateAuthenticationError } from "./client/errors"; | ||
export { AuthorizationCodeCredential } from "./credentials/authorizationCodeCredential"; | ||
export { AuthenticationError, AggregateAuthenticationError, AuthenticationErrorName, AggregateAuthenticationErrorName } from "./client/errors"; | ||
export function getDefaultAzureCredential() { | ||
@@ -16,0 +17,0 @@ return new DefaultAzureCredential(); |
@@ -8,2 +8,3 @@ 'use strict'; | ||
var tslib_1 = require('tslib'); | ||
var coreTracing = require('@azure/core-tracing'); | ||
var qs = _interopDefault(require('qs')); | ||
@@ -24,2 +25,6 @@ var coreHttp = require('@azure/core-http'); | ||
/** | ||
* The Error.name value of an AuthenticationError | ||
*/ | ||
const AuthenticationErrorName = "AuthenticationError"; | ||
/** | ||
* Provides details about a failure to authenticate with Azure Active | ||
@@ -69,6 +74,10 @@ * Directory. The `errorResponse` field contains more details about | ||
// Ensure that this type reports the correct name | ||
this.name = "AuthenticationError"; | ||
this.name = AuthenticationErrorName; | ||
} | ||
} | ||
/** | ||
* The Error.name value of an AggregateAuthenticationError | ||
*/ | ||
const AggregateAuthenticationErrorName = "AggregateAuthenticationError"; | ||
/** | ||
* Provides an `errors` array containing {@link AuthenticationError} instance | ||
@@ -79,6 +88,6 @@ * for authentication failures from credentials in a {@link ChainedTokenCredential}. | ||
constructor(errors) { | ||
super("Authentication failed to complete due to errors"); | ||
super(`Authentication failed to complete due to the following errors:\n\n${errors.join("\n\n")}`); | ||
this.errors = errors; | ||
// Ensure that this type reports the correct name | ||
this.name = "AggregateAuthenticationError"; | ||
this.name = AggregateAuthenticationErrorName; | ||
} | ||
@@ -89,2 +98,23 @@ } | ||
/** | ||
* Creates a span using the global tracer. | ||
* @param name The name of the operation being performed. | ||
* @param options The options for the underlying http request. | ||
*/ | ||
function createSpan(operationName, options = {}) { | ||
const tracer = coreTracing.getTracer(); | ||
const spanOptions = Object.assign({}, options.spanOptions, { kind: coreTracing.SpanKind.CLIENT }); | ||
const span = tracer.startSpan(`Azure.Identity.${operationName}`, spanOptions); | ||
span.setAttribute("component", "identity"); | ||
let newOptions = options; | ||
if (span.isRecordingEvents()) { | ||
newOptions = Object.assign({}, options, { spanOptions: Object.assign({}, options.spanOptions, { parent: span }) }); | ||
} | ||
return { | ||
span, | ||
options: newOptions | ||
}; | ||
} | ||
// Copyright (c) Microsoft Corporation. | ||
/** | ||
* Enables multiple {@link TokenCredential} implementations to be tried in order | ||
@@ -112,5 +142,6 @@ * until one of the getToken methods returns an {@link AccessToken}. | ||
const errors = []; | ||
const { span, options: newOptions } = createSpan("ChainedTokenCredential-getToken", options); | ||
for (let i = 0; i < this._sources.length && token === null; i++) { | ||
try { | ||
token = yield this._sources[i].getToken(scopes, options); | ||
token = yield this._sources[i].getToken(scopes, newOptions); | ||
} | ||
@@ -122,4 +153,10 @@ catch (err) { | ||
if (!token && errors.length > 0) { | ||
throw new AggregateAuthenticationError(errors); | ||
const err = new AggregateAuthenticationError(errors); | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
span.end(); | ||
return token; | ||
@@ -173,2 +210,3 @@ }); | ||
} | ||
const { span, options: newOptions } = createSpan("IdentityClient-refreshAccessToken", options); | ||
const refreshParams = { | ||
@@ -183,19 +221,21 @@ grant_type: "refresh_token", | ||
} | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
try { | ||
return yield this.sendTokenRequest(webResource, expiresOnParser); | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
spanOptions: newOptions.spanOptions, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const response = yield this.sendTokenRequest(webResource, expiresOnParser); | ||
return response; | ||
} | ||
catch (err) { | ||
if (err instanceof AuthenticationError && | ||
if (err.name === AuthenticationErrorName && | ||
err.errorResponse.error === "interaction_required") { | ||
@@ -205,8 +245,19 @@ // It's likely that the refresh token has expired, so | ||
// initiate the authentication flow again. | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
return null; | ||
} | ||
else { | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -216,3 +267,6 @@ } | ||
return { | ||
authorityHost: DefaultAuthorityHost | ||
authorityHost: DefaultAuthorityHost, | ||
requestPolicyFactories: (factories) => { | ||
return [coreHttp.tracingPolicy(), ...factories]; | ||
} | ||
}; | ||
@@ -260,22 +314,39 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("ClientSecretCredential-getToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -325,5 +396,23 @@ } | ||
getToken(scopes, options) { | ||
const { span, options: newOptions } = createSpan("EnvironmentCredential-getToken", options); | ||
if (this._credential) { | ||
return this._credential.getToken(scopes, options); | ||
try { | ||
return this._credential.getToken(scopes, newOptions); | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
} | ||
span.setStatus({ code: coreTracing.CanonicalCode.UNAUTHENTICATED }); | ||
span.end(); | ||
return Promise.resolve(null); | ||
@@ -423,4 +512,5 @@ } | ||
} | ||
pingImdsEndpoint(resource, clientId, timeout) { | ||
pingImdsEndpoint(resource, clientId, getTokenOptions) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const { span, options } = createSpan("ManagedIdentityCredential-pingImdsEndpoint", getTokenOptions); | ||
const request = this.createImdsAuthRequest(resource, clientId); | ||
@@ -433,24 +523,37 @@ // This will always be populated, but let's make TypeScript happy | ||
} | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
if (timeout) { | ||
webResource.timeout = timeout; | ||
} | ||
else { | ||
webResource.timeout = 500; | ||
} | ||
request.spanOptions = options.spanOptions; | ||
try { | ||
yield this.identityClient.sendRequest(webResource); | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
webResource.timeout = options.timeout || 500; | ||
try { | ||
yield this.identityClient.sendRequest(webResource); | ||
} | ||
catch (err) { | ||
if (err instanceof coreHttp.RestError && | ||
(err.code === coreHttp.RestError.REQUEST_SEND_ERROR || | ||
err.code === coreHttp.RestError.REQUEST_ABORTED_ERROR)) { | ||
// Either request failed or IMDS endpoint isn't available | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNAVAILABLE, | ||
message: err.message | ||
}); | ||
return false; | ||
} | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
} | ||
catch (err) { | ||
if (err instanceof coreHttp.RestError && | ||
(err.code === coreHttp.RestError.REQUEST_SEND_ERROR || err.code === coreHttp.RestError.REQUEST_ABORTED_ERROR)) { | ||
// Either request failed or IMDS endpoint isn't available | ||
return false; | ||
} | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -463,35 +566,50 @@ } | ||
let expiresInParser; | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody) => { | ||
// Parse a date format like "06/20/2019 02:57:58 +00:00" and | ||
// convert it into a JavaScript-formatted date | ||
const m = requestBody.expires_on.match(/(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d) (\+|-)(\d\d):(\d\d)/); | ||
return Date.parse(`${m[3]}-${m[1]}-${m[2]}T${m[4]}:${m[5]}:${m[6]}${m[7]}${m[8]}:${m[9]}`); | ||
}; | ||
const { span, options } = createSpan("ManagedIdentityCredential-authenticateManagedIdentity", getTokenOptions); | ||
try { | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody) => { | ||
// 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); | ||
}; | ||
} | ||
else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
} | ||
} | ||
else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
// Ping the IMDS endpoint to see if it's available | ||
if (!checkIfImdsEndpointAvailable || | ||
(yield this.pingImdsEndpoint(resource, clientId, options))) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} | ||
else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
} | ||
const webResource = this.identityClient.createWebResource(Object.assign({ disableJsonStringifyOnBody: true, deserializationMapper: undefined, abortSignal: options.abortSignal, spanOptions: options.spanOptions }, authRequestOptions)); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource, expiresInParser); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
else { | ||
// Ping the IMDS endpoint to see if it's available | ||
if (!checkIfImdsEndpointAvailable || | ||
(yield this.pingImdsEndpoint(resource, clientId, getTokenOptions ? getTokenOptions.timeout : undefined))) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} | ||
else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
const webResource = this.identityClient.createWebResource(Object.assign({ disableJsonStringifyOnBody: true, deserializationMapper: undefined, abortSignal: getTokenOptions && getTokenOptions.abortSignal }, authRequestOptions)); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource, expiresInParser); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -512,13 +630,26 @@ } | ||
let result = null; | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = yield this.authenticateManagedIdentity(scopes, this.isEndpointUnavailable === null, this.clientId, options); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
const { span, options: newOptions } = createSpan("ManagedIdentityCredential-getToken", options); | ||
try { | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = yield this.authenticateManagedIdentity(scopes, this.isEndpointUnavailable === null, this.clientId, newOptions); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
} | ||
return result; | ||
} | ||
return result; | ||
catch (err) { | ||
span.setStatus({ | ||
code: coreTracing.CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -607,43 +738,60 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("ClientCertificateCredential-getToken", options); | ||
try { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -695,22 +843,39 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const response = yield this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-sendDeviceCodeRequest", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const response = yield this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
} | ||
return response.parsedBody; | ||
} | ||
return response.parsedBody; | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -721,46 +886,65 @@ } | ||
let tokenResponse = null; | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
while (tokenResponse === null) { | ||
try { | ||
yield coreHttp.delay(deviceCodeResponse.interval * 1000); | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-pollForToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
while (tokenResponse === null) { | ||
try { | ||
yield coreHttp.delay(deviceCodeResponse.interval * 1000); | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
} | ||
tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
} | ||
tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
} | ||
catch (err) { | ||
if (err instanceof AuthenticationError) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
catch (err) { | ||
if (err.name === AuthenticationErrorName) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
default: // Any other error should be rethrown | ||
throw err; | ||
} | ||
} | ||
else { | ||
throw err; | ||
} | ||
} | ||
else { | ||
throw err; | ||
} | ||
} | ||
return tokenResponse; | ||
} | ||
return tokenResponse; | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -780,23 +964,39 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
let tokenResponse = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-getToken", options); | ||
try { | ||
let tokenResponse = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = yield this.identityClient.refreshAccessToken(this.tenantId, this.clientId, scopeString, this.lastTokenResponse.refreshToken, undefined, // clientSecret not needed for device code auth | ||
undefined, newOptions); | ||
} | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = yield this.sendDeviceCodeRequest(scopeString, newOptions); | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
}); | ||
tokenResponse = yield this.pollForToken(deviceCodeResponse, newOptions); | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = yield this.identityClient.refreshAccessToken(this.tenantId, this.clientId, scopeString, this.lastTokenResponse.refreshToken, undefined, // clientSecret not needed for device code auth | ||
undefined, options); | ||
} | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = yield this.sendDeviceCodeRequest(scopeString, options); | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
tokenResponse = yield this.pollForToken(deviceCodeResponse, options); | ||
throw err; | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -844,23 +1044,40 @@ } | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const { span, options: newOptions } = createSpan("UsernamePasswordCredential-getToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
@@ -871,2 +1088,109 @@ } | ||
// Copyright (c) Microsoft Corporation. | ||
/** | ||
* Enables authentication to Azure Active Directory using an authorization code | ||
* that was obtained through the authorization code flow, described in more detail | ||
* in the Azure Active Directory documentation: | ||
* | ||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow | ||
*/ | ||
class AuthorizationCodeCredential { | ||
/** | ||
* Creates an instance of CodeFlowCredential with the details needed | ||
* to request an access token using an authentication that was obtained | ||
* from Azure Active Directory. | ||
* | ||
* It is currently necessary for the user of this credential to initiate | ||
* the authorization code flow to obtain an authorization code to be used | ||
* with this credential. A full example of this flow is provided here: | ||
* | ||
* https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/samples/authorizationCodeSample.ts | ||
* | ||
* @param tenantId The Azure Active Directory tenant (directory) ID or name. | ||
* @param clientId The client (application) ID of an App Registration in the tenant. | ||
* @param clientSecret A client secret that was generated for the App Registration or | ||
'undefined' if using this credential in a desktop or mobile | ||
application. | ||
* @param authorizationCode An authorization code that was received from following the | ||
authorization code flow. This authorization code must not | ||
have already been used to obtain an access token. | ||
* @param redirectUri The redirect URI that was used to request the authorization code. | ||
Must be the same URI that is configured for the App Registration. | ||
* @param options Options for configuring the client which makes the access token request. | ||
*/ | ||
constructor(tenantId, clientId, clientSecret, authorizationCode, redirectUri, options) { | ||
this.lastTokenResponse = null; | ||
this.identityClient = new IdentityClient(options); | ||
this.tenantId = tenantId; | ||
this.clientId = clientId; | ||
this.clientSecret = clientSecret; | ||
this.authorizationCode = authorizationCode; | ||
this.redirectUri = redirectUri; | ||
} | ||
/** | ||
* Authenticates with Azure Active Directory and returns an {@link AccessToken} if | ||
* successful. If authentication cannot be performed at this time, this method may | ||
* return null. If an error occurs during authentication, an {@link AuthenticationError} | ||
* containing failure details will be thrown. | ||
* | ||
* @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. | ||
*/ | ||
getToken(scopes, options) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function* () { | ||
const { span, options: newOptions } = createSpan("AuthorizationCodeCredential-getToken", options); | ||
try { | ||
let tokenResponse = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = yield this.identityClient.refreshAccessToken(this.tenantId, this.clientId, scopeString, this.lastTokenResponse.refreshToken, this.clientSecret, undefined, newOptions); | ||
} | ||
if (tokenResponse === null) { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
grant_type: "authorization_code", | ||
scope: scopeString, | ||
code: this.authorizationCode, | ||
redirect_uri: this.redirectUri, | ||
client_secret: this.clientSecret | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
tokenResponse = yield this.identityClient.sendTokenRequest(webResource); | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
catch (err) { | ||
const code = err.name === AuthenticationErrorName | ||
? coreTracing.CanonicalCode.UNAUTHENTICATED | ||
: coreTracing.CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
finally { | ||
span.end(); | ||
} | ||
}); | ||
} | ||
} | ||
// Copyright (c) Microsoft Corporation. | ||
function getDefaultAzureCredential() { | ||
@@ -877,3 +1201,6 @@ return new DefaultAzureCredential(); | ||
exports.AggregateAuthenticationError = AggregateAuthenticationError; | ||
exports.AggregateAuthenticationErrorName = AggregateAuthenticationErrorName; | ||
exports.AuthenticationError = AuthenticationError; | ||
exports.AuthenticationErrorName = AuthenticationErrorName; | ||
exports.AuthorizationCodeCredential = AuthorizationCodeCredential; | ||
exports.ChainedTokenCredential = ChainedTokenCredential; | ||
@@ -880,0 +1207,0 @@ exports.ClientCertificateCredential = ClientCertificateCredential; |
{ | ||
"name": "@azure/identity", | ||
"sdk-type": "client", | ||
"version": "1.0.0-preview.3", | ||
"version": "1.0.0-preview.4", | ||
"description": "Provides credential implementations for Azure SDK libraries that can authenticate with Azure Active Directory", | ||
@@ -15,2 +15,3 @@ "main": "dist/index.js", | ||
"./dist-esm/src/credentials/deviceCodeCredential.js": "./dist-esm/src/credentials/deviceCodeCredential.browser.js", | ||
"./dist-esm/src/credentials/authorizationCodeCredential.js": "./dist-esm/src/credentials/authorizationCodeCredential.browser.js", | ||
"./dist-esm/src/credentials/interactiveBrowserCredential.js": "./dist-esm/src/credentials/interactiveBrowserCredential.browser.js", | ||
@@ -74,3 +75,4 @@ "./dist/index.js": "./browser/index.js" | ||
"dependencies": { | ||
"@azure/core-http": "1.0.0-preview.3", | ||
"@azure/core-http": "1.0.0-preview.4", | ||
"@azure/core-tracing": "1.0.0-preview.3", | ||
"events": "^3.0.0", | ||
@@ -85,2 +87,3 @@ "jws": "^3.2.2", | ||
"@azure/abort-controller": "1.0.0-preview.2", | ||
"@types/express": "^4.16.0", | ||
"@types/jws": "^3.2.0", | ||
@@ -96,2 +99,3 @@ "@types/mocha": "^5.2.5", | ||
"eslint": "^6.1.0", | ||
"express": "^4.16.3", | ||
"inherits": "^2.0.3", | ||
@@ -111,2 +115,3 @@ "karma": "^4.0.1", | ||
"mocha-multi": "^1.1.3", | ||
"open": "^6.4.0", | ||
"prettier": "^1.16.4", | ||
@@ -113,0 +118,0 @@ "puppeteer": "^1.11.0", |
@@ -42,4 +42,5 @@ ## Azure Identity client library for JS | ||
| [`DeviceCodeCredential`][6] | app registration details | constructor parameters | | ||
| [`InteractiveBrowserCredential`][7]| app registration details | constructor parameters | | ||
| [`UsernamePasswordCredential`][8] | user principal | constructor parameters | | ||
| [`AuthorizationCodeCredential`][7] | app registration details | constructor parameters | | ||
| [`InteractiveBrowserCredential`][8]| app registration details | constructor parameters | | ||
| [`UsernamePasswordCredential`][9] | user principal | constructor parameters | | ||
@@ -80,3 +81,3 @@ Credentials can be chained and tried in turn until one succeeds; see [chaining credentials](#chaining-credentials) for details. | ||
### Authenticating as a service principal: | ||
### Authenticating as a service principal | ||
@@ -101,4 +102,8 @@ ```javascript | ||
### Chaining credentials: | ||
### Using the `AuthorizationCodeCredential` | ||
The `AuthorizationCodeCredential` takes more up-front work to use than the other credential types at this time. A full sample demonstrating how to use this credential can be found in [`samples/authorizationCodeSample.ts`](samples/authorizationCodeSample.ts). | ||
### Chaining credentials | ||
```javascript | ||
@@ -156,3 +161,4 @@ const { ClientSecretCredential, ChainedTokenCredential } = require("@azure/identity"); | ||
[6]: https://azure.github.io/azure-sdk-for-js/identity/classes/devicecodecredential.html | ||
[7]: https://azure.github.io/azure-sdk-for-js/identity/classes/interactivebrowsercredential.html | ||
[8]: https://azure.github.io/azure-sdk-for-js/identity/classes/usernamepasswordcredential.html | ||
[7]: https://azure.github.io/azure-sdk-for-js/identity/classes/authorizationcodecredential.html | ||
[8]: https://azure.github.io/azure-sdk-for-js/identity/classes/interactivebrowsercredential.html | ||
[9]: https://azure.github.io/azure-sdk-for-js/identity/classes/usernamepasswordcredential.html |
@@ -30,2 +30,7 @@ // Copyright (c) Microsoft Corporation. | ||
/** | ||
* The Error.name value of an AuthenticationError | ||
*/ | ||
export const AuthenticationErrorName = "AuthenticationError"; | ||
/** | ||
* Provides details about a failure to authenticate with Azure Active | ||
@@ -83,3 +88,3 @@ * Directory. The `errorResponse` field contains more details about | ||
// Ensure that this type reports the correct name | ||
this.name = "AuthenticationError"; | ||
this.name = AuthenticationErrorName; | ||
} | ||
@@ -89,2 +94,7 @@ } | ||
/** | ||
* The Error.name value of an AggregateAuthenticationError | ||
*/ | ||
export const AggregateAuthenticationErrorName = "AggregateAuthenticationError"; | ||
/** | ||
* Provides an `errors` array containing {@link AuthenticationError} instance | ||
@@ -96,8 +106,10 @@ * for authentication failures from credentials in a {@link ChainedTokenCredential}. | ||
constructor(errors: any[]) { | ||
super("Authentication failed to complete due to errors"); | ||
super( | ||
`Authentication failed to complete due to the following errors:\n\n${errors.join("\n\n")}` | ||
); | ||
this.errors = errors; | ||
// Ensure that this type reports the correct name | ||
this.name = "AggregateAuthenticationError"; | ||
this.name = AggregateAuthenticationErrorName; | ||
} | ||
} |
@@ -11,5 +11,9 @@ // Copyright (c) Microsoft Corporation. | ||
RequestPrepareOptions, | ||
GetTokenOptions | ||
GetTokenOptions, | ||
tracingPolicy, | ||
RequestPolicyFactory | ||
} from "@azure/core-http"; | ||
import { AuthenticationError } from "./errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
import { AuthenticationError, AuthenticationErrorName } from "./errors"; | ||
import { createSpan } from "../util/tracing"; | ||
@@ -92,2 +96,4 @@ const DefaultAuthorityHost = "https://login.microsoftonline.com"; | ||
const { span, options: newOptions } = createSpan("IdentityClient-refreshAccessToken", options); | ||
const refreshParams = { | ||
@@ -104,20 +110,22 @@ grant_type: "refresh_token", | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
try { | ||
const webResource = this.createWebResource({ | ||
url: `${this.authorityHost}/${tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify(refreshParams), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
spanOptions: newOptions.spanOptions, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
try { | ||
return await this.sendTokenRequest(webResource, expiresOnParser); | ||
const response = await this.sendTokenRequest(webResource, expiresOnParser); | ||
return response; | ||
} catch (err) { | ||
if ( | ||
err instanceof AuthenticationError && | ||
err.name === AuthenticationErrorName && | ||
err.errorResponse.error === "interaction_required" | ||
@@ -128,6 +136,16 @@ ) { | ||
// initiate the authentication flow again. | ||
span.setStatus({ | ||
code: CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
return null; | ||
} else { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
} finally { | ||
span.end(); | ||
} | ||
@@ -138,3 +156,6 @@ } | ||
return { | ||
authorityHost: DefaultAuthorityHost | ||
authorityHost: DefaultAuthorityHost, | ||
requestPolicyFactories: (factories: RequestPolicyFactory[]) => { | ||
return [tracingPolicy(), ...factories]; | ||
} | ||
}; | ||
@@ -141,0 +162,0 @@ } |
@@ -6,2 +6,4 @@ // Copyright (c) Microsoft Corporation. | ||
import { AggregateAuthenticationError } from "../client/errors"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -36,5 +38,7 @@ /** | ||
const { span, options: newOptions } = createSpan("ChainedTokenCredential-getToken", options); | ||
for (let i = 0; i < this._sources.length && token === null; i++) { | ||
try { | ||
token = await this._sources[i].getToken(scopes, options); | ||
token = await this._sources[i].getToken(scopes, newOptions); | ||
} catch (err) { | ||
@@ -46,7 +50,14 @@ errors.push(err); | ||
if (!token && errors.length > 0) { | ||
throw new AggregateAuthenticationError(errors); | ||
const err = new AggregateAuthenticationError(errors); | ||
span.setStatus({ | ||
code: CanonicalCode.UNAUTHENTICATED, | ||
message: err.message | ||
}); | ||
throw err; | ||
} | ||
span.end(); | ||
return token; | ||
} | ||
} |
@@ -11,2 +11,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClientOptions, IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -90,48 +93,67 @@ const SelfSignedJwtLifetimeMins = 10; | ||
): Promise<AccessToken | null> { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header: jws.Header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const { span, options: newOptions } = createSpan( | ||
"ClientCertificateCredential-getToken", | ||
options | ||
); | ||
try { | ||
const tokenId = uuid.v4(); | ||
const audienceUrl = `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`; | ||
const header: jws.Header = { | ||
typ: "JWT", | ||
alg: "RS256", | ||
x5t: this.certificateX5t | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const payload = { | ||
iss: this.clientId, | ||
sub: this.clientId, | ||
aud: audienceUrl, | ||
jti: tokenId, | ||
nbf: timestampInSeconds(new Date()), | ||
exp: timestampInSeconds(addMinutes(new Date(), SelfSignedJwtLifetimeMins)) | ||
}; | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const clientAssertion = jws.sign({ | ||
header, | ||
payload, | ||
secret: this.certificateString | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: audienceUrl, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
client_assertion: clientAssertion, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
} |
@@ -7,2 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClientOptions, IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -59,24 +62,40 @@ /** | ||
): Promise<AccessToken | null> { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const { span, options: newOptions } = createSpan("ClientSecretCredential-getToken", options); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "client_credentials", | ||
client_id: this.clientId, | ||
client_secret: this.clientSecret, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
} |
@@ -7,3 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClientOptions, IdentityClient, TokenResponse } from "../client/identityClient"; | ||
import { AuthenticationError } from "../client/errors"; | ||
import { AuthenticationError, AuthenticationErrorName } from "../client/errors"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -79,24 +81,43 @@ /** | ||
): Promise<DeviceCodeResponse> { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const { span, options: newOptions } = createSpan( | ||
"DeviceCodeCredential-sendDeviceCodeRequest", | ||
options | ||
); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/devicecode`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
client_id: this.clientId, | ||
scope | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const response = await this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
const response = await this.identityClient.sendRequest(webResource); | ||
if (!(response.status === 200 || response.status === 201)) { | ||
throw new AuthenticationError(response.status, response.bodyAsText); | ||
} | ||
return response.parsedBody as DeviceCodeResponse; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
return response.parsedBody as DeviceCodeResponse; | ||
} | ||
@@ -109,49 +130,67 @@ | ||
let tokenResponse: TokenResponse | null = null; | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-pollForToken", options); | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
grant_type: "urn:ietf:params:oauth:grant-type:device_code", | ||
client_id: this.clientId, | ||
device_code: deviceCodeResponse.device_code | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
while (tokenResponse === null) { | ||
try { | ||
await delay(deviceCodeResponse.interval * 1000); | ||
while (tokenResponse === null) { | ||
try { | ||
await delay(deviceCodeResponse.interval * 1000); | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
} | ||
// Check the abort signal before sending the request | ||
if (options && options.abortSignal && options.abortSignal.aborted) { | ||
return null; | ||
} | ||
tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
} catch (err) { | ||
if (err instanceof AuthenticationError) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
} catch (err) { | ||
if (err.name === AuthenticationErrorName) { | ||
switch (err.errorResponse.error) { | ||
case "authorization_pending": | ||
break; | ||
case "authorization_declined": | ||
return null; | ||
case "expired_token": | ||
throw err; | ||
case "bad_verification_code": | ||
throw err; | ||
default: // Any other error should be rethrown | ||
throw err; | ||
} | ||
} else { | ||
throw err; | ||
} | ||
} else { | ||
throw err; | ||
} | ||
} | ||
return tokenResponse; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
return tokenResponse; | ||
} | ||
@@ -173,36 +212,51 @@ | ||
): Promise<AccessToken | null> { | ||
let tokenResponse: TokenResponse | null = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
} | ||
const { span, options: newOptions } = createSpan("DeviceCodeCredential-getToken", options); | ||
try { | ||
let tokenResponse: TokenResponse | null = null; | ||
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" "); | ||
if (scopeString.indexOf("offline_access") < 0) { | ||
scopeString += " offline_access"; | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = await this.identityClient.refreshAccessToken( | ||
this.tenantId, | ||
this.clientId, | ||
scopeString, | ||
this.lastTokenResponse.refreshToken, | ||
undefined, // clientSecret not needed for device code auth | ||
undefined, | ||
options | ||
); | ||
} | ||
// Try to use the refresh token first | ||
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) { | ||
tokenResponse = await this.identityClient.refreshAccessToken( | ||
this.tenantId, | ||
this.clientId, | ||
scopeString, | ||
this.lastTokenResponse.refreshToken, | ||
undefined, // clientSecret not needed for device code auth | ||
undefined, | ||
newOptions | ||
); | ||
} | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = await this.sendDeviceCodeRequest(scopeString, options); | ||
if (tokenResponse === null) { | ||
const deviceCodeResponse = await this.sendDeviceCodeRequest(scopeString, newOptions); | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
this.userPromptCallback({ | ||
userCode: deviceCodeResponse.user_code, | ||
verificationUri: deviceCodeResponse.verification_uri, | ||
message: deviceCodeResponse.message | ||
}); | ||
tokenResponse = await this.pollForToken(deviceCodeResponse, newOptions); | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
tokenResponse = await this.pollForToken(deviceCodeResponse, options); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
this.lastTokenResponse = tokenResponse; | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} | ||
} |
@@ -7,2 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { ClientSecretCredential } from "./clientSecretCredential"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -52,8 +55,25 @@ /** | ||
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null> { | ||
const { span, options: newOptions } = createSpan("EnvironmentCredential-getToken", options); | ||
if (this._credential) { | ||
return this._credential.getToken(scopes, options); | ||
try { | ||
return this._credential.getToken(scopes, newOptions); | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
span.setStatus({ code: CanonicalCode.UNAUTHENTICATED }); | ||
span.end(); | ||
return Promise.resolve(null); | ||
} | ||
} |
@@ -11,2 +11,4 @@ // Copyright (c) Microsoft Corporation. | ||
} from "./interactiveBrowserCredentialOptions"; | ||
import { createSpan } from "../util/tracing"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -117,21 +119,32 @@ /** | ||
scopes: string | string[], | ||
options?: GetTokenOptions // eslint-disable-line @typescript-eslint/no-unused-vars | ||
options?: GetTokenOptions | ||
): Promise<AccessToken | null> { | ||
if (!this.msalObject.getAccount()) { | ||
await this.login(); | ||
} | ||
const { span } = createSpan("InteractiveBrowserCredential-getToken", options); | ||
try { | ||
if (!this.msalObject.getAccount()) { | ||
await this.login(); | ||
} | ||
const authResponse = await this.acquireToken({ | ||
scopes: Array.isArray(scopes) ? scopes : scopes.split(",") | ||
}); | ||
const authResponse = await this.acquireToken({ | ||
scopes: Array.isArray(scopes) ? scopes : scopes.split(",") | ||
}); | ||
if (authResponse) { | ||
return { | ||
token: authResponse.accessToken, | ||
expiresOnTimestamp: authResponse.expiresOn.getTime() | ||
}; | ||
} else { | ||
return null; | ||
if (authResponse) { | ||
return { | ||
token: authResponse.accessToken, | ||
expiresOnTimestamp: authResponse.expiresOn.getTime() | ||
}; | ||
} else { | ||
return null; | ||
} | ||
} catch (err) { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
} |
@@ -13,2 +13,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClientOptions, IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -130,4 +133,8 @@ const DefaultScopeSuffix = "/.default"; | ||
clientId?: string, | ||
timeout?: number | ||
getTokenOptions?: GetTokenOptions | ||
): Promise<boolean> { | ||
const { span, options } = createSpan( | ||
"ManagedIdentityCredential-pingImdsEndpoint", | ||
getTokenOptions | ||
); | ||
const request = this.createImdsAuthRequest(resource, clientId); | ||
@@ -142,26 +149,39 @@ | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
if (timeout) { | ||
webResource.timeout = timeout; | ||
} else { | ||
webResource.timeout = 500; | ||
} | ||
request.spanOptions = options.spanOptions; | ||
try { | ||
await this.identityClient.sendRequest(webResource); | ||
// Create a request with a timeout since we expect that | ||
// not having a "Metadata" header should cause an error to be | ||
// returned quickly from the endpoint, proving its availability. | ||
const webResource = this.identityClient.createWebResource(request); | ||
webResource.timeout = options.timeout || 500; | ||
try { | ||
await this.identityClient.sendRequest(webResource); | ||
} catch (err) { | ||
if ( | ||
err instanceof RestError && | ||
(err.code === RestError.REQUEST_SEND_ERROR || | ||
err.code === RestError.REQUEST_ABORTED_ERROR) | ||
) { | ||
// Either request failed or IMDS endpoint isn't available | ||
span.setStatus({ | ||
code: CanonicalCode.UNAVAILABLE, | ||
message: err.message | ||
}); | ||
return false; | ||
} | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
} catch (err) { | ||
if ( | ||
err instanceof RestError && | ||
(err.code === RestError.REQUEST_SEND_ERROR || err.code === RestError.REQUEST_ABORTED_ERROR) | ||
) { | ||
// Either request failed or IMDS endpoint isn't available | ||
return false; | ||
} | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
// If we received any response, the endpoint is available | ||
return true; | ||
} | ||
@@ -179,49 +199,63 @@ | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody: any) => { | ||
// Parse a date format like "06/20/2019 02:57:58 +00:00" and | ||
// convert it into a JavaScript-formatted date | ||
const m = requestBody.expires_on.match( | ||
/(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d) (\+|-)(\d\d):(\d\d)/ | ||
); | ||
return Date.parse( | ||
`${m[3]}-${m[1]}-${m[2]}T${m[4]}:${m[5]}:${m[6]}${m[7]}${m[8]}:${m[9]}` | ||
); | ||
}; | ||
const { span, options } = createSpan( | ||
"ManagedIdentityCredential-authenticateManagedIdentity", | ||
getTokenOptions | ||
); | ||
try { | ||
// Detect which type of environment we are running in | ||
if (process.env.MSI_ENDPOINT) { | ||
if (process.env.MSI_SECRET) { | ||
// Running in App Service | ||
authRequestOptions = this.createAppServiceMsiAuthRequest(resource, clientId); | ||
expiresInParser = (requestBody: any) => { | ||
// 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); | ||
}; | ||
} else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
} | ||
} else { | ||
// Running in Cloud Shell | ||
authRequestOptions = this.createCloudShellMsiAuthRequest(resource, clientId); | ||
// Ping the IMDS endpoint to see if it's available | ||
if ( | ||
!checkIfImdsEndpointAvailable || | ||
(await this.pingImdsEndpoint(resource, clientId, options)) | ||
) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
} | ||
} else { | ||
// Ping the IMDS endpoint to see if it's available | ||
if ( | ||
!checkIfImdsEndpointAvailable || | ||
(await this.pingImdsEndpoint( | ||
resource, | ||
clientId, | ||
getTokenOptions ? getTokenOptions.timeout : undefined | ||
)) | ||
) { | ||
// Running in an Azure VM | ||
authRequestOptions = this.createImdsAuthRequest(resource, clientId); | ||
} else { | ||
// Returning null tells the ManagedIdentityCredential that | ||
// no MSI authentication endpoints are available | ||
return null; | ||
} | ||
} | ||
const webResource = this.identityClient.createWebResource({ | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
abortSignal: getTokenOptions && getTokenOptions.abortSignal, | ||
...authRequestOptions | ||
}); | ||
const webResource = this.identityClient.createWebResource({ | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
abortSignal: options.abortSignal, | ||
spanOptions: options.spanOptions, | ||
...authRequestOptions | ||
}); | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource, expiresInParser); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const tokenResponse = await this.identityClient.sendTokenRequest( | ||
webResource, | ||
expiresInParser | ||
); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
@@ -245,21 +279,33 @@ | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = await this.authenticateManagedIdentity( | ||
scopes, | ||
this.isEndpointUnavailable === null, | ||
this.clientId, | ||
options | ||
); | ||
const { span, options: newOptions } = createSpan("ManagedIdentityCredential-getToken", options); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
try { | ||
// isEndpointAvailable can be true, false, or null, | ||
// the latter indicating that we don't yet know whether | ||
// the endpoint is available and need to check for it. | ||
if (this.isEndpointUnavailable !== true) { | ||
result = await this.authenticateManagedIdentity( | ||
scopes, | ||
this.isEndpointUnavailable === null, | ||
this.clientId, | ||
newOptions | ||
); | ||
// If authenticateManagedIdentity returns null, it means no MSI | ||
// endpoints are available. In this case, don't try them in future | ||
// requests. | ||
this.isEndpointUnavailable = result === null; | ||
} | ||
return result; | ||
} catch (err) { | ||
span.setStatus({ | ||
code: CanonicalCode.UNKNOWN, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
return result; | ||
} | ||
} |
@@ -7,2 +7,5 @@ // Copyright (c) Microsoft Corporation. | ||
import { IdentityClientOptions, IdentityClient } from "../client/identityClient"; | ||
import { createSpan } from "../util/tracing"; | ||
import { AuthenticationErrorName } from "../client/errors"; | ||
import { CanonicalCode } from "@azure/core-tracing"; | ||
@@ -61,25 +64,44 @@ /** | ||
): Promise<AccessToken | null> { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal | ||
}); | ||
const { span, options: newOptions } = createSpan( | ||
"UsernamePasswordCredential-getToken", | ||
options | ||
); | ||
try { | ||
const webResource = this.identityClient.createWebResource({ | ||
url: `${this.identityClient.authorityHost}/${this.tenantId}/oauth2/v2.0/token`, | ||
method: "POST", | ||
disableJsonStringifyOnBody: true, | ||
deserializationMapper: undefined, | ||
body: qs.stringify({ | ||
response_type: "token", | ||
grant_type: "password", | ||
client_id: this.clientId, | ||
username: this.username, | ||
password: this.password, | ||
scope: typeof scopes === "string" ? scopes : scopes.join(" ") | ||
}), | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/x-www-form-urlencoded" | ||
}, | ||
abortSignal: options && options.abortSignal, | ||
spanOptions: newOptions.spanOptions | ||
}); | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
const tokenResponse = await this.identityClient.sendTokenRequest(webResource); | ||
return (tokenResponse && tokenResponse.accessToken) || null; | ||
} catch (err) { | ||
const code = | ||
err.name === AuthenticationErrorName | ||
? CanonicalCode.UNAUTHENTICATED | ||
: CanonicalCode.UNKNOWN; | ||
span.setStatus({ | ||
code, | ||
message: err.message | ||
}); | ||
throw err; | ||
} finally { | ||
span.end(); | ||
} | ||
} | ||
} |
@@ -21,3 +21,9 @@ // Copyright (c) Microsoft Corporation. | ||
export { UsernamePasswordCredential } from "./credentials/usernamePasswordCredential"; | ||
export { AuthenticationError, AggregateAuthenticationError } from "./client/errors"; | ||
export { AuthorizationCodeCredential } from "./credentials/authorizationCodeCredential"; | ||
export { | ||
AuthenticationError, | ||
AggregateAuthenticationError, | ||
AuthenticationErrorName, | ||
AggregateAuthenticationErrorName | ||
} from "./client/errors"; | ||
@@ -24,0 +30,0 @@ export { TokenCredential, GetTokenOptions, AccessToken } from "@azure/core-http"; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2476570
114
16270
161
8
42
+ Added@azure/core-http@1.0.0-preview.4(transitive)
+ Added@azure/core-tracing@1.0.0-preview.3(transitive)
+ Added@opencensus/web-types@0.0.7(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addedms@2.1.3(transitive)
- Removed@azure/core-http@1.0.0-preview.3(transitive)
- Removed@azure/core-tracing@1.0.0-preview.2(transitive)