Client Certificate - 403 - Forbidden: Access is denied - c#

When a third party tries to call my API endpoint with the certificate in .cer format, which I exported from the .pfx file and sent to them.
They will get 403 - Forbidden: Access is denied. You do not have permission to view this directory or page using the credentials that you supplied.
I investigate what could be caused this problem.
When i Install/import Certificate in .cer format in certificate store under Personal and than i try to call my endpoint i can see my certificate is NOT on list of certificates and than i hit ok button i will get 403 - Forbidden: Access is denied.
BUT
When i Install/import Certificate in .pfx format with passepharse in certificate store under Personal and then i try to call my endpoint on browser and also with postman THIS time i can see my certificate in the list of certificates on browser and than i choose the certificate and hit button i will successfully coming into directory and i also get 200 ok response in postman with ofcourse add certificate in .pfx format in postman.
And im confused now 3rd party only accept Client Certificate in .cer format and as i understand .pfx is for inside organization and not for outside organization.
** I should note my Client Certificate contains no Private Key its only contains Public key.
** I'm sure All configuration on server and IIS it is correct.
** I'm not sure how to add private key into my Client certificate with .cer format! or should i ?!
Did i missed something here ! its been 4 days working on this but still no luck :(
Can anyone please help me or point me into right direction! thanks :)
This is how i get Client certificate in ASP.NET Core 3.1:
MyCertificateValidationService.cs
I compared the client certificate I have with the client certificate I get from the request:
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
try
{
var _path = #"c:\temp\ClientCertification.cer";
var cert2 = new X509Certificate2(File.ReadAllBytes(_path));
if (clientCertificate.Thumbprint == cert2.Thumbprint)
{
return true;
}
}
catch (System.Exception)
{
throw;
}
return false;
}
My API Endpoint:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[Consumes("application/xml")]
[Produces("application/xml")]
[ProducesResponseType(typeof(DespatchAdvice), (int)HttpStatusCode.OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesDefaultResponseType]
[HttpPost("SendDespatch")]
public IActionResult SendDespatch([FromBody] DespatchAdvice despatches)
{
//do something
}
}
Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<MyCertificateValidationService>();
services.AddSingleton<MailHandler>();
services.AddScoped<IDespatch, DespatchRepo>();
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options => // code from ASP.NET Core sample
{
// https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth
options.AllowedCertificateTypes = CertificateTypes.All;
//options.RevocationMode = X509RevocationMode.NoCheck;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
else
{
context.Fail("invalid cert");
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization();
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-ARR-ClientCert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = StringToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
services.AddControllers().AddXmlSerializerFormatters();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
private static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
Program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
=> WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
var cert = new X509Certificate2(Path.Combine("cert.pfx"), "password");
options.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = cert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
})
.Build();
}

Let's clarify how SSL client certificate authentication works. Below I assume that "certificate" never contains private key, only public key.
Client provides a certificate he owns during SSL handshake. Server validates that this certificate matches some arbitrary criterias, for example server might require that it was issued by certain certification authority, or, like in your case - that this is a specific certificate (it's thumbprint matches what you expect).
Now, client must prove to server that he actually owns private key for this certificate, in simple terms by signing some info with that private key, and server then verifies that with client's certificate (sent before, as described above) public key.
If client successfully proves he owns private key for given certificate, AND that certificate matches server's criterias - then client is authenticated and can proceed.
You can already see why your current approach cannot work - .cer file you are sending to client does not contain private key, so it cannot be used for authentication purposes.
Now you have two ways:
1 - YOU generate fresh certificate, and send both certificate and private key to your client. Variation of this is when you create your own certification authority and then issue such client certificate under that authority. Then in validation code you can just ensure that certificate was issued by your authority instead of direct comparision of thumbprints. This is reasonable way if you have thousands of clients.
Drawback of such approach is that you now have (or had at one point) secure information you do not need - that is private key of certificate issued for your client. If unauthorized access had happened using that client's private key (client had his private key stolen) - client in theory can claim that YOU leaked this key. Another drawback is that you have to pass sensitive data (private key) over some, preferrably secure channel. If you just email private key to the client - anything bad can happen (like client won't delete it, then later his email is hacked and key leaks to the hacker).
2 - YOUR CLIENT generates certificate and private key. This is the best way in case you have not much clients. Client stores private key for himself and sends you certificate (say in .cer format) which does not contain private key. Now he authenticates as described above, and you just validate that certificate provided in SSL handshake matches certificate sent to you by client beforehand (like you are doing now, by comparing thumbprint). Asp.net then ensures that client has matching private key for this certificate.
Now, no sensitive data has to be sent anywhere, and in case client leaks his private key - you cannot be responsible for that since you never ever had this key in the first place.
Side note: if you are going 1st route by generating certificate for your client - note that it's a fresh certificate, completely unrelated to your server certificate. Your server certificate private key should of course not be ever sent anywhere. That's related to your comment "but it’s not dangerous to send pfx to Them ?!". No, because you just generated this pfx specifically for that client.

