I am setting up facebook authentication with servicestack and have been getting the return type #f=Unknown, I've tracked it down to coming from the authentication block:
try
{
var contents = accessTokenUrl.DownloadUrl();
var authInfo = HttpUtility.ParseQueryString(contents);
tokens.AccessTokenSecret = authInfo["access_token"];
session.IsAuthenticated = true;
authService.SaveSession(session, SessionExpiry);
OnAuthenticated(authService, session, tokens, authInfo.ToDictionary());
//Haz access!
return authService.Redirect(session.ReferrerUrl.AddHashParam("s", "1"));
}
catch (WebException we)
{
var statusCode = ((HttpWebResponse)we.Response).StatusCode;
if (statusCode == HttpStatusCode.BadRequest)
{
return authService.Redirect(session.ReferrerUrl.AddHashParam("f", "AccessTokenFailed"));
}
}
//Shouldn't get here
return authService.Redirect(session.ReferrerUrl.AddHashParam("f", "Unknown"));
The reason for it dropping through is the catch checks the response status code. In my scenario I am receiving 407 Proxy Authentication Required.
I've tracked it down further to the line:
var contents = accessTokenUrl.DownloadUrl();
Can anybody help with how I put in place the required proxy authentication?
For info, my app is running in a windows environment, it is run as an windows authenticated user so has permission access the proxy server, I just need to tell the code to use this - or any - credentials.
Thanks in anticipation
You can try setting the default proxy for .NET applications by setting the <defaultProxy/> in your Web.Config:
<configuration>
<system.net>
<defaultProxy>
<proxy
usesystemdefaults="true"
proxyaddress="http://192.168.1.10:3128"
bypassonlocal="true"
/>
<bypasslist
<add address="[a-z]+\.contoso\.com" />
</bypasslist>
</defaultProxy>
</system.net>
</configuration>
Related
I have a simple ASP.NET Core 3.1 app deployed on an Azure App Service, configured with a .NET Core 3.1 runtime. One of my endpoints are expected to receive a simple JSON payload with a single "data" property, which is a base64 encoded string of a file. It can be quite long, I'm running into the following issue when a the JSON payload is 1.6 MBs.
On my local workstation, when I call my API from Postman, everything's working as expected, my breakpoint in the Controller's action method is reached, the data is populated, all good - it's only when I deploy (via Azure DevOps CICD Pipelines) the app to the Azure App Service. Whenever trying to call the deployed API from Postman, no HTTP response is received, but this: "Error: write EPIPE".
I've tried modifying the web.config to include both a maxRequestLength and maxAllowedContentLength properties:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.web>
<httpRuntime maxRequestLength="204800" ></httpRuntime>
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="419430400" />
</requestFiltering>
</security>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\MyApp.API.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>
In the app's code, I've added to the Startup.cs:
services.Configure<IISServerOptions>(options => {
options.MaxRequestBodySize = int.MaxValue;
});
In the Program.cs, I've added:
.UseKestrel(options => { options.Limits.MaxRequestBodySize = int.MaxValue; })
In the controller, I've tried both of these attributes: [DisableRequestSizeLimit], [RequestSizeLimit(40000000)]
However, nothing's working so far - I'm pretty sure it has to be something configured on the App Service itself, not in my code, as locally everything's working. Yet, nothing so far helped in the web.config
It was related to the fact that in my App Service, I had to allow incoming client certificates, in the Configuration - turns out client certificates and large payloads don't mix well in IIS (apparently for more than a decade now): https://learn.microsoft.com/en-us/archive/blogs/waws/posting-a-large-file-can-fail-if-you-enable-client-certificates
None of the proposed workarounds in the above blog post fixed my issue, so I had to workaround: I've created an Azure Function (still using .NET Core 3.1 as a runtime stack) with a Consumption Plan, which is able to receive both the large payload and the incoming client certificate (I guess it doesn't use IIS under the hood?).
In my original backend, I added the original API's route to the App Service's "Certificate exclusion paths", to not get stuck waiting and timing out eventually with "Error: write EPIPE".
I've used Managed Identity to authenticate between my App Service and the new Azure Function (through a System Assigned identity in the Function).
The Azure Function takes the received certificate, and adds it to a new "certificate" property in the JSON body, next to the original "data" property, so my custom SSL validation can stay on the App Service, but the certificate is not being taken from the X-ARR-ClientCert header, but from the received payload's "certificate" property.
The Function:
#r "Newtonsoft.Json"
using System.Net;
using System.IO;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
private static HttpClient httpClient = new HttpClient();
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
var requestBody = string.Empty;
using (var streamReader = new StreamReader(req.Body))
{
requestBody = await streamReader.ReadToEndAsync();
}
dynamic deserializedPayload = JsonConvert.DeserializeObject(requestBody);
var data = deserializedPayload?.data;
var originalUrl = $"https://original-backend.azurewebsites.net/api/inbound";
var certificateString = string.Empty;
StringValues cert;
if (req.Headers.TryGetValue("X-ARR-ClientCert", out cert))
{
certificateString = cert;
}
var newPayload = new {
data = data,
certificate = certificateString
};
var response = await httpClient.PostAsync(
originalUrl,
new StringContent(JsonConvert.SerializeObject(newPayload), Encoding.UTF8, "application/json"));
var responseContent = await response.Content.ReadAsStringAsync();
try
{
response.EnsureSuccessStatusCode();
return new OkObjectResult(new { message = "Forwarded request to the original backend" });
}
catch (Exception e)
{
return new ObjectResult(new { response = responseContent, exception = JsonConvert.SerializeObject(e)})
{
StatusCode = 500
};
}
}
I need to add single-sign-on using Windows Authentication to my intranet Angular web application (hosted on IIS) which uses a JWT Bearer token for authentication. The controllers are secured using the [Authorize] attribute and JWT Bearer token authentication is working. All of the controllers are exposed under the api/ route.
The idea is to publish a new SsoController under the sso/ route, which should be secured with Windows Authentication and that exposes a WindowsLogin action that returns a valid bearer token for the application.
Back when I was using ASP.net Web Forms it was quite easy, you only had to enable Windows Authentication in the web.config/system.webServer section, disable it application-wide in the system.web section and then enable it again under a <location path="sso"> tag. This way ASP.net generated the NTLM/Negotiate challenges only for requests under the sso route.
I got it almost working - the SsoController gets the Windows user name and creates the JWT token just fine, but the pipeline is still generating the WWW-Authenticate: NTLM and WWW-Authenticate: Negotiate headers for all HTTP 401 responses, not just for the ones under the sso route.
How can I tell the pipeline that I want only Anonymous or Bearer auth for all of the api/ requests?
Thanks in advance for your help.
Program.cs
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseIISIntegration();
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Set up data directory
services.AddDbContext<AuthContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("AuthContext")));
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "AngularWebApp.Web",
ValidAudience = "AngularWebApp.Web.Client",
IssuerSigningKey = _signingKey,
ClockSkew = TimeSpan.Zero //the default for this setting is 5 minutes
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/sso"),
builder => builder.UseMiddleware<WindowsAuthMiddleware>());
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
WindowsAuthMiddleware.cs
public class WindowsAuthMiddleware
{
private readonly RequestDelegate next;
public WindowsAuthMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
return;
}
await next(context);
}
}
web.config
<system.webServer>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/>
<security>
<authentication>
<anonymousAuthentication enabled="true" />
<windowsAuthentication enabled="true" />
</authentication>
</security>
</system.webServer>
So, I spent the last few days investigating this problem and I got a working - if a bit hacky - solution.
It turns out that the main problem is that IIS will handle the Windows Authentication negotiation for all 401 responses sent by the application. It's something that's done at a lower level as soon as you enable Windows Authentication in IIS (or in the system.webServer section), and I haven't been able to find a way to bypass this behaviour. I actually did a test with a classic Web Form app and it works the same - the reason I never noticed this is that classic Forms Authentication rarely generates 401 responses, rather it uses redirects (30x) to take the user to the login page.
This gave me an idea: I could add another middleware to the pipeline that rewrites 401 responses generated by the authorization infrastructure to another, rarely used HTTP code, and detect that in my client Angular app to make it behave as a 401 (by refreshing an access token, or denying router navigation, etc). I used HTTP error 418 "I'm a teapot" since it's an existing but unused code. Here is the code:
ReplaceHttp401StatusCodeMiddleware.cs
public class ReplaceHttp401StatusCodeMiddleware
{
private readonly RequestDelegate next;
public ReplaceHttp401StatusCodeMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
await next(context);
if (context.Response.StatusCode == 401)
{
// Replace all 401 responses, except the ones under the /sso paths
// which will let IIS trigger the Windows Authentication mechanisms
if (!context.Request.Path.StartsWithSegments("/sso"))
{
context.Response.StatusCode = 418;
context.Response.Headers["X-Original-HTTP-Status-Code"] = "401";
}
}
}
}
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
// Enable the SSO login using Windows Authentication
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/sso"),
builder => builder.UseMiddleware<WindowsAuthMiddleware>());
app.UseMiddleware<ReplaceHttp401StatusCodeMiddleware>();
...
}
The middleware also injects the original status code in the response for further reference.
I also applied to my code the suggestion from Mickaƫl Derriey to use Authorization policies because it makes the controllers cleaner, but it's not necessary for the solution to work.
Welcome to StackOverflow! That's an interesting quesiton you have here.
First, let me state that I didn't test any of the content in this answer.
Using authorization policies to drive sources of authentication
I like the idea behind the WindowsAuthMiddleware you created, and how it's conditionally inserted in the pipeline if the URL starts with /sso.
MVC integrated with the authorization system and provides the same capabilities with authorization policies. The result is the same, and prevents you from having to write low-level code.
You can define authorization policies in the ConfigureServices method. In your case, if I'm not mistaken, there are two policies:
all requests to /sso should be authenticated with Windows authenticated; and
all other requests should be authenticated with JWTs
services.AddAuthorization(options =>
{
options.AddPolicy("Windows", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(IISDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
});
You can then reference those policies by name in the [Authorize] attributes used to decorate your controllers and/or actions.
[Authorize("Windows")]
public class SsoController : Controller
{
// Actions
}
[Authorize("JWT")]
public class ApiController : Controller
{
// Actions
}
Doing so means that the Windows authentication handler will not run against /api requests, hence the responses should not contain the WWW-Authenticate: NTLM and WWW-Authenticate: Negotiate headers.
Removing automatic authentication of all requests
When you pass an authentication scheme as an argument of AddAuthentication, this means the authentication middleware will try to authenticate every request against that scheme.
This is useful when you have one authentication scheme, but in this case, you could think about removing it, as even for requests to /sso, the JWT handler will analyze the request for a token.
Two calls to AddAuthentication
You should only have one call to AddAuthentication:
the first one sets the IIS authentication scheme as a default so the handler should run on every request;
the second call overwrites that setting and set the JWT scheme as the default one
Let me know how you go!
I have created a web api that uses the JWT system using this article here. When calling the API from a REST client it works just fine. However when trying to access it from a browser it gives a CORS error since it doesn't send out the correct response headers.
Startup.cs
app.UseCors(CorsOptions.AllowAll);
Note that on my controllers CORS works just fine, it just breaks for the OAuthAuthorizationServer.
CustomOAuthProvider.cs
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
var user = Database.Users.FirstOrDefault(u => u.Email == context.UserName);
if (user == null || !BCrypt.Net.BCrypt.Verify(context.Password, user.Password))
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return Task.FromResult<object>(null);
}
var companyId = int.Parse(context.OwinContext.Get<string>("company_id"));
var company = user.Companies.FirstOrDefault(c => c.Id == companyId);
if (company == null)
{
context.SetError("invalid_grant", "You don't belong to that company!");
return Task.FromResult<object>(null);
}
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim("uue", user.Email));
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{ "audience", company.ServerUrl }
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
return Task.FromResult<object>(null);
}
However after making the call to obtain the token, I only get back these response headers.
Content-Length:1245
Content-Type:text/html
Date:Wed, 20 Apr 2016 20:34:40 GMT
Server:Microsoft-IIS/8.5
X-Powered-By:ASP.NET
Is there something I'm doing wrong?
Note: I'm assuming you are using the same Startup.cs code defined in the liked tutorial.
Try to move the call to app.UseCors(CorsOptions.AllowAll); at the top of your Configuration method in Startup.cs:
public void Configuration(IAppBuilder app)
{
app.UseCors(CorsOptions.AllowAll);
HttpConfiguration config = new HttpConfiguration();
// Web API routes
config.MapHttpAttributeRoutes();
ConfigureOAuth(app);
app.UseWebApi(config);
}
In Owin every middleware in the pipeline is executed only if the preceding passes through the invocation. For this reason app.UseCors is executed only after the AuthenticationMiddleware (in your case OAuthAuthorizationServer) and only if it does not stop the flow in the pipeline (e.g. OAuth returns a response).
Moving the Cors middleware declaration before other middlewares ensures you that it is executed for each request.
Make sure you allow CORS in web config
<httpProtocol>
<customHeaders>
<clear />
<add name="Access-Control-Allow-Methods" value="GET,PUT,POST,OPTIONS,DEBUG" />
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="authorization,content-type" />
</customHeaders>
</httpProtocol>
I have a Service Stack service hosted within a SharePoint 2013 site. When attempting to make a cross domain request to one of the services, a preflight OPTIONS request is made, as expected.
The problem is that the response always comes back as 401 Unauthorized, due to the fact that authentication info is not sent across with the request. I have tried putting some request filters via servicestack to try and bypass the authentication, but these filters are not firing - it seems like something prior to service stack is sending the response.
Is there any way of specifying that OPTIONS requests to the sharepoint site do not need to be authenticated? If not, does anyone have a workaround for this scenario?
I tried 'fooling' the browser in to not sending a preflight request by changing the data type from application/json to text/plain in my ajax request, but then the data I send is not being deserialised in to the correct RequestDTO for the service calls on the server side.
Any help would be appreciated.
We ended up having to write our own HTTP module in order to support the options request. We basically add a key specifying which domains to allow the CORS requests from (can support more than one) and then have this HTTP module registered:
public class ECSPreFlightModule : IHttpModule
{
/// <summary>
/// You will need to configure this module in the Web.config file of your
/// web and register it with IIS before being able to use it. For more information
/// see the following link: http://go.microsoft.com/?linkid=8101007
/// </summary>
public void Dispose()
{
//clean-up code here.
}
private const string OptionsHeader = "OPTIONS";
private const string OriginHeader = "ORIGIN";
private const string AccessAllowOrigin = "Access-Control-Allow-Origin";
private string AllowedOriginUrlsArray
{
get
{
return GetWebConfigValue("CORSAllowedOriginUrls");
}
}
private string GetWebConfigValue(string key)
{
var configuration = WebConfigurationManager.OpenWebConfiguration("~");
object o = configuration.GetSection("system.web/httpModules");
var section = o as HttpModulesSection;
return section.CurrentConfiguration.AppSettings.Settings[key].Value;
}
public void Init(HttpApplication context)
{
context.PreSendRequestHeaders += (sender, e) =>
{
var splitUrls = AllowedOriginUrlsArray.Split('|');
var response = context.Response;
var originHeader = context.Request.Headers.Get(OriginHeader);
if (!String.IsNullOrEmpty(originHeader) && splitUrls.Length > 0)
{
foreach (var url in splitUrls)
{
var urlLower = url.ToLower();
var originHeaderLower = originHeader.ToLower();
// if the method being requested is an OPTIONS request and the url is the url specified in the web.config then return an OK response.
if (context.Request.HttpMethod.ToLowerInvariant() == OptionsHeader.ToLowerInvariant() &&
(urlLower == originHeaderLower))
{
response.StatusCode = (int)HttpStatusCode.OK;
}
// If the originating header url is equal to the url specified in the web.config then grant the access control
if (originHeaderLower == urlLower)
{
response.AddHeader(AccessAllowOrigin, originHeader);
break;
}
}
}
};
}
}
}
The above module was wrapped in a sharepoint feature that, when activated, made the appropriate changes to the web.config, namely registering the module and adding the following keys:
<add name='Access-Control-Allow-Credentials' value='true' />
<add name='Access-Control-Allow-Headers' value='Authorization, X-Requested-With, Content-Type, Origin, Accept, X-RequestDigest' />
<add name='Access-Control-Allow-Methods' value='GET,POST,OPTIONS,PUT, DELETE' />
i'm having a heck of a time trying to resolve an issue with authentication using HttpWebRequest.
So we have a SOA solutation that is being load balanced. Part of the solution is that all requests must be authenticated (using Windows Authentication). The other part of the solution is that the load balancer must have anonymous access to a keep alive page. So we've done the appropraite web.config sections as below
<location path="hello.aspx" allowOverride="false">
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
<system.web>
<authentication mode="Windows" />
<authorization>
<deny users="?" />
</authorization>
...
</system.web>
we've correctly setup an httpRequest as below
httpRequest.UseDefaultCredentials = true;
httpRequest.CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default);
so here's the problem. When only integrated authentication is enabled everything works great. However when both anonymous and integrated authentication are enabled (with the web.config defined above) we get an extra header coming back
Cache-Control: private
This is causing our client to barf. We can set the CachePolicy to NoCacheNoStore but that's not ideal because other requests can and should be cached. Setting the clientCache DisableCache has no effect.
Any ideas would be appreciated.
Never did find a solution but anyways, for those of you that are interested here's the workaround
public Foo ExecuteRequest(RequestCachePolicy cachePolicy, ...)
{
return ExecuteRequest(new RequestCachePolicy(RequestCacheLevel.Default), ...);
}
private Foo ExecuteRequest(RequestCachePolicy cachePolicy, ...)
{
...
try
{
...
// Make call using HttpWebRequest
...
}
catch (WebException ex)
{
var webResponse = ex.Response as HttpWebResponse;
if ((ex.Status == WebExceptionStatus.ProtocolError) &&
(null != webResponse) &&
(webResponse.StatusCode == HttpStatusCode.Unauthorized) &&
(cachePolicy.Level != RequestCacheLevel.NoCacheNoStore))
{
return ExecuteRequest(new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore), ...);
}
...
}
...
}