AzureAD / passport-azure-ad

The code for Passport Azure AD has been moved to the MSAL.js repo. Please open any issues or PRs at the link below.
https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/maintenance/passport-azure-ad
Other
422 stars 176 forks source link

Microsoft Azure Active Directory Passport.js Plug-In


Node JS validation replacement for passport.js

Dear Node JS customers,

We understand that many of you have been frustrated by the lack of a replacement for this deprecated passport.js library. We want you to know that we hear your concerns and are actively working on a solution. In the meantime, we want to assure you that we haven't forgotten about you. Auth validation is very complicated and hard to get right. For this reason, we will leverage Microsoft.IdentityModel, which has a proven track record over many years, and is hitting impressive perf metrics with the latest improvements in IdentityModel 7x on .NET 8, making Ahead of Time (AOT) comparable with non-AOT, especially in terms of request per second, which is very promising for our goal of covering all services, regardless of language with Microsoft.IdentityModel.

We are experimenting with a simple wrapper over a shared library in Node JS that will allow you to continue using the Microsoft identity platforms. This wrapper is currently only available to internal Microsoft services, but we're working hard to make it available to all of our customers as soon as possible. Thank you for your patience and understanding as we work to improve our offerings. We'll keep you updated as we make progress on this front. Please follow this discussion for more details on the progress and to provide us with your feedback/questions/concerns as we go through this process together.


passport-azure-ad is a collection of Passport Strategies to help you integrate with Azure Active Directory. It includes OpenID Connect, WS-Federation, and SAML-P authentication and authorization. These providers let you integrate your Node app with Microsoft Azure AD so you can use its many features, including web single sign-on (WebSSO), Endpoint Protection with OAuth, and JWT token issuance and validation.

passport-azure-ad has been tested to work with both Microsoft Azure Active Directory and with Microsoft Active Directory Federation Services.

1. Security Vulnerability in Versions < 1.4.6 and 2.0.0

passport-azure-ad has a known security vulnerability affecting versions <1.4.6 and 2.0.0. Please update to >=1.4.6 or >=2.0.1 immediately. For more details, see the security notice.

2. Versions

Current version - 4.0.0
Latest version that support's SAML and WSFED - 2.0.3 Minimum recommended version - 1.4.6
You can find the changes for each version in the change log.

3. Installation

$ npm install passport-azure-ad

4. Usage

This library contains two strategies: OIDCStrategy and BearerStrategy.

OIDCStrategy uses OpenID Connect protocol for web application login purposes. It works in the following manner: If a user is not logged in, passport sends an authentication request to AAD (Azure Active Directory), and AAD prompts the user for his or her sign-in credentials. On successful authentication, depending on the flow you choose, web application will eventually get an id_token back either directly from the AAD authorization endpoint or by redeeming a code at the AAD token endpoint. Passport then validates the id_token and propagates the claims in id_token back to the verify callback, and let the framework finish the remaining authentication procedure. If the whole process is successful, passport adds the user information to req.user and passes it to the next middleware. In case of error, passport either sends back an unauthorized response or redirects the user to the page you specified (such as homepage or login page).

BearerStrategy uses Bearer Token protocol to protect web resource/api. It works in the following manner: User sends a request to the protected web api which contains an access_token in either the authorization header or body. Passport extracts and validates the access_token, and propagates the claims in access_token to the verify callback and let the framework finish the remaining authentication procedure. On successful authentication, passport adds the user information to req.user and passes it to the next middleware, which is usually the business logic of the web resource/api. In case of error, passport sends back an unauthorized response.

We support AAD v1, v2 and B2C tenants for both strategies. Please check out section 8 for the samples. You can manage v1 tenants and register applications at https://manage.windowsazure.com. For v2 tenants and applications, you should go to https://aka.ms/appregistrations. For B2C tenants, go to https://manage.windowsazure.com and click 'Manage B2C settings' to register applications and policies.

4.1 OIDCStrategy

4.1.1 Configure strategy and provide callback function

4.1.1.1 Sample using the OIDCStrategy
passport.use(new OIDCStrategy({
    identityMetadata: config.creds.identityMetadata,
    clientID: config.creds.clientID,
    responseType: config.creds.responseType,
    responseMode: config.creds.responseMode,
    redirectUrl: config.creds.redirectUrl,
    allowHttpForRedirectUrl: config.creds.allowHttpForRedirectUrl,
    clientSecret: config.creds.clientSecret,
    validateIssuer: config.creds.validateIssuer,
    isB2C: config.creds.isB2C,
    issuer: config.creds.issuer,
    passReqToCallback: config.creds.passReqToCallback,
    scope: config.creds.scope,
    loggingLevel: config.creds.loggingLevel,
    loggingNoPII: config.creds.loggingNoPII,
    nonceLifetime: config.creds.nonceLifetime,
    nonceMaxAmount: config.creds.nonceMaxAmount,
    useCookieInsteadOfSession: config.creds.useCookieInsteadOfSession,
    cookieSameSite: config.creds.cookieSameSite, // boolean
    cookieEncryptionKeys: config.creds.cookieEncryptionKeys,
    clockSkew: config.creds.clockSkew,
    proxy: { port: 'proxyport', host: 'proxyhost', protocol: 'http' },
  },
  function(iss, sub, profile, accessToken, refreshToken, done) {
    if (!profile.oid) {
      return done(new Error("No oid found"), null);
    }
    // asynchronous verification, for effect...
    process.nextTick(function () {
      findByOid(profile.oid, function(err, user) {
        if (err) {
          return done(err);
        }
        if (!user) {
          // "Auto-registration"
          users.push(profile);
          return done(null, profile);
        }
        return done(null, user);
      });
    });
  }
));
4.1.1.2 Options
4.1.1.3 Verify callback

If you set passReqToCallback option to false, you can use one of the following signatures for the verify callback

  function(iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
  function(iss, sub, profile, access_token, refresh_token, params, done)
  function(iss, sub, profile, access_token, refresh_token, done)
  function(iss, sub, profile, done)
  function(iss, sub, done)
  function(profile, done)

If you set passReqToCallback option to true, you can use one of the following signatures for the verify callback

  function(req, iss, sub, profile, jwtClaims, access_token, refresh_token, params, done)
  function(req, iss, sub, profile, access_token, refresh_token, params, done)
  function(req, iss, sub, profile, access_token, refresh_token, done)
  function(req, iss, sub, profile, done)
  function(req, iss, sub, done)
  function(req, profile, done)

4.1.1.4 JWE support

We support encrypted id_token in JWE Compact Serialization format.

The key encryption algorithms supported are:

RSA1_5, RSA-OAEP, A128KW, A256KW, dir.

The content encryption algorithms supported are:

A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, and A256GCM.

In order to decrypt the id_token, keys have to be provided in JWK format using jweKeyStore option. We will first try the key with the corresponding kid. If decryption fails, we will try every possible key in jweKeyStore. The following is an example of jweKeyStore:


    jweKeyStore: [ 
      { 'kid': 'sym_key_256', 'kty': 'oct', 'k': 'WIVds2iwJPwNhgUgwZXmn/46Ql1EkiL+M+QqDRdQURE=' }, 
      { 'kid': 'sym_key_128', 'kty': 'oct', 'k': 'GawgguFyGrWKav7AX4VKUg'}, 
      { 'kid': 'sym_key_384', 'kty': 'oct', 'k': 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v'},
      { 'kid': 'rsa_key', 
        'kty': 'RSA', 
        "n":"6-FrFkt_TByQ_L5d7or-9PVAowpswxUe3dJeYFTY0Lgq7zKI5OQ5RnSrI0\n\
             T9yrfnRzE9oOdd4zmVj9txVLI-yySvinAu3yQDQou2Ga42ML_-K4Jrd5cl\n\
             MUPRGMbXdV5Rl9zzB0s2JoZJedua5dwoQw0GkS5Z8YAXBEzULrup06fnB5\n\
             n6x5r2y1C_8Ebp5cyE4Bjs7W68rUlyIlx1lzYvakxSnhUxSsjx7u_mIdyw\n\
             yGfgiT3tw0FsWvki_KYurAPR1BSMXhCzzZTkMWKE8IaLkhauw5MdxojxyB\n\
             VuNY-J_elq-HgJ_dZK6g7vMNvXz2_vT-SykIkzwiD9eSI9UWfsjw",
        "e":"AQAB",
        "d":"C6EGZYf9U6RI5Z0BBoSlwy_gKumVqRx-dBMuAfPM6KVbwIUuSJKT3ExeL5\n\
             P0Ky1b4p-j2S3u7Afnvrrj4HgVLnC1ks6rEOc2ne5DYQq8szST9FMutyul\n\
             csNUKLOM5cVromALPz3PAqE2OCLChTiQZ5XZ0AiH-KcG-3hKMa-g1MVnGW\n\
             -SSmm27XQwRtUtFQFfxDuL0E0fyA9O9ZFBV5201ledBaLdDcPBF8cHC53G\n\
             m5G6FRX3QVpoewm3yGk28Wze_YvNl8U3hvbxei2Koc_b9wMbFxvHseLQrx\n\
             vFg_2byE2em8FrxJstxgN7qhMsYcAyw1qGJY-cYX-Ab_1bBCpdcQ",
        "p":"_avCCyuo7hHlqu9Ec6R47ub_Ul_zNiS-xvkkuYwW-4lNnI66A5zMm_BOQV\n\
             MnaCkBua1OmOgx7e63-jHFvG5lyrhyYEmkA2CS3kMCrI-dx0fvNMLEXInP\n\
             xd4np_7GUd1_XzPZEkPxBhqf09kqryHMj_uf7UtPcrJNvFY-GNrzlJk",
        "q":"7gvYRkpqM-SC883KImmy66eLiUrGE6G6_7Y8BS9oD4HhXcZ4rW6JJKuBzm\n\
             7FlnsVhVGro9M-QQ_GSLaDoxOPQfHQq62ERt-y_lCzSsMeWHbqOMci_pbt\n\
             vJknpMv4ifsQXKJ4Lnk_AlGr-5r5JR5rUHgPFzCk9dJt69ff3QhzG2c",
        "dp":"ErP3OpudePAY3uGFSoF16Sde69PnOra62jDEZGnPx_v3nPNpA5sr-tNc8\n\
              bQP074yQl5kzSFRjRlstyW0TpBVMP0ocbD8RsN4EKsgJ1jvaSIEoP87Ox\n\
              duGkim49wFA0Qxf_NyrcYUnz6XSidY3lC_pF4JDJXg5bP_x0MUkQCTtQE",
        "dq":"YbBsthPt15Pshb8rN8omyfy9D7-m4AGcKzqPERWuX8bORNyhQ5M8JtdXc\n\
              u8UmTez0j188cNMJgkiN07nYLIzNT3Wg822nhtJaoKVwZWnS2ipoFlgrB\n\
              gmQiKcGU43lfB5e3qVVYUebYY0zRGBM1Fzetd6Yertl5Ae2g2CakQAcPs",
        "qi":"lbljWyVY-DD_Zuii2ifAz0jrHTMvN-YS9l_zyYyA_Scnalw23fQf5WIcZ\n\
              ibxJJll5H0kNTIk8SCxyPzNShKGKjgpyZHsJBKgL3iAgmnwk6k8zrb_lq\n\
              a0sd1QWSB-Rqiw7AqVqvNUdnIqhm-v3R8tYrxzAqkUsGcFbQYj4M5_F_4"
      },
      { 'kid': 'ras_key_2',
        'kty': 'RSA',
        'privatePemKey': 
          '-----BEGIN RSA PRIVATE KEY-----\n\
          MIIEowIBAAKCAQEA6+FrFkt/TByQ/L5d7or+9PVAowpswxUe3dJeYFTY0Lgq7zKI\n\
          5OQ5RnSrI0T9yrfnRzE9oOdd4zmVj9txVLI+yySvinAu3yQDQou2Ga42ML/+K4Jr\n\
          d5clMUPRGMbXdV5Rl9zzB0s2JoZJedua5dwoQw0GkS5Z8YAXBEzULrup06fnB5n6\n\
          x5r2y1C/8Ebp5cyE4Bjs7W68rUlyIlx1lzYvakxSnhUxSsjx7u/mIdywyGfgiT3t\n\
          w0FsWvki/KYurAPR1BSMXhCzzZTkMWKE8IaLkhauw5MdxojxyBVuNY+J/elq+HgJ\n\
          /dZK6g7vMNvXz2/vT+SykIkzwiD9eSI9UWfsjwIDAQABAoIBAAuhBmWH/VOkSOWd\n\
          AQaEpcMv4CrplakcfnQTLgHzzOilW8CFLkiSk9xMXi+T9CstW+Kfo9kt7uwH5766\n\
          4+B4FS5wtZLOqxDnNp3uQ2EKvLM0k/RTLrcrpXLDVCizjOXFa6JgCz89zwKhNjgi\n\
          woU4kGeV2dAIh/inBvt4SjGvoNTFZxlvkkpptu10MEbVLRUBX8Q7i9BNH8gPTvWR\n\
          QVedtNZXnQWi3Q3DwRfHBwudxpuRuhUV90FaaHsJt8hpNvFs3v2LzZfFN4b28Xot\n\
          iqHP2/cDGxcbx7Hi0K8bxYP9m8hNnpvBa8SbLcYDe6oTLGHAMsNahiWPnGF/gG/9\n\
          WwQqXXECgYEA/avCCyuo7hHlqu9Ec6R47ub/Ul/zNiS+xvkkuYwW+4lNnI66A5zM\n\
          m/BOQVMnaCkBua1OmOgx7e63+jHFvG5lyrhyYEmkA2CS3kMCrI+dx0fvNMLEXInP\n\
          xd4np/7GUd1/XzPZEkPxBhqf09kqryHMj/uf7UtPcrJNvFY+GNrzlJkCgYEA7gvY\n\
          RkpqM+SC883KImmy66eLiUrGE6G6/7Y8BS9oD4HhXcZ4rW6JJKuBzm7FlnsVhVGr\n\
          o9M+QQ/GSLaDoxOPQfHQq62ERt+y/lCzSsMeWHbqOMci/pbtvJknpMv4ifsQXKJ4\n\
          Lnk/AlGr+5r5JR5rUHgPFzCk9dJt69ff3QhzG2cCgYASs/c6m5148Bje4YVKgXXp\n\
          J17r0+c6trraMMRkac/H+/ec82kDmyv601zxtA/TvjJCXmTNIVGNGWy3JbROkFUw\n\
          /ShxsPxGw3gQqyAnWO9pIgSg/zs7F24aSKbj3AUDRDF/83KtxhSfPpdKJ1jeUL+k\n\
          XgkMleDls//HQxSRAJO1AQKBgGGwbLYT7deT7IW/KzfKJsn8vQ+/puABnCs6jxEV\n\
          rl/GzkTcoUOTPCbXV3LvFJk3s9I9fPHDTCYJIjdO52CyMzU91oPNtp4bSWqClcGV\n\
          p0toqaBZYKwYJkIinBlON5XweXt6lVWFHm2GNM0RgTNRc3rXemHq7ZeQHtoNgmpE\n\
          AHD7AoGBAJW5Y1slWPgw/2bootonwM9I6x0zLzfmEvZf88mMgP0nJ2pcNt30H+Vi\n\
          HGYm8SSZZeR9JDUyJPEgscj8zUoShio4KcmR7CQSoC94gIJp8JOpPM62/5amtLHd\n\
          UFkgfkaosOwKlarzVHZyKoZvr90fLWK8cwKpFLBnBW0GI+DOfxf+\n\
          -----END RSA PRIVATE KEY-----\n';
      }
  ]

4.1.2 Use passport.authenticate to protect routes

To complete the sample, provide a route that corresponds to the path configuration parameter that is sent to the strategy:


app.get('/login', 
  passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }),
  function(req, res) {
    log.info('Login was called in the Sample');
    res.redirect('/');
});