Related

Kestrel isn't "using" pfx file for https

My local development is wanting to install a localhost ssl certificate even though I have specified a pfx I want it to use.
core API .NET 5.0
My understanding was that kestrel was used by default so this is what I have done.
In my hosts file I setup a FQDN.
127.0.0.1 myapi.mysite.com
In my program.cs I added ConfigureKestrel
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.ConfigureKestrel(options =>
{
options.Listen(IPAddress.Loopback, 44394, listenOptions =>
{
var serverCertificate = LoadCertificate();
listenOptions.UseHttps(serverCertificate); // <- Configures SSL
});
});
});
private static X509Certificate2 LoadCertificate()
{
var assembly = typeof(Startup).GetTypeInfo().Assembly;
var embeddedFileProvider = new EmbeddedFileProvider(assembly, "My.API");
var certificateFileInfo = embeddedFileProvider.GetFileInfo("wildcard_mydomain_com.pfx");
using (var certificateStream = certificateFileInfo.CreateReadStream())
{
byte[] certificatePayload;
using (var memoryStream = new MemoryStream())
{
certificateStream.CopyTo(memoryStream);
certificatePayload = memoryStream.ToArray();
}
return new X509Certificate2(certificatePayload, "password");
}
}
I do not get any errors but I don't get the custom domain certificate presented to the browser either. The browser tell me I am unsecure and wants me to accept the localhost cert....which I don't want to accept. I want my custom domain cert to be used.
Am I not understanding how the certs work in a browser?
Doesn't the web server present the cert to the browser and the browser checks to see if it is issued by a legit CA?
Am I loading my custom cert but not "attaching" it to the development server to use?

AspNet.Core, IdentityServer 4: Unauthorized (401) during websocket handshake with SignalR 1.0 using JWT bearer token

