Client Certificate Mapping Authentication in self-hosted Owin endpoint - c#

Is it possible (and if so, how?) to configure a self hosted owin endpoint to use client certificate mapping authentication with A/D? IIS has this feature link, but so far I have not found an equivalent for self-hosted endpoints.
The way in which I have gotten this to work though (bearing in mind this approach is probably not 100% foolproof), is a 2-step process by using a combination of authenticationSchemeSelectorDelegate and OWIN.
This will choose the appropriate AuthenticationScheme (allowing requests containing a cert through, otherwise deferring to NTLM authentication)
public void Configuration(IAppBuilder appBuilder)
{
var listener = (HttpListener)appBuilder.Properties[typeof(HttpListener).FullName];
listener.AuthenticationSchemeSelectorDelegate += AuthenticationSchemeSelectorDelegate;
}
private AuthenticationSchemes AuthenticationSchemeSelectorDelegate(HttpListenerRequest httpRequest)
{
if (!httpRequest.IsSecureConnection) return AuthenticationSchemes.Ntlm;
var clientCert = httpRequest.GetClientCertificate();
if (clientCert == null) return AuthenticationSchemes.Ntlm;
else return AuthenticationSchemes.Anonymous;
}
This will read the contents of the cert and populate the "server.User" environment variable accordingly
public class CertificateAuthenticator
{
readonly Func<IDictionary<string, object>, Task> _appFunc;
public CertificateAuthenticator(Func<IDictionary<string, object>, Task> appFunc)
{
_appFunc = appFunc;
}
public Task Invoke(IDictionary<string, object> environment)
{
// Are we authenticated already (NTLM)
var user = environment["server.User"] as IPrincipal;
if (user != null && user.Identity.IsAuthenticated) return _appFunc.Invoke(environment);
var context = environment["System.Net.HttpListenerContext"] as HttpListenerContext;
if (context == null) return _appFunc.Invoke(environment);
var clientCertificate = context.Request.GetClientCertificate();
// Parse out username from certificate
var identity = new GenericPrincipal
(
new GenericIdentity(username), new string[0]
);
environment["server.User"] = identity;
}
}
Is there not a better/standardized way?

I have not seen any standard components built for this yet. That said, it should be possible to clean up your code a little:
You don't need to downcast to HttpListenerContext to get the client cert. The client cert should already be available in the OWIN environment under "ssl.ClientCertificate". See https://katanaproject.codeplex.com/wikipage?title=OWIN%20Keys. You'll also want to check ssl.ClientCertificateErrors because the cert may not have passed all validation checks.
You don't need the AuthenticationSchemeSelectorDelegate code. You could just set the listner.AuthenticationSchemes = NTLM | Anonymous. Then you add a middleware after your cert middleware that returns a 401 if server.User is not valid.

Related

How to issue access token based on Windows Authentication with Identity Server 4

My goal is to protect a Web API, such that it can only be accessed by a client using an access token issued by IS based on Windows authentication. I worked through this basic sample:
http://docs.identityserver.io/en/release/quickstarts/1_client_credentials.html
Now, I need to extend the basic sample such that the access token returned to the client is issued based on Windows authentication. More specifically, I need to have the user (which is executing the client application) to be authenticated against Active Directory when requesting an access token. How should this be done?
I have already been running the quick start (https://github.com/IdentityServer/IdentityServer4.Templates) successfully, where the login is based on a Windows external provider, but I cannot figure out how to adopt this functionality to my strategy.
I tried using an Extension Grant (http://docs.identityserver.io/en/release/topics/extension_grants.html) and have the ValidateAsync() method be the one to do the authentication against AD, but could not make it work (primarily since HttpContext is not available). Is this even the correct approach?
Update
In this system, the client is a console application (without human interaction), thus the context is the account running the application.
I have been running the QuickstartUI and see how the AccountController logic handles the "Windows" button, but cannot grasp how to combine this with requesting access tokens. My client code goes like this:
static async Task Main(string[] args)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:50010");
var tokenClient = new TokenClient(disco.TokenEndpoint);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("CustomWindows"); // Not sure about this
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:50011/api/identity");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
Console.ReadLine();
}
I am not sure how to use the TokenClient to get an access token in this case. I would prefer not to store and use passwords, but have IS issue access tokens based on authenciating the client context against AD. If implicit or hybrid flows must be used in this case, how must that be done?
I had the same requirement and implemented it using an extension grant.
This is the code of the extension grant:
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => WinAuthConstants.GrantType;
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
And this is the client code:
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler, AuthenticationStyle.PostValues);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("windows_auth", "api1");

