ServiceStack and Auth0 - servicestack

I am looking to use Auth0 as the authentication provider for ServiceStack. There is a great sample application documented at Auth0 which applies & works well when working with ServiceStack and using ServiceStack.Host.MVC: https://auth0.com/docs/quickstart/webapp/servicestack/01-login.
However, I am at a loss how to construct the authorization URL and redirect the user to that URL in a scenario where I am NOT using MVC & the AccountController to redirect the user. How can I construct the redirect URLs using ServiceStack Auth Plugin, if I want to replicate the logic as per MVC sample code below:
public class AccountController : Controller
{
public ActionResult Login()
{
string clientId = WebConfigurationManager.AppSettings["oauth.auth0.AppId"];
string domain = WebConfigurationManager.AppSettings["oauth.auth0.OAuthServerUrl"].Substring(8);
var redirectUri = new UriBuilder(this.Request.Url.Scheme, this.Request.Url.Host, this.Request.Url.IsDefaultPort ? -1 : this.Request.Url.Port, "api/auth/auth0");
var client = new AuthenticationApiClient(new Uri($"https://{domain}"));
var authorizeUrlBuilder = client.BuildAuthorizationUrl()
.WithClient(clientId)
.WithRedirectUrl(redirectUri.ToString())
.WithResponseType(AuthorizationResponseType.Code)
.WithScope("openid profile")
.WithAudience($"https://{domain}/userinfo");
return Redirect(authorizeUrlBuilder.Build().ToString());
}
}

For all who are interested,here is the solution I ended up adopting.
Steps:
1) Create an Auth0 plugin (see gist here)
2) Register the Plugin in your AppHost.
Plugins.Add(new AuthFeature(() => new Auth0UserSession(), new IAuthProvider[] {
new Auth0Provider(appSettings,appSettings.GetString("oauth.auth0.OAuthServerUrl"))
}));
3) Add the relevant keys in your Web.Config.
<appSettings>
<add key="oauth.auth0.OAuthServerUrl" value="https://xxxxxxx.auth0.com" />
<add key="oauth.auth0.AppId" value="xxxxxx" />
<add key="oauth.auth0.AppSecret" value="xxxxxxx" />
</appSettings>

Related

ASP.NET Core 3.1 app running on Azure App Service throwing EPIPE errors for 1.6 MB json payload

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
};
}
}

Different authentication schema (Windows, Bearer) for each route

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!

Response headers not setting on OwinContext

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>

When the user is authenticate after few seconds identity redirect to login page but the session id remains in browser storage

Here the identity class for authentication. it works fine on my localhost but it's behaving strange after deployment on server.I used OWIN for authentication, it works fine for first login, but after few seconds if I refresh the page, it redirects me back to the login page.
public class IdentityConfig
{
public void Configuration(IAppBuilder app)
{
app.CreatePerOwinContext<AppDBContext>(AppDBContext.Create);
app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<AppUserManager, AppUser>(
validateInterval: TimeSpan.FromMinutes(15),
regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie))
},
CookieName = "MyCookie",
//CookieDomain = "www.example.com",
//CookieHttpOnly = true,
//CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(double.Parse(ConfigurationManager.AppSettings["app:SessionTimeout"])),
SlidingExpiration = true
});
}
}
Here the web.config code of search4best used for session timeout
<add key="owin:AppStartup" value="Search4Best.App_Start.IdentityConfig" />
<add key="app:SessionTimeout" value="15"/>
on web.config try this
<machineKeyvalidationKey="F18753F2CF84EFFFB10600B1E29D9849A74F080A1E1170BF728D8381979271EF6894673001C877FD8A349F8D953024019AF6C4C5090309B4569C1933ECC90D94"
decryptionKey="504430FBB7D426A3C401600906CD5C121DC0808B0D40328E02EAF7A59652157B"
validation="SHA1" decryption="AES"/>

License error attempting to implement AppHostHttpListenerBase

I'm attempting to create a second App host for self-hosting, so my unit tests are in the same process as the service, to aid debugging. I created the new app host as follows. When my Unit test calls the .Init() method, I receive the following error:
ServiceStack.LicenseException was unhandled by user code
HResult=-2146233088
Message=The free-quota limit on '10 ServiceStack Operations' has been reached. Please see https://servicestack.net to upgrade to a commercial license or visit https://github.com/ServiceStackV3/ServiceStackV3 to revert back to the free ServiceStack v3.
Source=ServiceStack.Text
The class below is in the same assembly as my real AppHost (my main ASP.NET service project)., so there is definitely a license key in the web.config. file.
public class ServiceTestAppHost : AppSelfHostBase
{
public const string BaseUrl = "http://localhost/dvsvc";
public ServiceTestAppHost()
: base("Test Web Services", typeof(DV.Svc.Interface.HelloService).Assembly) { }
public override void Configure(Funq.Container container)
{
ServiceStack.Text.JsConfig.IncludeNullValues = true;
ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601;
ServiceStack.Text.JsConfig.ExcludeTypeInfo = true; //exclude the type specification
ServiceStack.Formats.HtmlFormat.Humanize = false;
//most apps use credentials auth. the TVTI player uses Basic auth
Plugins.Add(new AuthFeature(() =>
new DVAuthUserSession(),
new ServiceStack.Auth.IAuthProvider[] { new DVCredentialsAuthProvider(), new DVBasicAuthProvider() })
/*{ HtmlRedirect = null }*/
);
//in memory cache
container.RegisterAs<MemoryCacheClient, ICacheClient>();
SetConfig(new HostConfig { DebugMode = true });
}
}
Self hosted applications don't read from Web.config, they read from the app config App.config, so you would have to create an appropriate config file for the host executable.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<add key="servicestack:license" value="{licenseKeyText}" />
</configuration>

Resources