Building VSTS Extensions with feature flags – Part 2

In Building VSTS Extensions with feature flags we started a discussion about feature flags and how we’re using LaunchDarkly to eliminate risk and deliver value. We closed with a brief mention that we’re trying to find a way to exchange a more secure user key as part of the communication between our extensions and the LaunchDarkly service. We believe we found a solution, which is the focus of this post.

Continued from Part 1

Context

image

Read https://docs.launchdarkly.com/docs/js-sdk-reference#section-secure-mode for details on the secure mode.

Our proposed solution

After some investigation, and, inspired by the Create a VSTS Extension that uses Azure Functions blog, we experimented with the use of Azure Functions to be able to call server-side code. The VSTS extension calls the Azure Function, which will process and load the hash key and return it to the extension. But with this solution, we had a challenge of the security of this Azure Function tunnelling for passing secures parameters in this Azure Function: We can imagine that a malicious user could call the Azure function by passing in the user key of another user and recovering the values of the user’s flags.

So, we had to find the most secure parameters.

The flow in simple steps:

  1. The VSTS extension calls Azure Function with user token parameter
  2. The Azure Function check the user token and return the hashed userkey.
  3. The VSTS extension gets the response from the Azure Function and continues processing by calling the Initialize method with this hash key in parameter from the LaunchDarkly SDK.

image

Here’s the specification for the Azure Function.

Inputs parameters

  • User Token: session type token provided by VSTS service. For example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1laWQiOiI4NGVhNDg0NS01N2JhLTQwOTYtOTA5Zi0yOGYyM2NlNTRmZTUiLCJ0aWQiOiJXaW5kb3dzIExpdmUgSUQiLCJpc3MiOiJhcHAudnNzcHMudmlzdWFsc3R1ZGlvLmNvbSIsImF1ZCI6ImZiMzk4ZTkyLWY2ODktNDAyOS05ZDhhLWQwNmI2YzdjODc2YyIsIm5iZiI6MTQ5ODY2Njk2MywiZXhwIjoxNDk4NjcwNTYzfQ.yioxdiH6AGMpSzoTWmf3953yjqg46DS0N2TWhR8EX1E
  • VSTS user account. For example: mikaelkrief

Azure Function process (see sample source code at the end of this post)

  1. Check the validity of the user token (using the certificate of the extension).

    Read Auth and security for details.

  2. If the user token is valid, extract the user id from the principalClaims encrypted in the token. For example, 22544b1b-d4cd-489b-a2ea-ed932c8853b6.

    -> If not valid, return a 500 error

  3. Create the LaunchDarly userkey with this pattern: userid:vstsaccount

  4. Generate the Hash for the userkey created in (3).

    See public string SecureModeHash(User user) for details.

Output

  • The Azure Function returns the hash key for the current user.

Let’s validate and test this solution

SCENARIO 1 - Change the user token

Test: Malicious user tries to change the user token; knowing that it is impossible to have a valid token of another user who is currently connected (is the goal of the session token J )

Result: The token validation fails and the Azure function returns error 500

SCENARIO 2 - Change the VSTS account

Test: Malicious user tries to change the VSTS account by passing another VSTS account and a correct user token.

Result: The Azure function does not fail as the check of the user token return true. It returns a hash key. However, the hash key does not match with the current userkey passed in LaunchDarkly Initialization method, resulting in a validation failure in the LaunchDarkly service and returning an 400 status code error (Bad request) and a message "Environment is in secure mode, and user hash does not match.".

What’s Next?

Now that we find secure solution for call Azure Function from our VSTS extension, we use this solution to call LD Rest APIs, it will certainly be exposed in a future blog article. And we’re polishing the team-services-extension-featureflag-sample and implementing feature flags in our Roll-Up-Board-Widget-Extension, and Work-Item-Details-Widget-Extension solutions. Once we’re done, we’ll summarize the learnings and recommendations in an article “Phase the features of your application with feature flags” on https://aka.ms/techarticles. Watch the space.

