Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@azure/identity

Package Overview
Dependencies
Maintainers
1
Versions
540
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@azure/identity - npm Package Compare versions

Comparing version 1.0.0-preview.3 to 1.0.0-preview.4

dist-esm/src/credentials/authorizationCodeCredential.browser.d.ts

7

CHANGELOG.md
# 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}.

14

dist-esm/src/client/errors.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc