In a previous article I talked about how to authenticate your function application against Azure Active Directory Business to Consumer (which we're going to call B2C for the sake of my fingers). Chances are in your function you're going to want to get some of the information which is available as a claim from the bearer token. Here is how to do it.
On the surface this seems like a really simple problem. After all you can take the bearer token you got and paste it into jwt.io and get back all the information you want. However we should probably take some effort to validate that what's coming back is what was originally derived from the B2C login. We can do this by making use of the System.IdentityModel.Tokens.Jwt NugGet package along with a bunch of other cryptographic stuff from the framework.
In my case I'm most interested in the emails claim so I can send the user an e-mail. The first thing we need to do is get the Issuer Signing key from Azure B2C. The easiest way to do this is to use the json metadata endpoint your service provided.
If you click on that link you'll get a document like
The information in here is what's needed to decode the JWT and validate it. The keys here can rotate so if you're going to cache the information you'll want to make sure it expires with some frequency. I've seen an hour recommended but your mileage may vary. I followed the very helpful post here for how to consume this information.
In my function I grabbed the bearer token out of the request and passed it
1 2 3 4 5 6 7 8 9 10 11 12 13 14
var bearerToken = request.Headers["Authorization"].ToString().Split(' ').Last(); var json = await client.GetStringAsync(configService.TokenMetadataEndpoint()); //client is a static HTTP Client JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); var claims = handler.ValidateToken(bearerToken, new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true, ValidateLifetime = true, ValidIssuer = configService.TokenIssuer(), ValidAudience = configService.TokenAudience(), IssuerSigningKeys = GetSecurityKeys(JsonConvert.DeserializeObject<JsonWebKeySet>(json)) }, outvar validatedToken).Claims;
The token audience is the ID of the API application in Azure B2C. The token issuer I had trouble figuring out, in the end the only place I could find it was in a known JWT that I decoded. For me it ended up being https://yourtenant.b2clogin.com/a04a23ad-1e6a-4114-a91b-b8f8f7ac9660/v2.0/. I'm not sure if that GUID in there changes from tenant to tenant but certainly the host name in the URL will change to yours. We want to make sure to validate that the JWT hasn't expired (lifetime) that it was issued by our B2C instance (issuer) and that it was intended for this application (audience).
The claims object here will contain all of the claims for the JWT. I'm interested in emails so I run
1
var email = claims.Single(x => x.Type == "emails");
Getting the security keys is done via this handy function which deserializes the keys into a list of SecurityKeys.
var rsaParameters = new RSAParameters { Exponent = exponent, Modulus = modulus };
var rsaSecurityKey = new RsaSecurityKey(rsaParameters) { KeyId = key.Kid };
keys.Add(rsaSecurityKey); } else { thrownew Exception("JWK data is missing in token validation"); } } else { thrownew NotImplementedException("Only RSA key type is implemented for token validation"); } }
staticbyte[] Base64UrlDecode(string arg) { string s = arg; s = s.Replace('-', '+'); // 62nd char of encoding s = s.Replace('_', '/'); // 63rd char of encoding switch (s.Length % 4) // Pad with trailing '='s { case0: break; // No pad chars in this case case2: s += "=="; break; // Two pad chars case3: s += "="; break; // One pad char default: thrownew System.Exception( "Illegal base64url string!"); } return Convert.FromBase64String(s); // Standard base64 decoder }
As with all things security related this stuff is confusing and convoluted. Hopefully this post will help out somebody in the future.