References

https://blogs.msdn.microsoft.com/visualstudioalmrangers/2017/06/27/building-vsts-extensions-with-feature-flags/

https://launchdarkly.com/

SAMPLE CODE – Azure Function

    1. #r "D:\home\site\wwwroot\CheckToken\System.IdentityModel.dll"

    2. using System.Net;

    3. using System.Collections.Generic;

    4. using System.Security.Cryptography;

    5. using System.IdentityModel.Tokens;

    6. using System.ServiceModel.Security.Tokens;

    7. public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log)

    8. {

    9.     try

    10.     {

    11.         //Gettings input POST parameters

    12.         var data = req.Content.ReadAsStringAsync().Result;

    13.         var formValues = data.Split('&')

    14.             .Select(value => value.Split('='))

    15.             .ToDictionary(pair => Uri.UnescapeDataString(pair[0]).Replace("+", " "),

    16.                           pair => Uri.UnescapeDataString(pair[1]).Replace("+", " "));

    17.         var issuedToken = formValues["token"];

    18.         var account = formValues["account"];     

    19.         //Check the token, and extract the userid crypted in the token

    20.         var userId = checkTokenValidityAndGetUserId(issuedToken);

    21.         if (userId != null)

    22.         {

    23.             //hash the User Key

    24.             string hash = getHashKey(userId + ":" + account);

    25.             //return the hash key

    26.             return req.CreateResponse(HttpStatusCode.OK, hash);

    27.         }

    28.         else

    29.         {

    30.             return req.CreateResponse(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError);

    31.         }

    32.     }

    33.     catch

    34.     {

    35.         return req.CreateResponse(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError);

    36.     }

    37. }

    38. public static string checkTokenValidityAndGetUserId(string issuedToken)

    39. {

    40.     try

    41.     {

    42.         string secret = "<My extension certificate>"; // Load your extension's secret

    43.         var validationParameters = new TokenValidationParameters()

    44.         {

    45.             IssuerSigningTokens = new List<BinarySecretSecurityToken>()

    46.                         {

    47.                             new BinarySecretSecurityToken (System.Text.UTF8Encoding.UTF8.GetBytes(secret))

    48.                         },

    49.             ValidateIssuer = false,

    50.             RequireSignedTokens = true,

    51.             RequireExpirationTime = true,

    52.             ValidateLifetime = true,

    53.             ValidateAudience = false,

    54.             ValidateActor = false

    55.         };

    56.         SecurityToken token = null;

    57.         var tokenHandler = new JwtSecurityTokenHandler();

    58.         var principal = tokenHandler.ValidateToken(issuedToken, validationParameters, out token);

    59.         //extract the userId from principalClaims

    60.        string principalUserId = principal.Claims.FirstOrDefault(q => string.Compare(q.Type,
      "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", true) == 0).Value;

    61.         return principalUserId;

    62.     }

    63.     catch

    64.     {

    65.         return null;

    66.     }

    67. }

    68. //Hash the secure userkey

    69. //source : https://github.com/launchdarkly/.net-client/blob/eafb706589ba57e72f93f58cfb80f48c6fba03ec/src/LaunchDarkly.Client/LdClient.cs#L189

    70. public static string getHashKey(string userkey)

    71. {

    72.     if (string.IsNullOrEmpty(userkey))

    73.     {

    74.  

    75.        return null;

    76.     }

    77.     System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();

    78.     byte[] keyBytes = encoding.GetBytes("sdk-59baef5c-3851-4fef-a6a6-05a6e9c38ea9");

    79.     HMACSHA256 hmacSha256 = new HMACSHA256(keyBytes);

    80.     byte[] hashedMessage = hmacSha256.ComputeHash(encoding.GetBytes(userkey));

    81.     return BitConverter.ToString(hashedMessage).Replace("-", "").ToLower();

    82. }