Get user Id from reference token in API

My setup,
An IdentityServer using MVC Identity to store the Users, created with dotnet new mvc -au Individual and applying the http://docs.identityserver.io/en/release/quickstarts/0_overview.html tutorial, running in localhost 5000.
A client App, but now I'm using postman to do tests.
A WEB API, created with dotnet new webapi, running in localhost 5001.
The IdentityServer resources and clients configuration is the following, notice that I'm using reference tokens:
public static IEnumerable<IdentityResource> GetIdentityResources() {
return new List<IdentityResource>{ new IdentityResources.OpenId() };
}
public static IEnumerable<ApiResource> GetApiResources() {
return new List<ApiResource>{
new ApiResource("api_resource", "API Resource") {
Description= "API Resource Access",
ApiSecrets= new List<Secret> { new Secret("apiSecret".Sha256()) },
}
};
}
public static IEnumerable<Client> GetClients() {
return new List<Client>{
new Client {
ClientId= "angular-client",
ClientSecrets= { new Secret("secret".Sha256()) },
AllowedGrantTypes= GrantTypes.ResourceOwnerPassword,
AllowOfflineAccess= true,
AccessTokenType = AccessTokenType.Reference,
AlwaysIncludeUserClaimsInIdToken= true,
AllowedScopes= { "api_resource" }
}
}
The password and user is send with postman and the token received is send to the WEB API also with postman, something like call localhost:5001/v1/test with the token pasted in option bearer token.
In the API Startup, in ConfigureServices I'm adding the lines below
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority= "http://localhost:5000";
options.ApiName= "api_resource";
options.ApiSecret = "apiSecret";
});
And I'm getting the Id of the user inside the controller as follows:
public async Task<IActionResult> Get(int id) {
var discoveryClient = new DiscoveryClient("http://localhost:5000");
var doc = await discoveryClient.GetAsync();
var introspectionClient = new IntrospectionClient(
doc.IntrospectionEndpoint,
"api_resource",
"apiSecret");
var token= await HttpContext.GetTokenAsync("access_token");
var response = await introspectionClient.SendAsync(
new IntrospectionRequest { Token = token });
var userId = response.Claims.Single(c => c.Type == "sub").Value;
}
The question itself is, am I using the right path to get the Id from the reference token?, because now It works but I don't want to miss anything, specially thinking that is a security concern.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference tokens.
Thanks in advance.
Inside a controller action that is protected with an [Authorize] attribute you can simply get claims directly from the ClaimsPrinciple, without having to go through a manual discovery client. The claims principle is handily aliased simply with User inside your controllers.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type ==
ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference
tokens.
It works just fine with reference tokens. You should have no problems accessing the sub claim.
EDIT:
As I mentioned in a comment below, I tend to use the standard JwtClaimTypes and create some extension methods on the ClaimsPrinciple, such as:
public static string GetSub(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Subject))?.Value;
}
or
public static string GetEmail(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Email))?.Value;
}
... so that within my protected actions I can simply use User.GetEmail() to get hold of claim values.
It's worth stating the obvious, that any method for retrieving claim values will only work if the claims actually exist. i.e. asking for the ZoneInfo claim will not work unless that claim was requested as part of the token request in the first place.

secure webapi with a valid JWT based on claims

Here is how I've created an claim based authorization attribute. But I have some doubts about how this work.
Given the code from my startup class:
public void Configuration(IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["Authentication:Authority"],
RequiredScopes = ConfigurationManager.AppSettings["Authentication:Scopes"].Split(' ').ToList(),
PreserveAccessToken = true
});
}
I was expecting that if I have this attribute to my controller and I send an invalid token(invalid signature) the request will be automatically rejected as unauthorized, but the code from the attribute is executed.
Shouldn't OWIN validate the token first?
How to make sure that the token is valid (valid stricture, signature, not expired, etc) and only after validate the claims?
The issue is within your linked question in your ClaimAuthorizationAttribute - it doesn't ever call base.IsAuthorized(), thus bypassing the built in protection mechanisms offered by AuthorizeAttribute.
Instead of just returning here after seeing whether or not the claim is present:
return token.Claims.Any(c => c.Type.Equals(this.Claim) && c.Value.Equals("True", StringComparison.OrdinalIgnoreCase));
You should instead carry on with making sure that the base class is satisfied, and thus the token itself is valid, too:
var claimValid = token.Claims.Any(c => c.Type.Equals(this.Claim) && c.Value.Equals("True", StringComparison.OrdinalIgnoreCase));
if (claimValid)
return base.IsAuthorized();
else
return false;

