Azure Function create and read JWT without using Active Directory - azure

I'm trying to create and read (validate) a JSON Web Token (JWT) in an Azure Function using C#. I came across this post:
https://www.codeproject.com/Tips/1208535/Create-And-Consume-JWT-Tokens-in-csharp
which outlines the process very nicely. Being relatively new to Azure Functions, I put the reference to "System.IdentityModel.Tokens.Jwt" in my project.json file like this:
{
"frameworks": {
"net46":{
"dependencies": {
"System.IdentityModel.Tokens.Jwt" : "5.0"
}
}
}
}
The version I used came from this post: Namespaces for .NET JWT token validation: System vs. Microsoft, which talks about versioning issues back in 2016.
Unfortunately, this didn't work. References to SecurityAlgorithms, JwtHeader, JwtPayload, JwtSecurityToken, and JwtSecurityTokenHandler all report, "[run.csx] The type or namespace name 'class name' could not be found (are you missing a using directive or an assembly reference?)".
Researching further, I discovered this page: https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/, which displays Nuget version information for System.IdentityModel.Tokens.Jwt. After trying several versions (by changing the version in my project.json file), I've still had no luck in getting the Function App to recognize the classes I need.
I assume that this is a versioning issue. If so, where can I go to determine which version of "System.IdentityModel.Tokens.Jwt" is compatible with "net46"? I haven't written C# code in years (I'm a Java developer), so I may be wrong about the versioning assumption.
BTW, here's what the code in my function looks like, it's appears exactly like the code sample in: https://www.codeproject.com/Tips/1208535/Create-And-Consume-JWT-Tokens-in-csharp. The only difference is I've wrapped it in a Function App.
using System.Net;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using System.IdentityModel;
using System.Security;
using System.Text;
using System.IdentityModel.Tokens;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
// Define const Key this should be private secret key stored in some safe place
string key = "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1";
// Create Security key using private key above:
// not that latest version of JWT using Microsoft namespace instead of System
var securityKey = new Microsoft
.IdentityModel.Tokens.SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
// Also note that securityKey length should be >256b
// so you have to make sure that your private key has a proper length
//
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials
(securityKey, SecurityAlgorithms.HmacSha256Signature);
// Finally create a Token
var header = new JwtHeader(credentials);
//Some PayLoad that contain information about the customer
var payload = new JwtPayload
{
{ "some ", "hello "},
{ "scope", "http://dummy.com/"},
};
//
var secToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
// Token to String so you can use it in your client
var tokenString = handler.WriteToken(secToken);
// And finally when you received token from client
// you can either validate it or try to read
var token = handler.ReadJwtToken(tokenString);
return req.CreateResponse(HttpStatusCode.Created, "test");
}
So, my questions are:
Which version of System.IdentityModel.Tokens should I use with "net46" in my project file?
The next time this happens, how do I determine myself which versions work together?

I just tried this and saw the same thing. You're missing a reference to System.IdentityModel and a using System.IdentityModel.Tokens.Jwt;
Changing to this got things building:
#r "System.IdentityModel"
using System.Net;
using System.IdentityModel;
using System.Security;
using System.Text;
using System.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
I'd also recommend you move your JWT package reference up to 5.2.4, which is the latest version of that package.

I figured it out. From various sites and hundreds of combinations of versions, it works.
I wish I could explain why, but instead I'll post the working code here with the appropriate libraries listed. If anyone else runs into this problem, I hope it helps. Thanks for looking into this brettsam!
The Function App looks like this:
using System;
using System.Net;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Configuration;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
string token = JwtManager.GenerateToken("rbivens#mydomain.com", 60);
ClaimsPrincipal simplePrinciple = JwtManager.GetPrincipal(token);
var identity = simplePrinciple.Identity as ClaimsIdentity;
log.Info(identity.IsAuthenticated.ToString());
var usernameClaim = identity.FindFirst(ClaimTypes.Name);
var username = usernameClaim ? .Value;
log.Info(username);
return req.CreateResponse(HttpStatusCode.Created, token);
}
public static class JwtManager
{
private static string secret = ConfigurationManager.AppSettings["FunctionsJwtSecret"];
public static string GenerateToken(string username, int expireMinutes = 60)
{
var symmetricKey = Convert.FromBase64String(secret);
var tokenHandler = new JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}),
Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(symmetricKey), SecurityAlgorithms.HmacSha256Signature)
};
var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);
return token;
}
public static ClaimsPrincipal GetPrincipal(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;
if (jwtToken == null)
return null;
var symmetricKey = Convert.FromBase64String(secret);
var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
};
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);
// log.Info(securityToken.ToString());
return principal;
}
catch (Exception)
{
return null;
}
}
}
And the project.json looks like this:
{
"frameworks": {
"net46":{
"dependencies": {
"Microsoft.IdentityModel.Logging" : "1.0.0.127",
"Microsoft.IdentityModel.Tokens" : "5.0.0.127",
"Newtonsoft.Json" : "9.0.0.0",
"System.IdentityModel.Tokens.Jwt" : "5.0.0.127"
}
}
}
}
Again, I don't know why this combination of versions work together, but I hope this saves someone else 20 hours of tedious trial and error.

