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
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:
- The VSTS extension calls Azure Function with user token parameter
- The Azure Function check the user token and return the hashed userkey.
- 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.
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)
Check the validity of the user token (using the certificate of the extension).
Read Auth and security for details.
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
Create the LaunchDarly userkey with this pattern: userid:vstsaccount
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
SAMPLE CODE – Azure Function
-
#r "D:\home\site\wwwroot\CheckToken\System.IdentityModel.dll"
using System.Net;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.IdentityModel.Tokens;
using System.ServiceModel.Security.Tokens;
public static HttpResponseMessage Run(HttpRequestMessage req, TraceWriter log)
{
try
{
//Gettings input POST parameters
var data = req.Content.ReadAsStringAsync().Result;
var formValues = data.Split('&')
.Select(value => value.Split('='))
.ToDictionary(pair => Uri.UnescapeDataString(pair[0]).Replace("+", " "),
pair => Uri.UnescapeDataString(pair[1]).Replace("+", " "));
var issuedToken = formValues["token"];
var account = formValues["account"];
//Check the token, and extract the userid crypted in the token
var userId = checkTokenValidityAndGetUserId(issuedToken);
if (userId != null)
{
//hash the User Key
string hash = getHashKey(userId + ":" + account);
//return the hash key
return req.CreateResponse(HttpStatusCode.OK, hash);
}
else
{
return req.CreateResponse(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError);
}
}
catch
{
return req.CreateResponse(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError);
}
}
public static string checkTokenValidityAndGetUserId(string issuedToken)
{
try
{
string secret = "<My extension certificate>"; // Load your extension's secret
var validationParameters = new TokenValidationParameters()
{
IssuerSigningTokens = new List<BinarySecretSecurityToken>()
{
new BinarySecretSecurityToken (System.Text.UTF8Encoding.UTF8.GetBytes(secret))
},
ValidateIssuer = false,
RequireSignedTokens = true,
RequireExpirationTime = true,
ValidateLifetime = true,
ValidateAudience = false,
ValidateActor = false
};
SecurityToken token = null;
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(issuedToken, validationParameters, out token);
//extract the userId from principalClaims
string principalUserId = principal.Claims.FirstOrDefault(q => string.Compare(q.Type,
"https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", true) == 0).Value;return principalUserId;
}
catch
{
return null;
}
}
//Hash the secure userkey
public static string getHashKey(string userkey)
{
if (string.IsNullOrEmpty(userkey))
{
return null;
}
System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
byte[] keyBytes = encoding.GetBytes("sdk-59baef5c-3851-4fef-a6a6-05a6e9c38ea9");
HMACSHA256 hmacSha256 = new HMACSHA256(keyBytes);
byte[] hashedMessage = hmacSha256.ComputeHash(encoding.GetBytes(userkey));
return BitConverter.ToString(hashedMessage).Replace("-", "").ToLower();
}