I have two aspnet.core services. One for IdentityServer 4, and one for the API used by Angular4+ clients. The SignalR hub runs on the API. The whole solution runs on docker but that should not matter (see below).
I use implicit auth flow which works flawlessly. The NG app redirects to the login page of IdentityServer where the user logs in. After that the browser is redirected back to the NG app with the access token. The token is then used to call the API and to build up the communication with SignalR. I think I've read everything that is available (see sources below).
Since SignalR is using websockets that does not support headers, the token should be sent in the querystring. Then on the API side the token is extracted and set for the request just as it was in the header. Then the token is validated and the user is authorized.
The API works without any problem the users gets authorized and the claims can be retrieved on the API side. So there should be no problem with the IdentityServer then since SignalR does not need any special configuration. Am I right?
When I do not use the [Authorized] attribute on the SignalR hub the handshake succeeds. This is why I think there is nothing wrong with the docker infrastructure and reverse proxy I use (the proxy is set to enable websockets).
So, without authorization SignalR works. With authorization the NG client gets the following response during handshake:
Failed to load resource: the server responded with a status of 401
Error: Failed to complete negotiation with the server: Error
Error: Failed to start the connection: Error
The request is
Request URL: https://publicapi.localhost/context/negotiate?signalr_token=eyJhbGciOiJSUz... (token is truncated for simplicity)
Request Method: POST
Status Code: 401
Remote Address: 127.0.0.1:443
Referrer Policy: no-referrer-when-downgrade
The response I get:
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:4200
content-length: 0
date: Fri, 01 Jun 2018 09:00:41 GMT
server: nginx/1.13.10
status: 401
vary: Origin
www-authenticate: Bearer
According to the logs, the token is validated successfully. I can include the full logs however I suspect where the problem is. So I will include that part here:
[09:00:41:0561 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
[09:00:41:0564 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Identity.Application was not authenticated.
I get these in the log file and I am not sure what it means. I include the code part on the API where I get and extract the token along with the authentication configuration.
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultForbidScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(options =>
{
options.Authority = "http://identitysrv";
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
options.JwtBearerEvents.OnMessageReceived = context =>
{
if (context.Request.Query.TryGetValue("signalr_token", out StringValues token))
{
context.Options.Authority = "http://identitysrv";
context.Options.Audience = "publicAPI";
context.Token = token;
context.Options.Validate();
}
return Task.CompletedTask;
};
});
There are no other errors, exceptions in the system. I can debug the app and everything seems to be fine.
What does the included log lines mean?
How can I debug what is going on during the authorization?
EDIT: I almost forgot to mention, that I thought the problem was with the authentication schemes so, I set every scheme to the one I think was needed. However sadly it did not help.
I am kind of clueless here, so I appreciate any suggestion. Thanks.
Sources of information:
Pass auth token to SignalR
Securing SignalR with IdentityServer
Microsoft docs on SignalR authorization
Another GitHub question
Authenticate against SignalR
Identity.Application was not authenticated
I have to answer my own question because I had a deadline and surprisingly I managed to solve this one. So I write it down hoping it is going to help someone in the future.
First I needed to have some understanding what was happening, so I replaced the whole authorization mechanism to my own. I could do it by this code. It is not required for the solution, however if anyone needed it, this is the way to do.
services.Configure<AuthenticationOptions>(options =>
{
var scheme = options.Schemes.SingleOrDefault(s => s.Name == JwtBearerDefaults.AuthenticationScheme);
scheme.HandlerType = typeof(CustomAuthenticationHandler);
});
With the help of IdentityServerAuthenticationHandler and overriding every possible method I finally understood that the OnMessageRecieved event is executed after the token is checked. So if there weren't any token during the call for HandleAuthenticateAsync the response would be 401. This helped me to figure out where to put my custom code.
I needed to implement my own "protocol" during token retrieval. So I replaced that mechanism.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme,
options =>
{
options.Authority = "http://identitysrv";
options.TokenRetriever = CustomTokenRetriever.FromHeaderAndQueryString;
options.RequireHttpsMetadata = false;
options.ApiName = "publicAPI";
});
The important part is the TokenRetriever property and what comes to replace it.
public class CustomTokenRetriever
{
internal const string TokenItemsKey = "idsrv4:tokenvalidation:token";
// custom token key change it to the one you use for sending the access_token to the server
// during websocket handshake
internal const string SignalRTokenKey = "signalr_token";
static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }
static CustomTokenRetriever()
{
AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
}
public static string FromHeaderAndQueryString(HttpRequest request)
{
var token = AuthHeaderTokenRetriever(request);
if (string.IsNullOrEmpty(token))
{
token = QueryStringTokenRetriever(request);
}
if (string.IsNullOrEmpty(token))
{
token = request.HttpContext.Items[TokenItemsKey] as string;
}
if (string.IsNullOrEmpty(token) && request.Query.TryGetValue(SignalRTokenKey, out StringValues extract))
{
token = extract.ToString();
}
return token;
}
And this is my custom token retriever algorithm that tries the standard header and query string first to support the common situations such as web API calls. But if the token is still empty it tries to get it from the query string where client put it during websocket handshake.
EDIT: I use the following client side (TypeScript) code in order to provide the token for the SignalR handshake
import { HubConnection, HubConnectionBuilder, HubConnectionState } from '#aspnet/signalr';
// ...
const url = `${apiUrl}/${hubPath}?signalr_token=${accessToken}`;
const hubConnection = new HubConnectionBuilder().withUrl(url).build();
await hubConnection.start();
Where apiUrl, hubPath and accessToken are the required parameters of the connection.
I know this is an old thread, but in case someone stumbles upon this like I did. I found an alternative solution.
TLDR: JwtBearerEvents.OnMessageReceived, will catch the token before it is checked when used as illustrated below:
public void ConfigureServices(IServiceCollection services)
{
// Code removed for brevity
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://myauthority.io";
options.ApiName = "MyApi";
options.JwtBearerEvents = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/myhubname")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
This Microsoft doc gave me a hint:
https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.1. However, in the Microsoft example, options.Events is called, because it is not an example using IdentityServerAuthentication. If options.JwtBearerEvents is used the same way as options.Events in the Microsoft example, IdentityServer4 is happy!
Let me put my two cents to this. I think most of us store tokens in cookies and during WebSockets handshake they are also sent to the server, so I suggest using token retrieval from cookie.
To do this add this below last if statement:
if (string.IsNullOrEmpty(token) && request.Cookies.TryGetValue(SignalRCookieTokenKey, out string cookieToken))
{
token = cookieToken;
}
Actually we could delete retrieval from query string at all as according to Microsoft docs this is not truly secure and can be logged somewhere.

Proper way to test authenticated methods (using bearer tokens) in C# (web api)

I have a Web API with tons of methods that all require a bearer token to be present in order to be used. These methods all extract information from the bearer token.
I want to test whether the API is properly populating the bearer token upon its generation. I'm using the Microsoft.Owin.Testing framework to write my tests. I have a test that looks like this:
[TestMethod]
public async Task test_Login()
{
using (var server = TestServer.Create<Startup>())
{
var req = server.CreateRequest("/authtoken");
req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
req.And(x => x.Content = new StringContent("grant_type=password&username=test&password=1234", System.Text.Encoding.ASCII));
var response = await req.GetAsync();
// Did the request produce a 200 OK response?
Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK);
// Retrieve the content of the response
string responseBody = await response.Content.ReadAsStringAsync();
// this uses a custom method for deserializing JSON to a dictionary of objects using JSON.NET
Dictionary<string, object> responseData = deserializeToDictionary(responseBody);
// Did the response come with an access token?
Assert.IsTrue(responseData.ContainsKey("access_token"));
}
}
So I'm able to retrieve the string that represents the token. But now I want to actually access that token's contents, and make sure that certain claims were provided.
Code that I would use in an actual authenticated method to check the claims looks like this:
var identity = (ClaimsIdentity)User.Identity;
IEnumerable<Claim> claims = identity.Claims;
var claimTypes = from x in claims select x.Type;
if (!claimTypes.Contains("customData"))
throw new InvalidOperationException("Not authorized");
So what I want to be able to do is, within my test itself, provide the bearer token string and reeceive a User.Identity object or in some other way gain access to the claims that token contains. This is how I want to test whether my method is properly adding the necessary claims to the token.
The "naive" approach could be to write a method in my API that simply returns all the claims in the bearer token it is given. But it feels like this should be unnecessary. ASP.NET is somehow decoding the given token to an object before my controller's method is called. I want to replicate the same action on my own, in my test code.
Can this be done? If so, how?
EDIT: My OWIN startup class instantiates an authentication token provider that I have coded which handles authentication and token generation. In my startup class I have this:
public void Configuration(IAppBuilder app)
{
// Setup configuration object
HttpConfiguration config = new HttpConfiguration();
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// configure the OAUTH server
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//AllowInsecureHttp = false,
AllowInsecureHttp = true, // THIS HAS TO BE CHANGED BEFORE PUBLISHING!
TokenEndpointPath = new PathString("/authtoken"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new API.Middleware.MyOAuthProvider()
};
// Now we setup the actual OWIN pipeline.
// setup CORS support
// in production we will only allow from the correct URLs.
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
// insert actual web API and we're off!
app.UseWebApi(config);
}
Here is the relevant code from my OAuth provider:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Will be used near end of function
bool isValidUser = false;
// Simple sanity check: all usernames must begin with a lowercase character
Match testCheck = Regex.Match(context.UserName, "^[a-z]{1}.+$");
if (testCheck.Success==false)
{
context.SetError("invalid_grant", "Invalid credentials.");
return;
}
string userExtraInfo;
// Here we check the database for a valid user.
// If the user is valid, isValidUser will be set to True.
// Invalid authentications will return null from the method below.
userExtraInfo = DBAccess.getUserInfo(context.UserName, context.Password);
if (userExtraInfo != null) isValidUser = true;
if (!isValidUser)
{
context.SetError("invalid_grant", "Invalid credentials.");
return;
}
// The database validated the user. We will include the username in the token.
string userName = context.UserName;
// generate a claims object
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
// add the username to the token
identity.AddClaim(new Claim(ClaimTypes.Sid, userName));
// add the custom data on the user to the token.
identity.AddClaim(new Claim("customData", userExtraInfo));
// store token expiry so the consumer can determine expiration time
DateTime expiresAt = DateTime.Now.Add(context.Options.AccessTokenExpireTimeSpan);
identity.AddClaim(new Claim("expiry", expiresAt.ToString()));
// Validate the request and generate a token.
context.Validated(identity);
}
The unit test would want to ensure that the customData claim is in fact present in the authentication token. So thus my need for a way to evaluate the token provided to test which claims it contains.
EDIT 2: I've spent some time looking over the Katana source code and searching out some other posts online, and it looks like it's important that I'm hosting this app on IIS, so I would be using SystemWeb. It looks like SystemWeb uses Machine Key encryption for the token. It also looks like the AccessTokenFormat parameter in the options is relevant here.
So now what I'm wondering is if I can instantiate my own "decoder" based on this knowledge. Assuming I will only ever be hosting on IIS, can I instantiate a decoder that can then decode the token and convert it into a Claims object?
The docs on this are kind of sparse and the code seems to throw you all over the place, a lot to try to keep straight in my head.
EDIT 3: I found a project that contains what is supposed to be a bearer token deserializer. I adapted the code in its "API" library and have been trying to use it to decrypt the tokens generated by my API.
I generated a <machineKey...> value using a PowerShell script from Microsoft and placed it both in the Web.config file of the API itself and the App.confg file in the test project.
The tokens still fail to decrypt, however. I receive a Exception thrown: System.Security.Cryptography.CryptographicException with the message "Error occurred during a cryptographic operation." The following is the stacktrace of the error:
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.HomogenizeErrors(Func`2 func, Byte[] input)
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.Unprotect(Byte[] protectedData)
at System.Web.Security.MachineKey.Unprotect(ICryptoServiceProvider cryptoServiceProvider, Byte[] protectedData, String[] purposes)
at System.Web.Security.MachineKey.Unprotect(Byte[] protectedData, String[] purposes)
at MyAPI.Tests.BearerTokenAPI.MachineKeyDataProtector.Unprotect(Byte[] protectedData) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 251
at MyAPI.Tests.BearerTokenAPI.SecureDataFormat`1.Unprotect(String protectedText) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 287
At this point I'm stumped. With the MachineKey value set to the same across the entire project, I don't see why I'm unable to decrypt the tokens. I'm guessing the cryptographic error is being deliberately vague, but I am not sure where to start with figuring this out now.
And all I wanted to do was test that the token contains the desired data in a unit test.... :-)
I was finally able to figure out a solution. I added a public variable to my Startup class that exposes the OAuthBearerAuthenticationOptions object passed into the UseBearerTokenAuthentication method. From that object, I'm able to call AccessTokenFormat.Unprotect and get a decrypted token.
I also rewrote my test to instantiate the Startup class separately, so that I have access to the value from within the test.
I still don't understand why the MachineKey thing isn't working, why I'm not able to directly unprotect the token. It would seem that as long as the MachineKey's match, I should be able to decrypt the token, even manually. But at least this seems to work, even if it's not the best solution.
This could probably be done more cleanly, for instance perhaps the Startup class could somehow detect if it's being started under test and pass the object to the test class in some other fashion rather than leaving it hanging out there in the breeze. But for now this seems to do exactly what I needed.
My startup class exposes the variable this way:
public partial class Startup
{
public OAuthBearerAuthenticationOptions oabao;
public void Configuration(IAppBuilder app)
{
// repeated code omitted
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
oabao = new OAuthBearerAuthenticationOptions();
app.UseOAuthBearerAuthentication(oabao);
// insert actual web API and we're off!
app.UseWebApi(config);
}
}
My test now looks like this:
[TestMethod]
public async Task Test_SignIn()
{
Startup owinStartup = new Startup();
Action<IAppBuilder> owinStartupAction = new Action<IAppBuilder>(owinStartup.Configuration);
using (var server = TestServer.Create(owinStartupAction))
{
var req = server.CreateRequest("/authtoken");
req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
// repeated code omitted
// Is the access token of an appropriate length?
string access_token = responseData["access_token"].ToString();
Assert.IsTrue(access_token.Length > 32);
AuthenticationTicket token = owinStartup.oabao.AccessTokenFormat.Unprotect(access_token);
// now I can check whatever I want on the token.
}
}
Hopefully all my efforts will help someone else trying to do something similar.