Related

Azure Cognitive Service\Computer Visio\OCR - Can I use it into into WebSite C#

I'm trying to use Azure Ocr into my website c#.
I added the package Microsoft.Azure.CognitiveServices.Vision.ComputerVision and I wrote code, with key and endpoint of my subscription.
static string subscriptionKey = "mykey";
static string endpoint = "https://myocr.cognitiveservices.azure.com/";
private const string ANALYZE_URL_IMAGE = "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-sample-data-files/master/ComputerVision/Images/printed_text.jpg";
protected void Page_Load(object sender, EventArgs e)
{
// Create a client
ComputerVisionClient client = Authenticate(endpoint, subscriptionKey);
// Analyze an image to get features and other properties.
AnalyzeImageUrl(client, ANALYZE_URL_IMAGE).Wait();
}
public static ComputerVisionClient Authenticate(string endpoint, string key)
{
ComputerVisionClient client =
new ComputerVisionClient(new ApiKeyServiceClientCredentials(key))
{ Endpoint = endpoint };
return client;
}
public static async Task AnalyzeImageUrl(ComputerVisionClient client, string imageUrl)
{
// Read text from URL
var textHeaders = await client.ReadAsync(imageUrl);
...
}
It seems all ok, but at line
var textHeaders = await client.ReadAsync(urlFile);
website crashes.
I don't understand why. No error, it's just stopped.
So I ask: azure ocr can to be use only with console app?
EDIT
The code is ok for ConsoleApp and WebApp but not working for my asp.net WEBSITE.
Could be a problem with async?
We can use OCR with web app also,I have taken the .net core 3.1 webapp in Visual Studio and installed the dependency of Microsoft.Azure.CognitiveServices.Vision.ComputerVision by selecting the check mark of include prerelease as shown in the below image:
After creating computer vision resource in Azure Portal, copied the key and endpoint from there and used inside the c# code.
using System;
using System.Collections.Generic;
using Microsoft.Azure.CognitiveServices.Vision.ComputerVision;
using Microsoft.Azure.CognitiveServices.Vision.ComputerVision.Models;
using System.Threading.Tasks;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.Linq;
namespace ComputerVisionQuickstart
{
class Program
{
// Add your Computer Vision subscription key and endpoint
static string subscriptionKey = "c1****b********";
static string endpoint = ".abc.cognitiveservices.azure.com/";
private const string READ_TEXT_URL_IMAGE = "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-sample-data-files/master/ComputerVision/Images/printed_text.jpg";
static void Main(string[] args)
{
Console.WriteLine("Azure Cognitive Services Computer Vision - .NET quickstart example");
Console.WriteLine();
ComputerVisionClient client = Authenticate(endpoint, subscriptionKey);
// Extract text (OCR) from a URL image using the Read API
ReadFileUrl(client, READ_TEXT_URL_IMAGE).Wait();
}
public static ComputerVisionClient Authenticate(string endpoint, string key)
{
ComputerVisionClient client =
new ComputerVisionClient(new ApiKeyServiceClientCredentials(key))
{ Endpoint = endpoint };
return client;
}
public static async Task ReadFileUrl(ComputerVisionClient client, string urlFile)
{
Console.WriteLine("----------------------------------------------------------");
Console.WriteLine("READ FILE FROM URL");
Console.WriteLine();
// Read text from URL
var textHeaders = await client.ReadAsync(urlFile);
// After the request, get the operation location (operation ID)
string operationLocation = textHeaders.OperationLocation;
Thread.Sleep(2000);
// Retrieve the URI where the extracted text will be stored from the Operation-Location header.
// We only need the ID and not the full URL
const int numberOfCharsInOperationId = 36;
string operationId = operationLocation.Substring(operationLocation.Length - numberOfCharsInOperationId);
// Extract the text
ReadOperationResult results;
Console.WriteLine($"Extracting text from URL file {Path.GetFileName(urlFile)}...");
Console.WriteLine();
do
{
results = await client.GetReadResultAsync(Guid.Parse(operationId));
}
while ((results.Status == OperationStatusCodes.Running ||
results.Status == OperationStatusCodes.NotStarted));
// Display the found text.
Console.WriteLine();
var textUrlFileResults = results.AnalyzeResult.ReadResults;
foreach (ReadResult page in textUrlFileResults)
{
foreach (Line line in page.Lines)
{
Console.WriteLine(line.Text);
}
}
Console.WriteLine();
}
}
}
The above code is taken from the Microsoft Document.
I can be able to read the text inside the image successfully as shown in the below screenshot:

Need help demystifying the new feature introduced on Microsoft.Graph 4.0.0

Question:
I am not sure if this falls under question or code review because the code works where I do not know if it is implemented correctly. But, do we need to acquire the access token from Microsoft.Graph using either silent or interactive modes? From what I can tell the answer is, No. (see Context below)
The new implementation seems to be drastically scaled down with the whole idea of silent and interactive token retrieval being removed. Is this correct?
using Azure.Identity;
using Microsoft.Graph;
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var scopes = new[] { "User.Read" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "SomeGuid";
// Value from app registration
var clientId = "SomeGuid";
var options = new InteractiveBrowserCredentialOptions
{
TenantId = tenantId,
ClientId = clientId,
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
// MUST be http://localhost or http://localhost:PORT
// See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core
RedirectUri = new Uri("http://localhost:1234"),
};
// https://learn.microsoft.com/dotnet/api/azure.identity.interactivebrowsercredential
var interactiveCredential = new InteractiveBrowserCredential(options);
var graphClient = new GraphServiceClient(interactiveCredential, scopes);
// Interactive browser login occurs here.
var me = graphClient.Me.Request().GetAsync().Result;
// Printing the results
Console.WriteLine("-------- Data from call to MS Graph --------");
Console.Write(Environment.NewLine);
Console.WriteLine($"Id: {me.Id}");
Console.WriteLine($"Display Name: {me.DisplayName}");
Console.WriteLine($"Email: {me.Mail}");
//Console.ReadLine();
}
}
}
Context:
As part of our routine maintenance, I was tasked with upgrading our NuGet packages on a Winforms desktop application that is running in Azure and whose users are in Azure Active Directory Services (AADS). One of the packages, Microsoft.Graph, had a major version change. https://www.nuget.org/packages/Microsoft.Graph/4.0.0
The documentation on it indicated a new feature for handling the TokenCredentialClass. https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/4.0.0/docs/upgrade-to-v4.md#new-capabilities
From what I can tell, there is a separate and distinct break on how the token is retrieved. Previously, we followed the method provided here: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-windows-desktop#add-the-code-to-initialize-msal
Old way:
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
_PublicClientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithRedirectUri("http://localhost:1234")
.WithAuthority(AzureCloudInstance.AzurePublic, TenantId)
.Build();
// We sign the user in here
bolIsAutorizeSSO = CallMicrosoftSSO().GetAwaiter().GetResult();
InteractiveAuthenticationProvider = new InteractiveAuthenticationProvider(PublicClientApp, Scopes);
GraphServiceClient = new Microsoft.Graph.GraphServiceClient(InteractiveAuthenticationProvider);
if (bolIsAutorizeSSO)
{
// We also signt the user in here.
var User = GraphServiceClient.Me.Request().GetAsync().Result;
// Printing the results
Console.WriteLine("-------- Data from call to MS Graph --------");
Console.Write(Environment.NewLine);
Console.WriteLine($"Id: {User.Id}");
Console.WriteLine($"Display Name: {User.DisplayName}");
Console.WriteLine($"Email: {User.Mail}");
}
else
{
// signout
Console.ReadLine();
}
}
public static async Task<bool> CallMicrosoftSSO()
{
AuthenticationResult authResult = null;
var app = PublicClientApp;
var accounts = await app.GetAccountsAsync();
try
{
authResult = await app.AcquireTokenInteractive(Scopes)
.WithAccount(accounts.FirstOrDefault())
.WithPrompt(Microsoft.Identity.Client.Prompt.ForceLogin)
.ExecuteAsync();
}
catch (MsalUiRequiredException _Exception)
{
// A MsalUiRequiredException happened on AcquireTokenSilent.
// This indicates you need to call AcquireTokenInteractive to acquire a token.
Console.WriteLine(_Exception.Message);
}
catch (MsalException msalex)
{
if (msalex.ErrorCode != "authentication_canceled")
{
Console.WriteLine(msalex.Message);
}
}
catch (Exception _Exception)
{
Console.WriteLine(_Exception.Message);
}
if (authResult != null)
{
return true;
}
return false;
}
private static string ClientId = "SomeGuid";
private static string TenantId = "SomeGuid";
private static string[] Scopes = new string[] { "User.Read" };
private static Microsoft.Graph.GraphServiceClient GraphServiceClient;
private static bool bolIsAutorizeSSO = false;
private static InteractiveAuthenticationProvider InteractiveAuthenticationProvider;
private static IPublicClientApplication _PublicClientApp;
public static IPublicClientApplication PublicClientApp { get { return _PublicClientApp; } }
}
}
I am struggling to make sense of it. Partly because the feature is brand new and there are very few code samples up on the internet that say do it this way. What I have found seems to point me back to what we already are using (more on that in a bit). So, the examples may not yet be fully updated.

How to call an Azure Function App API with Easy-Auth Enables using Active Directory from a C# Client

I have an Azure Function App with Azure Active Directory configured but when I call if from my client I keep getting an Unauthorized response.
I have tried a couple different scenarios but nothing worked. Below is a snippet of the last bit of code that I tried.
///
var #params2 = new NameValueCollection
{
{"grant_type", "client_credentials"},
{"client_id", $"{ClientId}"},
{"client_secret", $"{ClientSecret}"},
{"username", userId},
{"resource", "https://management.azure.com/"}
};
var queryString2 = HttpUtility.ParseQueryString(string.Empty);
queryString2.Add(#params2);
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"grant_type", "client_credentials"},
{"client_id", ClientId},
{"client_secret", ClientSecret},
{"username", userId}
});
var authorityUri2 = $"{string.Format(CultureInfo.InvariantCulture, AadInstance, Tenant).TrimEnd('/')}/oauth2/token";
//var authorityUri2 = $"https://login.microsoftonline.com/{Tenant}/v2.0/.well-known/openid-configuration";
var authUri2 = String.Format("{0}?{1}", authorityUri2, queryString2);
var client2 = new HttpClient();
var message = client2.PostAsync(authorityUri2, content).Result;
//var message = client2.GetAsync(authorityUri2).Result;
var response = message.Content.ReadAsStringAsync().Result;
dynamic values=null;
try
{
values = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
}
catch
{
values = response;
}
var AuthToken2 = values["access_token"];
client2.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AuthToken2);
HttpResponseMessage response2 = await client2.GetAsync(AppBaseAddress.TrimEnd('/') + "/api/AADIntegration");
if (response.IsSuccessStatusCode)
{
// Read the response and data-bind to the GridView to display To Do items.
string s = await response.Content.ReadAsStringAsync();
log.LogInformation($"Success while getting / api / AADIntegration : {s}");
return (ActionResult)new OkObjectResult(s);
}
else
{
string failureDescription = await response.Content.ReadAsStringAsync();
log.LogInformation($"An error occurred while getting / api / AADIntegration : {response.ReasonPhrase}\n {failureDescription}");
return (ActionResult)new OkObjectResult(failureDescription);
}
Data should returned from the Function App.
For client_credentials grant flow your code seems little different. Here I am giving you exact sample for azure function. Just plug and play :))
Example contains:
How would you get token using client_credentials flow
Getting user list From Azure Active Directory tenant using above
token
Access Token Class:
public class AccessTokenClass
{
public string token_type { get; set; }
public string expires_in { get; set; }
public string resource { get; set; }
public string scope { get; set; }
public string access_token { get; set; }
}
Reference To Add:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net.Http;
using System.Collections.Generic;
using System.Net.Http.Headers;
Azure Function Body:
public static class FunctionGetUserList
{
[FunctionName("FunctionGetUserList")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
try
{
log.LogInformation("C# HTTP trigger function processed a request.");
//Token Request endpoint Just replace yourTennantId/Name
string tokenUrl = $"https://login.microsoftonline.com/yourTennantId/Name.onmicrosoft.com/oauth2/token";
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
tokenRequest.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = "b603c7bead87-Your_client_id-e6921e61f925",
["client_secret"] = "Vxf1SluKbgu4P-Your_client_Secret-F0Nf3wE5oGl/2XDSeZ=",
["resource"] = "https://graph.microsoft.com"
});
dynamic json;
AccessTokenClass results = new AccessTokenClass();
HttpClient client = new HttpClient();
var tokenResponse = await client.SendAsync(tokenRequest);
json = await tokenResponse.Content.ReadAsStringAsync();
results = JsonConvert.DeserializeObject<AccessTokenClass>(json);
var accessToken = results.access_token;
//Create Request To Server
using (HttpClient clientNew = new HttpClient())
{
//Pass Token on header
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
//Get Data from API
var requestToAzureEndpoint = await client.GetAsync("https://graph.microsoft.com/v1.0/users");
if (requestToAzureEndpoint.IsSuccessStatusCode)
{
var result_string = await requestToAzureEndpoint.Content.ReadAsStringAsync();
dynamic responseResults = JsonConvert.DeserializeObject<dynamic>(result_string);
return new OkObjectResult(responseResults);
}
else
{
var result_string = await requestToAzureEndpoint.Content.ReadAsStringAsync();
return new OkObjectResult(result_string);
}
}
}
catch (Exception ex)
{
return new OkObjectResult(ex.Message);
}
}
}
Point To Remember
For Azure Active Directory List users access make sure you have following permission:
User.Read.All
Permission Type: Application
You can check here. See the screen shot for better understanding; make sure you have clicked "Grant admin consent for yourTenant" after adding permission.
Note: This is how you can access Azure Active Directory Token using Azure Function after that how to access resource using that token to a specific API endpoint efficiently.
Are you sure you have properly implemented this properly? It looks like a few of your parameters are wrong for the client credential flow. Please double check that you are properly following the client credential flow.
The client credential grant flow is documented here : https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
But for more information on getting this properly working in your function app, please refer to the blog below for more information/help on implementing this.
https://blogs.msdn.microsoft.com/ben/2018/11/07/client-app-calling-azure-function-with-aad/
The value of resource is not correct.
Replace {"resource", "https://management.azure.com/"} with {"resource", $"{ClientId}"}

How to manually decrypt an ASP.NET Core Authentication cookie?

Let's consider a common-known ASP.NET Core scenario. Firstly we add the middleware:
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "MyCookie",
CookieName = "MyCookie",
LoginPath = new PathString("/Home/Login/"),
AccessDeniedPath = new PathString("/Home/AccessDenied/"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
//...
}
Then serialize a principal:
await HttpContext.Authentication.SignInAsync("MyCookie", principal);
After these two calls an encrypted cookie will be stored at the client side. You can see the cookie (in my case it was chunked) in any browser devtools:
It's not a problem (and not a question) to work with cookies from application code.
My question is: how to decrypt the cookie outside the application? I guess a private key is needed for that, how to get it?
I checked the docs and found only common words:
This will create an encrypted cookie and add it to the current
response. The AuthenticationScheme specified during configuration must
also be used when calling SignInAsync.
Under the covers the encryption used is ASP.NET's Data Protection
system. If you are hosting on multiple machines, load balancing or
using a web farm then you will need to configure data protection to
use the same key ring and application identifier.
So, is it possible to decrypt the authentication cookie, and if so how?
UPDATE #1:
Based on Ron C great answer and comments, I've ended up with code:
public class Startup
{
//constructor is omitted...
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection().PersistKeysToFileSystem(
new DirectoryInfo(#"C:\temp-keys\"));
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "MyCookie",
CookieName = "MyCookie",
LoginPath = new PathString("/Home/Index/"),
AccessDeniedPath = new PathString("/Home/AccessDenied/"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
public class HomeController : Controller
{
public async Task<IActionResult> Index()
{
await HttpContext.Authentication.SignInAsync("MyCookie", new ClaimsPrincipal());
return View();
}
public IActionResult DecryptCookie()
{
var provider = DataProtectionProvider.Create(new DirectoryInfo(#"C:\temp-keys\"));
string cookieValue = HttpContext.Request.Cookies["MyCookie"];
var dataProtector = provider.CreateProtector(
typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(false, true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
return Content(plainText);
}
}
Unfortunately this code always produces exception on Unprotect method call:
CryptographicException in Microsoft.AspNetCore.DataProtection.dll:
Additional information: The payload was invalid.
I tested different variations of this code on several machines without positive result. Probably I made a mistake, but where?
UPDATE #2: My mistake was the DataProtectionProvider hasn't been set in UseCookieAuthentication. Thanks to #RonC again.
Decrypting the Authentication Cookie without needing the keys
It's worth noting that you don't need to gain access to the keys to decrypt the authentication cookie. You simply need to use the right IDataProtector created with the right purpose parameter, and subpurpose parameters.
Based on the CookieAuthenticationMiddleware source code https://github.com/aspnet/Security/blob/rel/1.1.1/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationMiddleware.cs#L4 it looks like the purpose you need to pass is typeof(CookieAuthenticationMiddleware). And since they are passing additional parameters to the IDataProtector you will need to match them. So this line of code should get you an IDataProtector that can be used to decrypt the authentication cookie:
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, Options.AuthenticationScheme, "v2");
Note thatOptions.AuthenticationScheme is just "MyCookie" in this case since that's what it was set to in the Configure method of the startup.cs file.
Here is an example action method for decrypting your authentication cookie two different ways:
public IActionResult DecryptCookie() {
//Get the encrypted cookie value
string cookieValue = HttpContext.Request.Cookies["MyCookie"];
//Get a data protector to use with either approach
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");
//Get the decrypted cookie as plain text
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
//Get the decrypted cookie as a Authentication Ticket
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);
return View();
}
This method uses an IDataProtectionProvider called provider that is constructor injected.
Decrypting the Authentication Cookie when persisting keys to a directory
If you want to share cookies between applications then you might decide to persist the data protection keys to a directory. This can be done by adding the following to the ConfigureServices method of the startup.cs file:
services.AddDataProtection().PersistKeysToFileSystem(
new DirectoryInfo(#"C:\temp-keys\"));
BE CAREFUL though because the keys are not encrypted so it's up to you to protect them!!! Only persist the keys to a directory if you absolutely must, (or if you are just trying to understand how the system works). You will also need to specify a cookie DataProtectionProvider that uses those keys. This can be done with the help of the UseCookieAuthentication configuration in the Configure method of the startup.cs class like so:
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(#"C:\temp-keys\")),
AuthenticationScheme = "MyCookie",
CookieName = "MyCookie",
LoginPath = new PathString("/Home/Login"),
AccessDeniedPath = new PathString("/Home/AccessDenied"),
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
With that configuration done. You can now decrypt the authentication cookie with the following code:
public IActionResult DecryptCookie() {
ViewData["Message"] = "This is the decrypt page";
var user = HttpContext.User; //User will be set to the ClaimsPrincipal
//Get the encrypted cookie value
string cookieValue = HttpContext.Request.Cookies["MyCookie"];
var provider = DataProtectionProvider.Create(new DirectoryInfo(#"C:\temp-keys\"));
//Get a data protector to use with either approach
var dataProtector = provider.CreateProtector(typeof(CookieAuthenticationMiddleware).FullName, "MyCookie", "v2");
//Get the decrypted cookie as plain text
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookieValue);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
//Get teh decrypted cookies as a Authentication Ticket
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookieValue);
return View();
}
You can learn more about this latter scenario here: https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/compatibility/cookie-sharing
While inside ASP.NET Core app you can just use CookieAuthenticationOptions.TicketDataFormat.Unprotect(cookieValue).
Here, a simple static (!) method I wrote:
public static AuthenticationTicket DecryptAuthCookie(HttpContext httpContext)
{
// ONE - grab the CookieAuthenticationOptions instance
var opt = httpContext.RequestServices
.GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>()
.Get(CookieAuthenticationDefaults.AuthenticationScheme); //or use .Get("Cookies")
// TWO - Get the encrypted cookie value
var cookie = opt.CookieManager.GetRequestCookie(httpContext, opt.Cookie.Name);
// THREE - decrypt it
return opt.TicketDataFormat.Unprotect(cookie);
}
Works fine under .NET 5 and .NET 6.
I'm adding this answer for reference, because this question pops up on every search engine if you search for how to manually decrypt ASP.NET auth cookie.
See below a helper method for .NET Core 2 to get claims from a cookie:
private IEnumerable<Claim> GetClaimFromCookie(HttpContext httpContext, string cookieName, string cookieSchema)
{
// Get the encrypted cookie value
var opt = httpContext.RequestServices.GetRequiredService<IOptionsMonitor<CookieAuthenticationOptions>>();
var cookie = opt.CurrentValue.CookieManager.GetRequestCookie(httpContext, cookieName);
// Decrypt if found
if (!string.IsNullOrEmpty(cookie))
{
var dataProtector = opt.CurrentValue.DataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", cookieSchema, "v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
var ticket = ticketDataFormat.Unprotect(cookie);
return ticket.Principal.Claims;
}
return null;
}
As was pointed by #Cirem, the dodgy way of creating a protector is exactly how Microsoft does it (see their code here). Therefore, it may change in future versions.
Another variation for ASP.NET Core 2.2:
var cookieManager = new ChunkingCookieManager();
var cookie = cookieManager.GetRequestCookie(HttpContext, ".AspNetCore.Identity.Application");
var dataProtector = dataProtectionProvider.CreateProtector("Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware", "Identity.Application", "v2");
//Get the decrypted cookie as plain text
UTF8Encoding specialUtf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
byte[] protectedBytes = Base64UrlTextEncoder.Decode(cookie);
byte[] plainBytes = dataProtector.Unprotect(protectedBytes);
string plainText = specialUtf8Encoding.GetString(plainBytes);
//Get teh decrypted cookies as a Authentication Ticket
TicketDataFormat ticketDataFormat = new TicketDataFormat(dataProtector);
AuthenticationTicket ticket = ticketDataFormat.Unprotect(cookie);
I just got this working in Classic ASP.net (4.6.1). Note the following required installs:
Microsoft.Owin.Security.Interop (will come with a bunch of dependencies - note, I used verison 3.0.1 due to an exception, but that might not be necessary).
Microsfot.AspNetCore.DataProtection (will come with a bunch of dependencies)
Standard web stuff for the 4.6.1 framework
The following constants are defined by the framework:
PROVIDER_NAME = "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware"
SCHEME_NAME = "Identity.Application"
COOKIE_NAME = ".AspNetCore.Identity.Application" (can be customized)
The following constants are configuration-specific, but must be the same between applications.
APP_NAME = "Auth.Test.App"
SHARED_KEY_DIR = "C:\\app-keyring"
The Process:
This article was helpful in getting this set up on both sides, but particularly in properly configuring the .Net Core side. Thus, we shall leave that as an exercise for the reader.
Once you have these set up, on the 4.6.1 Decryption Side, the following code will yield the ClaimsIdentity set in (for example) .Net Core 3.0:
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Owin.Security.Interop;
using System.IO;
using System.Security.Claims;
using System.Web;
public static ClaimsIdentity GetClaimsIdentity(HttpContext context)
{
//Get the encrypted cookie value
var cookie = context.Request.Cookies[Constants.COOKIE_NAME];
if (cookie == null) {
return null;
}
var cookieValue = cookie.Value;
//Get a data protector to use with either approach
var keysDir = new DirectoryInfo(Constants.SHARED_KEY_DIR);
if (!keysDir.Exists) { keysDir.Create(); }
var provider = DataProtectionProvider.Create(keysDir,
options => options.SetApplicationName(Constants.APP_NAME));
var dataProtector = provider.CreateProtector(Constants.PROVIDER_NAME, Constants.SCHEME_NAME, "v2");
//Get the decrypted cookie as a Authentication Ticket
var shim = new DataProtectorShim(dataProtector);
var ticketDataFormat = new AspNetTicketDataFormat(shim);
var ticket = ticketDataFormat.Unprotect(cookieValue);
return ticket.Identity;
}

How to get full list of CloudConfiguration from inside a web service at runtime?

ConfigurationManager has AppSettings name-value collection but CloudConfigurationManager has only GetSetting(string) method where you can get the config settings 1 by 1 if you know the key.
Is there a way to get the whole config of the role runtime?
The root cause is that I want to make strong typed configuration in order to abstract it away and make my code more testable. Using CloudConfigurationManager directly is implicit dependency which I want to remove with an abstraction which I want to stub in tests. So I find this practical. Which brings me to my question.
I do not want to use library like fx.configuration.azure because I will have to carry its dependency altogether because it requires inheritance of a base class.
AFAIK, there's no direct method available which will give you this information.
However there's a workaround that you can use. It involves making use of Service Management API's Get Deployment operation. This operation will return an XML and one of the element there is Configuration which contains your service configuration file in Base64 encoded format. You can read this element, convert it into string and parse the XML to get to ConfigurationSettings elements. It's child elements contains all the settings.
For this, you could either write your own wrapper over Service Management REST API or make use of Azure Management Library.
UPDATE
So here's a sample code for listing all configuration settings from Service Configuration File using Azure Management Library. It's a simple console app hacked together in very short amount of time thus has a lot of scope of improvement :). For management certificate, I have used the data from Publish Setting File.
You just have to install Azure Management Library Nuget Package in your console application:
Install-Package Microsoft.WindowsAzure.Management.Libraries
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Management.Compute;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Linq;
namespace ReadConfigurationSettingsUsingAzureManagementLibrary
{
class Program
{
static string subscriptionId = "<subscription-id>";
static string managementCertContents = "<Base64 Encoded Management Certificate String from Publish Setting File>";//Certificate string from Azure Publish Settings file
static string cloudServiceName = "<your cloud service name>";
static string ns = "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration";
static void Main(string[] args)
{
var managementCetificate = new X509Certificate2(Convert.FromBase64String(managementCertContents));
var credentials = new CertificateCloudCredentials(subscriptionId, managementCetificate);
var computeManagementClient = new ComputeManagementClient(credentials);
var response = computeManagementClient.HostedServices.GetDetailed(cloudServiceName);
var deployment = response.Deployments.FirstOrDefault(d => d.DeploymentSlot == Microsoft.WindowsAzure.Management.Compute.Models.DeploymentSlot.Production);
if (deployment != null)
{
var config = deployment.Configuration;
XElement configXml = XElement.Parse(config);
var roles = configXml.Descendants(XName.Get("Role", ns));
foreach (var role in roles)
{
Console.WriteLine(role.Attribute("name").Value);
Console.WriteLine("-----------------------------");
var configurationSettings = role.Element(XName.Get("ConfigurationSettings", ns));
foreach (var element in configurationSettings.Elements(XName.Get("Setting", ns)))
{
var settingName = element.Attribute("name").Value;
var settingValue = element.Attribute("value").Value;
Console.WriteLine(string.Format("{0} = {1}", settingName, settingValue));
}
Console.WriteLine("==========================================");
}
}
Console.ReadLine();
}
}
}
Here is an updated implementation which takes care if you are running in emulator or not and if you are running in local web server or not. After returning the dictionary it can be easily abstracted away from the whole application by Castle.DictionaryAdapter. I shared the code as template project on GitHub here. Here is an excerpt:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Xml;
using System.Xml.Linq;
using Castle.Components.DictionaryAdapter;
using Core.Configuration.Interfaces;
using Microsoft.Azure;
using Microsoft.WindowsAzure.Management.Compute;
using Microsoft.WindowsAzure.Management.Compute.Models;
using Microsoft.WindowsAzure.ServiceRuntime;
namespace Core.Configuration
{
public class AzureServiceConfigurationProvider : IAzureServiceConfigurationProvider
{
private readonly string _subscriptionId;
// The Base64 Encoded Management Certificate string from Azure Publish Settings file
// download from https://manage.windowsazure.com/publishsettings/index
private readonly string _managementCertContents;
private readonly string _cloudServiceName;
private readonly string _serviceConfigurationNamespace;
public DefaultAzureServiceConfigurationProvider(IWebConfigSettings webConfigSettings)
{
_subscriptionId = webConfigSettings.SubscriptionId;
_managementCertContents = webConfigSettings.ManagementCertContents;
_cloudServiceName = webConfigSettings.CloudServiceName;
_serviceConfigurationNamespace = webConfigSettings.ServiceConfigurationNamespace;
}
public Dictionary<string, Dictionary<string, string>> GetConfigRaw()
{
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigRaw->Start");
var configuration = new Dictionary<string, Dictionary<string, string>>();
var configXml = GetConfigXml();
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigRaw->XmlExtracted");
var roles = configXml.Descendants(XName.Get("Role", _serviceConfigurationNamespace));
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigRaw->Roles : ");
foreach(var role in roles)
{
var roleConfiguration = new Dictionary<string, string>();
var roleName = role.Attribute("name").Value;
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigRaw->RoleName : " + roleName);
var configurationSettings = role.Element(XName.Get("ConfigurationSettings", _serviceConfigurationNamespace));
if (configurationSettings == null)
{
throw new InvalidOperationException("configurationSettings is null");
}
foreach(var element in configurationSettings.Elements(XName.Get("Setting", _serviceConfigurationNamespace)))
{
var settingName = element.Attribute("name").Value;
var settingValue = element.Attribute("value").Value;
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigRaw->settingName : " + settingName + " settingValue : " + settingValue);
roleConfiguration.Add(settingName, settingValue);
}
configuration.Add(roleName, roleConfiguration);
}
return configuration;
}
public IAzureServiceConfiguration GetConfig()
{
var configFactory = new DictionaryAdapterFactory();
IAzureServiceConfiguration config;
try
{
var rawAzureServiceConfig = GetConfigRaw();
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfig :");
var rawAzureWebServiceConfig = rawAzureServiceConfig["Core.Web"];
config = configFactory.GetAdapter<IAzureServiceConfiguration>(rawAzureWebServiceConfig);
config = ComplementConfigurationFromConfigurationManager(config);
}
catch(Exception exception)
{
// happens in some projects when using Full Emulator
// so we fallback to cloudconfigurationmanager
// this is not bad since we have isolated it in configuration assembly
Trace.WriteLine(exception.Message);
Trace.WriteLine(exception.StackTrace);
Hashtable hashConfig = GetConfigFromConfigurationManager();
config = configFactory.GetAdapter<IAzureServiceConfiguration>(hashConfig);
}
return config;
}
private IAzureServiceConfiguration ComplementConfigurationFromConfigurationManager(IAzureServiceConfiguration config)
{
Trace.WriteLine("Complementing configuration");
var azureConfigType = config.GetType();
foreach(PropertyInfo property in config.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
{
var xmlConfigValue = CloudConfigurationManager.GetSetting(property.Name);
var liveConfigPropValue = (string)azureConfigType.GetProperty(property.Name).GetValue(config, null);
if(string.IsNullOrEmpty(liveConfigPropValue))
{
Trace.WriteLine(property.Name + " in live config is empty. Complementing with '" + xmlConfigValue + "' from ConfigurationManager.");
property.SetValue(config, xmlConfigValue);
}
// do something with the property
}
return config;
}
private Hashtable GetConfigFromConfigurationManager()
{
Hashtable hashConfig = new Hashtable();
var configProperties = typeof(IAzureServiceConfiguration).GetProperties();
foreach(PropertyInfo prop in configProperties)
{
hashConfig.Add(prop.Name, CloudConfigurationManager.GetSetting(prop.Name));
}
return hashConfig;
}
private XElement GetConfigXml()
{
XElement configXml = null;
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigXml");
if(!RoleEnvironment.IsAvailable/*as local web project*/ || RoleEnvironment.IsEmulated /*as azure emulator project*/)
{
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigXml->!RoleEnvironment.IsAvailable || RoleEnvironment.IsEmulated");
try
{
var localConfigFile =
new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).Parent.EnumerateFiles(
"*Local.cscfg", SearchOption.AllDirectories).FirstOrDefault();
XmlDocument doc = new XmlDocument();
doc.Load(localConfigFile.FullName);
configXml = XElement.Parse(doc.InnerXml);
}
catch(Exception exception) // happens in some projects when using Full Emulator
{
Trace.WriteLine(exception.Message);
Trace.WriteLine(exception.StackTrace);
throw; // intended - just marking - will catch it above
}
}
else
{
Trace.WriteLine("DefaultAzureServiceConfigurationProvider->GetConfigXml->RoleEnvironment ->in cloud");
var managementCertificate = new X509Certificate2(Convert.FromBase64String(_managementCertContents));
var credentials = new CertificateCloudCredentials(_subscriptionId, managementCertificate);
var computeManagementClient = new ComputeManagementClient(credentials);
var response = computeManagementClient.HostedServices.GetDetailed(_cloudServiceName);
var deployment = response.Deployments.FirstOrDefault(d => d.DeploymentSlot == DeploymentSlot.Production);
if(deployment != null)
{
var config = deployment.Configuration;
configXml = XElement.Parse(config);
}
}
return configXml;
}
}
internal static class TypeHelpers
{
public static bool IsNumber(this object value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}
public static bool IsString(this object value)
{
return value is string;
}
}
}

Resources