IdentityServer3 - rejected because invalid CORS path

We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}

Reconnecting to Servicestack session in an asp.net MVC4 application

I have an asp.net mvc4 web application that is consuming data data from an API written in C# and hosted on a Linux machine w/ Apache / mod_mono
The client application is written in C#/asp.net - It runs on a different web server, also Linux / Apache / mod_mono. I'm not sure if those details are important in this case, but I figured any background may help.
The question leading up to this one: AppHostBase instance not set - Helped me gain quite a bit more understanding of how this all fits together.
I believe the proper question I should be asking now is: Once I create a session in servicestack (On the API server), how do I properly reconnect to it?
Following the answers in previous questions, I've used this bit of code in my auth controller on the client application:
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var AuthResponse = authService.Authenticate(new Auth
{
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
This returns a ResolutionException:
Required dependency of type ServiceStack.ServiceInterface.Auth.AuthService could not be resolved.
Is there something simple I might be missing when it comes to getting the client to work from within an asp.net application?
I apologize if the question is too vague and will happily provide any more information.
Update:
This is AuthController - Excuse the mess, I've been trying a few things since my last post:
{
public partial class AuthController : BaseController
{
JsonServiceClient client = new ServiceStack.ServiceClient.Web.JsonServiceClient("<TheAPIurl>");
// GET: /Login/
public ActionResult login()
{
if (Session["IsAuthenticated"] != null)
{
ViewData["Result"] = Session["IsAuthenticated"];
}
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult login(UserModel user)
{
try
{
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var AuthResponse = authService.Authenticate(new Auth
{
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
if (AuthResponse.SessionId != null)
{
Session["IsAuthenticated"] = true;
Session["UserID"] = AuthResponse.UserName;
Session["jsclient"] = client;
FormsAuthentication.SetAuthCookie(user.user_id, true);
return Redirect("/default");
}
else
{
Session["IsAuthenticated"] = false;
}
}
catch (Exception ex)
{
Session["IsAuthenticated"] = false;
}
return View();
}
protected override void ExecuteCore()
{
throw new NotImplementedException();
}
}
}
Authenticating with a local ServiceStack instance
You can only retrieve an auto-wired ServiceStack service (or other IOC dependency) out from a ServiceStack Container if the ServiceStack instance is hosted within the same App Domain as MVC, i.e:
var authService = AppHostBase.Resolve<AuthService>();
authService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
Although the recommended code for resolving the auto-wired implementation of another service is:
using (var authAservice = AppHostBase.ResolveService<AuthService>()) {
...
}
i.e. As services may make use of resources that should be disposed. Inside a ServiceStack service you should use base.ResolveService<AuthService>() instead.
So if ServiceStack hosted within the same AppDomain as MVC, you can call the Service directory, like this:
var authResponse = authService.Authenticate(new Auth {
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
Authenticating with a Remote ServiceStack instance
Otherwise if it's remote you need to use one of ServiceStack's C# Service Clients, e.g:
var client = new JsonServiceClient(ServiceStackBaseUrl);
var authResponse = client.Post(new Auth {
provider = "credentials",
UserName = user.user_id,
Password = user.password,
RememberMe = true
});
Attaching ServiceStack SessionId back to originating MVC request
This will setup an authenticated session with that ServiceClient client, by attaching it to the ss-pid Cookie (see Session docs for more info). You can pass through this cookie to the originating browser that called MVC with:
var response = HttpContext.Current.Response.ToResponse();
response.Cookies.AddSessionCookie(
SessionFeature.PermanentSessionId, authResponse.SessionId);
Subsequent requests with the authenticated session
To re-attach with the remote authenticated ServiceStack Session from within MVC you would then need to pass the cookie back into the Service Client, e.g:
var cookie = HttpContext.Request.Cookies.Get(SessionFeature.PermanentSessionId);
var client = new JsonServiceClient(ServiceStackBaseUrl);
var cookie = new Cookie(SessionFeature.PermanentSessionId, cookie.Value);
client.CookieContainer.Add(cookie);
You can set the cookie domain, globally in the Web.Config:
<httpCookies domain="mydomain.com" />
Or at runtime with:
cookie.Domain = "mydomain.com";
The ServiceStack AuthTests.cs Integration Tests has some other useful examples showing how Authentication works in ServiceStack.

Categories