Firebase .NET Token Verification

Working on a project that uses Firebase for some data storage, and our client requests that the server be implemented with C#.NET. We're setting up REST endpoints on the server so that the client is able to communicate with it for a few purposes (for example, triggering an algorithm to run which can only occur on the server).
Firebase recommends we identify users via an ID token, as noted here: https://firebase.google.com/docs/auth/server/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library
Since there is no official .NET Firebase server SDK that supports token authentication, we've resorted to using a 3rd-party JWT library to do this: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet
As specified in the Firebase documentation, we're first generating a sending a token to the server. After checking a few different fields in the token, we're using the kid field to grab the public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com
We've been digging around through documentation and StackOverflow for a long time, but we can't find a way to use this public key to do this, as specified by the Firebase documentation:
Finally, ensure that the ID token was signed by the private key corresponding to the token's kid claim. Grab the public key from https://www.googleapis.com/robot/v1/metadata/x509/securetoken#system.gserviceaccount.com and use a JWT library to verify the signature.
The Firebase documentation doesn't really offer any explanation for this, and neither does the documentation for the library we are using. So we've not been able to even get a basic idea as to how we could possibly verify the token was signed by a private key when all we are given is the public key.
What would be the best way to verify that the token was actually signed by the correct private key?
You should be able to accomplish the token validation by doing something like the following, which leverages the System.IdentityModel.Tokens.Jwt Nuget package to perform most the validations:
class Program {
static HttpClient client = new HttpClient();
static void Main() { RunAsync().Wait(); }
static async Task RunAsync() {
string encodedJwt = "[TOKEN_TO_BE_VALIDATED]";
// 1. Get Google signing keys
client.BaseAddress = new Uri("https://www.googleapis.com/robot/v1/metadata/");
HttpResponseMessage response = await client.GetAsync(
"x509/securetoken#system.gserviceaccount.com");
if (!response.IsSuccessStatusCode) { return; }
var x509Data = await response.Content.ReadAsAsync<Dictionary<string, string>>();
SecurityKey[] keys = x509Data.Values.Select(CreateSecurityKeyFromPublicKey).ToArray();
// 2. Configure validation parameters
const string FirebaseProjectId = "[FIREBASE_PROJECT_ID]";
var parameters = new TokenValidationParameters {
ValidIssuer = "https://securetoken.google.com/" + FirebaseProjectId,
ValidAudience = FirebaseProjectId,
IssuerSigningKeys = keys,
};
// 3. Use JwtSecurityTokenHandler to validate signature, issuer, audience and lifetime
var handler = new JwtSecurityTokenHandler();
SecurityToken token;
ClaimsPrincipal principal = handler.ValidateToken(encodedJwt, parameters, out token);
var jwt = (JwtSecurityToken)token;
// 4.Validate signature algorithm and other applicable valdiations
if (jwt.Header.Alg != SecurityAlgorithms.RsaSha256) {
throw new SecurityTokenInvalidSignatureException(
"The token is not signed with the expected algorithm.");
}
}
static SecurityKey CreateSecurityKeyFromPublicKey(string data) {
return new X509SecurityKey(new X509Certificate2(Encoding.UTF8.GetBytes(data)));
}
}
List of using statements for sample program:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
Now we can use the Firebase Admin SDK for .NET.
https://github.com/Firebase/firebase-admin-dotnet
var decoded = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
var uid = decoded.Uid;
Use the following code snippet in Startup.cs to create a service that automatically validates the JWT token when a request received to the server. After using this code snippet, you have to use [Authorize] attribute above [ApiController] in controller class file(s) to force program to authenticate before giving access to the action methods in that particular controller class.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {
options.Authority = "https://securetoken.google.com/<PROJECT ID>";
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuer = "https://securetoken.google.com/<PROJECT ID>",
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = "<PROJECT ID>",
ValidateLifetime = true
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
}
Edit> How you should send the Token to the server.
As you see, you have to send a Post/Get request with Authorization header. The value should be in Bearer format. Please check firebase documentation to find out how to extract the token id from an authenticated user.
https://firebase.google.com/docs/reference/node/firebase.auth.Auth#signinwithemailandpassword
https://firebase.google.com/docs/reference/node/firebase.auth#usercredential
https://firebase.google.com/docs/reference/node/firebase.User#getidtoken
If you're using another Newtonsoft.JSON library like me and don't want to import the Microsoft one, try this one: https://gist.github.com/saltyJeff/41029c9facf3ba6159ac019c1a85711a
Use Verify(string token) to asynchronously verify that a token is valid: returns the unique identifier of the user if valid, and null if invalid.
Firebase has a real lack of support for c sharpers. I've created a 3rd party token verification library for the C# community. https://github.com/gtaylor44/FirebaseAuth
Most of the code is based on João Angelo's answer. I've added caching of the web request using the Cache-Control["max-age"] property in response header as documented by Firebase for better performance.