function regenerateSessionAfterAuthentication(req, res, next) {
  var passportInstance = req.session.passport;
  return req.session.regenerate(function (err){
    if (err) {
      return next(err);
    }
    req.session.passport = passportInstance;
    return req.session.save(next);
  });
}

// POST /auth/openid/return
//   Use passport.authenticate() as route middleware to authenticate the
//   request.  If authentication fails, the user will be redirected back to the
//   home page.  Otherwise, the primary route function function will be called,
//   which, in this example, will redirect the user to the home page.
app.post('/auth/openid/return',
  passport.authenticate('azuread-openidconnect', { failureRedirect: '/' }),
  regenerateSessionAfterAuthentication,
  function(req, res) { 
    res.redirect('/');
  });

app.get('/logout', function(req, res){
  req.logout();
  res.redirect('/');
});

4.1.3 Options available for passport.authenticate

Example:

  passport.authenticate('azuread-openidconnect', { failureRedirect: '/', session: false, customState: 'my_state', resourceURL: 'https://graph.microsoft.com/mail.send'});

  passport.authenticate('azuread-openidconnect', { tenantIdOrName: 'contoso.onmicrosoft.com' });

4.1.4 Session free support

Passport framework uses session to keep a persistent login session. As a plug in, we also use session to store state and nonce by default, regardless whether you use { session: false } option in passport.authenticate or not. To be completely session free, you must configure passport-azure-ad to create state/nonce cookie instead of saving them in session. Please follow the following example:

  passport.use(new OIDCStrategy({
    ...
    nonceLifetime: 600,  // state/nonce cookie expiration in seconds
    nonceMaxAmount: 5,   // max amount of state/nonce cookie you want to keep (cookie is deleted after validation so this can be very small)
    useCookieInsteadOfSession: true,  // use cookie, not session
    cookieEncryptionKeys: [ { key: '12345678901234567890123456789012', 'iv': '123456789012' }],  // encrypt/decrypt key and iv, see `cookieEncryptionKeys` instruction in section 4.1.1.2
  },
    // any supported verify callback
    function(iss, sub, profile, accessToken, refreshToken, done) {
    ...
  });

  app.get('/login', passport.authenticate('azuread-openidconnect', { session: false }));

4.2 BearerStrategy

4.2.1 Configure strategy and provide callback function

4.2.1.1 Sample using the BearerStrategy

// We pass these options in to the ODICBearerStrategy.

var options = {
  identityMetadata: config.creds.identityMetadata,
  clientID: config.creds.clientID,
  validateIssuer: config.creds.validateIssuer,
  issuer: config.creds.issuer,
  passReqToCallback: config.creds.passReqToCallback,
  isB2C: config.creds.isB2C,
  policyName: config.creds.policyName,
  allowMultiAudiencesInToken: config.creds.allowMultiAudiencesInToken,
  audience: config.creds.audience,
  loggingLevel: config.creds.loggingLevel,
  loggingNoPII: config.creds.loggingNoPII,
  clockSkew: config.creds.clockSkew,
  scope: config.creds.scope
};

var bearerStrategy = new BearerStrategy(options,
  function(token, done) {
    log.info('verifying the user');
    log.info(token, 'was the token retreived');
    findById(token.oid, function(err, user) {
      if (err) {
        return done(err);
      }
      if (!user) {
        // "Auto-registration"
        log.info('User was added automatically as they were new. Their oid is: ', token.oid);
        users.push(token);
        owner = token.oid;
        return done(null, token);
      }
      owner = token.oid;
      return done(null, user, token);
    });
  }
);
4.2.1.2 Options
4.2.1.3 Verify callback

If you set passReqToCallback option to false, you can use the following verify callback

  function(token, done)

If you set passReqToCallback option to true, you can use the following verify callback

  function(req, token, done)

4.2.2 Use passport.authenticate to protect resources or APIs

In the following example, we are using passport to protect '/api/tasks'. User sends a GET request to '/api/tasks' with access_token in authorization header or body. Passport validates the access_token, adds the related claims from access_token to req.user, and passes the request to listTasks middleware. The listTasks middleware can then read the user information in req.user and list all the tasks related to this user. Note that we do authentication every time, so we don't need to keep this user in session, and this can be achieved by using session: false option.

  server.get('/api/tasks', passport.authenticate('oauth-bearer', { session: false }), listTasks);

4.2.3 Options available for passport.authenticate

Example:

  passport.authenticate('oauth-bearer', { session: false });

5. Test

In the library root folder, type the following command to install the dependency packages:

    $ npm install

5.1. Run all tests except the end to end tests

Type the following command to run tests:

    $ npm test

5.2. Run all tests including the end to end tests

5.2.1. Create test applications

First you need to register one application in v1 tenant, one in v2 tenant and one in B2C tenant.

For the v2 application, you should register it at https://apps.dev.microsoft.com/ instead of Azure Portal.

For the B2C application, create policies named 'B2C_1_signin', 'B2C_1_signup'. For each policy, select 'Local Account' as the identity provider, and select the following:

You will also need to click the 'Run now' button in the 'B2C_1_signup' blade to create an user.

For B2C application, you will also need to create at least one scope and provide it to test parameters. See how to create scope for B2C access token. In the bearer_b2c_test, We will use OIDCStrategy to get a B2C access token for the scope, and use BearerStrategy to validate the scope. Note for scope we use the full url in b2c_params.scopeForOIDC but only the name in b2c_params.scopeForBearer. For example, b2c_params.scopeForOIDC=['https://sijun1b2c.onmicrosoft.com/oidc-b2c/read'] and b2c_params.scopeForBearer=['read'].

5.2.2. Fill the test parameters

Open test/End_to_end_test/script.js, set is_test_parameters_completed parameter to true. For test_parameters variable, fill in the tenant id/client id/client secret of your applications, and the username/password of your application user.

For thumbprint and privatePEMKey parameters, you need to specify a certificate for your app and register the public key in Azure Active Directory. thumbprint is the base64url format of the thumbprint of the public key, and privatePEMKey is the private pem key string. For a v1 tenant, you can follow this post to generate a certificate and register the public key. For a v2 tenant, you can go to your application page in the v2 portal and click Generate New Key Pair. A certificate will be generated for you to download. The corresponding public key is automatically registered in this case.

5.2.3. Run the tests

Type the following commands to run the tests:

    $ cd test/End_to_end_test
    $ npm install
    $ npm install grunt -g
    $ grunt run_tests_with_e2e

Tests will run automatically and in the terminal you can see how many tests are passing/failing. More details can be found here.

6. Logging

Personal Identifiable Information (PII) & Organizational Identifiable Information (OII)

By default, passport-azure-ad logging does not capture or log any PII or OII. The library allows app developers to turn this on by configuring loggingNoPII in the config options. By turning on PII or OII, the app takes responsibility for safely handling highly-sensitive data and complying with any regulatory requirements.

//PII or OII logging disabled. Default Logger does not capture any PII or OII.
var options = {
  ...
  loggingNoPII: true,
  ...
};

//PII or OII logging enabled
var options = {
  ...
  loggingNoPII: false,
  ...
};

7. Samples and Documentation

We provide a full suite of sample applications and documentation on GitHub to help you get started with learning the Azure Identity system. This includes tutorials for native clients such as Windows, Windows Phone, iOS, OSX, Android, and Linux. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect, Graph API, and other awesome features.

Azure Identity samples for this plug-in can be found in the following links:

7.1 Samples for OpenID connect strategy

7.2 Samples for Bearer strategy

8. Community Help and Support

We leverage Stack Overflow to work with the community on supporting Azure Active Directory and its SDKs, including this one. We highly recommend you ask your questions on Stack Overflow (we're all on there!) Also browser existing issues to see if someone has had your question before.

We recommend you use the "msal" tag so we can see it! Here is the latest Q&A on Stack Overflow for MSAL: http://stackoverflow.com/questions/tagged/msal

9. Security Reporting

If you find a security issue with our libraries or services please report it to secure@microsoft.com with as much detail as possible. Your submission may be eligible for a bounty through the Microsoft Bounty program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting this page and subscribing to Security Advisory Alerts.

10. Contributing

All code is licensed under the MIT license and we triage actively on GitHub. We enthusiastically welcome contributions and feedback. You can clone the repo and start contributing now.

More details about contribution

11. Releases

Please check the releases for updates.

12. Acknowledgements

The code is based on Henri Bergius's passport-saml library and Matias Woloski's passport-wsfed-saml2 library as well as Kiyofumi Kondoh's passport-openid-google.

13. License

Copyright (c) Microsoft Corp. All rights reserved. Licensed under the MIT License;

14. Microsoft Open Source Code of Conduct

We Value and Adhere to the Microsoft Open Source Code of Conduct

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.