Getting an access token in ASP.NET 5

My ASP.NET 5 (MVC 6 + beta7) web application (MVC + WebAPI) is required to get back an access_token from WebAPI login calls.
So far, from googling, I have created the following code for startup.cs:
app.UseOAuthBearerAuthentication(options => {
options.AutomaticAuthentication = true;
options.Audience = "http://localhost:62100/";
options.Authority = "http://localhost:62100/";
});
My client side is:
var login = function ()
{
var url = "http://localhost:62100/";
var data = $("#userData").serialize();
data = data + "&grant_type=password";
$.post(url, data)
.success(saveAccessToken)
.always(showResponse);
return false;
};
Is it required to use UseOpenIdConnectServer? If so, how do I use SigningCredentials so that I get a token (e.g. MVC5 ApplicationOAuthProvider)?
Please note that my site is simple demo HTTP site and I do not need any SSL.
Is it required to use UseOpenIdConnectServer?
Using AspNet.Security.OpenIdConnect.Server is not "required". You're - of course - free to opt for another server (like IdentityServer) or for a custom solution.
Being the main developer behind aspnet-contrib, I'm not really objective, so I'll necessarily suggest going with app.UseOpenIdConnectServer().
If so, how do I use SigningCredentials so that I get a token (e.g. MVC5 ApplicationOAuthProvider)?
When implementing the password and using the default token type, registering a signing key/certificate is not mandatory.
Here's how you can get started:
ASP.NET Core 1.x:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
}
public void Configure(IApplicationBuilder app)
{
// Add a new middleware validating the encrypted
// access tokens issued by the OIDC server.
app.UseOAuthValidation();
// Add a new middleware issuing tokens.
app.UseOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Override OnValidateTokenRequest to skip client authentication.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private),
// call Skip() to inform the server the request should be
// accepted without enforcing client authentication.
context.Skip();
return Task.FromResult(0);
};
// Override OnHandleTokenRequest to support
// grant_type=password token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Do your credentials validation here.
// Note: you can call Reject() with a message
// to indicate that authentication failed.
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");
// By default, claims are not serialized
// in the access and identity tokens.
// Use the overload taking a "destinations"
// parameter to make sure your claims
// are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
};
});
}
}
.csproj
<ItemGroup>
<PackageReference Include="AspNet.Security.OpenIdConnect.Server" Version="1.0.2" />
</ItemGroup>
ASP.NET Core 2.x:
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
// Add a new middleware validating the encrypted
// access tokens issued by the OIDC server.
.AddOAuthValidation()
// Add a new middleware issuing tokens.
.AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/connect/token";
// Override OnValidateTokenRequest to skip client authentication.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject the token requests that don't use
// grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private),
// call Skip() to inform the server the request should be
// accepted without enforcing client authentication.
context.Skip();
return Task.CompletedTask;
};
// Override OnHandleTokenRequest to support
// grant_type=password token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let the
// OpenID Connect server middleware handle the other grant types.
if (context.Request.IsPasswordGrantType())
{
// Do your credentials validation here.
// Note: you can call Reject() with a message
// to indicate that authentication failed.
var identity = new ClaimsIdentity(context.Scheme.Name);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");
// By default, claims are not serialized
// in the access and identity tokens.
// Use the overload taking a "destinations"
// parameter to make sure your claims
// are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.CompletedTask;
};
});
}
}
.csproj
<ItemGroup>
<PackageReference Include="AspNet.Security.OpenIdConnect.Server" Version="2.0.0-*" />
</ItemGroup>
You can also read this blog post, that explains how to implement the resource owner password grant: http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-resource-owner-password-credentials-grant/

Categories

Resources