ASP.NET Core custom AuthenticationHandler in combination with Cookies - c#

In .NET Core 1 we could use
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AutomaticAuthenticate = true,
...
to ensure the cookie was evaluated first. On top of this we had either OAuth2 or a custom authentication module using the Basic authentication header.
This made the following flow trivial:
User reach website, gets a WWW-Authorize challenge (Bearer or Basic depending on settings)
User responds to the challenge (so for Basic, provide username/password)
As soon as the relevant middleware would validate the Bearer or Basic token on the next request, it would sign in the user - and as a result the cookie middleware would add its cookie to the respond.
On the next request the incoming cookie would be validated. If it is still valid, the user is signed in and as a result the following OAuth2 or custom middleware simply backs off and do not perform any action.
We are now trying to update that to ASP.NET Core 6 (or whatever it is called these days).
In the event handler of our custom AuthenticationHandler the following code:
context.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, context.Principal)
does result in the cookies being set.
With
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, "MyCustomScheme")
.Build();
});
it will first validate the cookie, but it will always run "MyCustomScheme" as well. In some environments validating a username/password can add 30 seconds due to timeouts in their way too complicated infrastructure setup, and I do not see a way for my custom handler to access any previous identities allowing it to shortcut (so no, "oh, already validated by cookie, so I do not need to do anything" unless I start seriously hacking).
I could also write a "ForwardDefaultSelector" (from https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0)
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
{
var token = authorization.Substring("Bearer ".Length).Trim();
var jwtHandler = new JwtSecurityTokenHandler();
return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals("B2C-Authority"))
? "B2C" : "AAD";
}
return "AAD";
};
but checking the cookie exists is of course not enough - it might be present but expired in which case setting the only authentication schema to be the cookie would result in a failed login - even if the correct token is being passed in as well.
Am I missing some obvious way to get the cookie to authenticate if able, but if not able let another handler have a go? It seems like a simple task: Try to authenticate with Cookie, if fail, try X. 2 lines of code if I could just find a place to put those lines.
And yes, I am aware we should skip this and only support OAuth2. Unfortunately the world moves slow.

Related

JWT authentication returns 401 when policy is set in ASP.NET Core

I am trying to add multiple ways to authenticate users in our app. All but one are working flawlessly. I am able to log in with ASP.NET Core Identity, GitHub, Azure AD, and even API auth, but JWT is giving me a bit of a headache as I always get a 401 response when I pass in an bearer token to the authorization header.
This might have something to do with a custom middleware class that works on this header:
app.UseMiddleware<JwtAuthMiddleware>()
.UseAuthentication()
.UseAuthorization()
The middleware class in question:
public class JwtAuthMiddleware
{
private readonly RequestDelegate _next;
public JwtAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null)
{
string jwtEncodedString = authHeader[7..]; // Fetch token without Bearer
JwtSecurityToken token = new(jwtEncodedString: jwtEncodedString);
ClaimsIdentity identity = new(token.Claims, "jwt");
context.User = new ClaimsPrincipal(identity);
}
return _next(context);
}
}
The identity setup is fairly simple.
services
.AddAuthentication()
.AddCookie(/*config*/)
.AddGitHub(/*config*/)
.AddJwtBearer(/*config*/)
.AddAzureAd(/*config*/);
string[] authSchemes = new string[]
{
IdentityConstants.ApplicationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
GitHubAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme,
"InHeader"
};
AuthorizationPolicy authnPolicy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
services.AddAuthorization(options =>
{
options.FallbackPolicy = authnPolicy;
});
I also set a filter in AddMvc:
.AddMvc(options =>
{
AuthorizationPolicy policy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
JWT works when options.FallbackPolicy is not set. Otherwise, I get a 401 response. Inspecting the DenyAnonymousAuthorizationRequirement in debug mode confirms this as there is no principal set with the right claims filled out. So it looks like context.User is ignored or reset.
Ideally, I would want to get rid of JwtAuthMiddleware altogether, but I still have to figure out how to combine JWT with cookie authentication in this particular setup. Any thoughts?
You don't need to use this custom JwtAuthMiddleware and define the custom ClaimsPrincipal like that. The problem is that you are just extracting the claims from the token. Setting the ClaimsPrincipal like that wouldn't automatically authenticate the user i.e. Context.User.Identity.IsAuthenticated would be false. Hence you get 401.
The token needs to be validated. You are already using AddJwtBearer which you can customize the code like below if you haven't done that.
JWT bearer authentication performs authentication automatically by extracting and validating a JWT token from the Authorization request header. However, you need to set the TokenValidationParameters which tells the handler what to validate.
services
.AddAuthentication()
.AddJwtBearer("Bearer", options =>
{
// you don't have to validate all of the parameters below
// but whatever you need to, but see the documentation for more details
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer, // presuming config is your appsettings
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
})
However, there is a caveat. When combining multiple authentication mechanisms, you need to utilise multiple authentication schemes. They play a role in defining the logic to select the correct authentication mechanism. That is because you can call AddJwtBearer or AddCookie multiple times for different scenarios. If the scheme name is not passed, it would try to utilize the default scheme and the authentication will fail, as it would have no way of knowing which mechanism to use JWT or AzureAd as they both use the Bearer token.
You mentioned that JWT works when FallbackPolicy is not set but you get 401 otherwise. That is because your authorization policy requires a user to be authenticated. The user was never authenticated as I mentioned in the beginning. If FallbackPolicy is not set it would work in the context of JWT being passed in the alright, but there is no requirement to check e.g user is authenticated and has a specific claim or role, so it works.
You would need to define an authentication policy scheme and set the ForwardDefaultSelector property of the PolicySchemeOptions. ForwardDefaultSecltor is used to select a default scheme for the current request that authentication handlers should forward all authentication operations to by default.
So basically you need to set the ForwardDefaultSecltor delegate which uses some logic to forward the request to the correct scheme to handle the authentication.
So the above code would change to:
// scheme names below can be any string you like
services
.AddAuthentication("MultiAuth") // virtual scheme that has logic to delegate
.AddGitHub(GitHubAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses GitHub scheme
.AddAzureAd("AzureAd", /*config*/) // uses AzureAd scheme
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses Cookies scheme
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { // above code for AddJwtBearer }) // uses Bearer scheme
// this is quite important to use the same virtual scheme name below
// which was used in the authentication call.
.AddPolicyScheme("MultiAuth", null, options =>
{
// runs on each request should return the exact scheme name defined above
options.ForwardDefaultSelector = context =>
{
// if the authorization header is present, use the Bearer scheme
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
{
return "Bearer";
}
// custom logic for GitHub
...
return GitHubAuthenticationDefaults.AuthenticationScheme;
// custom logic for AzureAd
...
return "AzureAd";
// otherwise fallback to Cookies or whichever is the default authentication scheme
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
That way, you just remove the JwtAuthMiddleware. AddJwtBearer would automatically add the claims in the Claims object when the token is validated, which you can then use to define custom authorization policies to authorize the user.
I hope that helps.
#Shazad Hassan made some good points in his answer. The custom middleware was in fact not necessary in the end because the System.IdentityModel.Tokens.XXXX assemblies were throwing errors that the tokens were invalid. I never really knew the token was invalid because the middleware assumed it was correct (and content-wise, it was), while all this time the context was running for an unauthenticated user, which became apparent when I applied the fallback policy.
So I rewrote the token generation code and now I no longer get this issue, and I can safely remove the custom middleware. So far so good.
While I still have to bone up on authentication schemes and all that, for the moment it doesn't even seem necessary to have multiple calls to AddAuthentication because this works:
services
.AddAuthentication()
.AddCookie(configureCookieAuthOptions)
.AddGitHub(configureGithub)
.AddAzureAd(config)
.AddJwtBearer(configureJwtBearerOptions)
.AddApiKeyInHeader<ApiKeyProvider>("InHeader", options =>
{
options.Realm = "My realm";
options.KeyName = "X-API-KEY";
});
With this, I am able to log in with every method in the list. Looks promising, but might need to be reviewed by someone who knows the ins and outs of authentication in ASP.NET Core.
So: some clients will be sending a cookie, and some will be sending Authorize header with a bearer token.
These are authentication schemes that you set up with AddAuthentication.
After that you need to set up authorization (AddAuthorization) to grant access to users who are authentication with either of these schemes.
(Authentication means checking that you are who you say you are (users), authorization means what you are and aren't allowed to do (roles) -- it's confusing.)

JWT auth with asp.net core to create token and store in http only cookies and angular to call method with header

I am new to JWT with basic idea of how it works. I have set the jwt token inside cookie from my web api.
Response.Cookies.Append("X-Access-Token", foundUser.Token
, new CookieOptions { HttpOnly = true }) ;
Now i am trying to call a web api get request which is marked as authorised from my agular application.
But inside angular i dont have a way to send the cookie. As per few documents i came to know that http only cookies are sent directly with our interference but i am not able to fetch the data with unauthorised error, which means that the token is not being used. I have not included the same in the get method as well. see below.
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet]
public async Task<ActionResult<IEnumerable<Invoice>>> GetInvoices([FromQuery]QueryParameters queryParameters)
{
return await _uOW.InvoiceRepository.GetInvoices(queryParameters);
}
Do i have to add some logic to read the cookie in backend? I didnt find any answer for this.
Please let me know if there is something missing.
Also inside angular i have not written any special code for calling this get. Is that required?
var headerForToken = new HttpHeaders({'Authorization':`Bearer ${sessionStorage.getItem("token")}`});
return this.http.get<any>(this.APIUrl+'Invoices/GetInvoices',{headers:headerForToken });
This is my current approach in which i am using local storage, but i really need to use the http only cookie.
If there is any link for solution or anything that would be really helpfull
Update 1: So we need to add a domain for this. I tried adding domain still it is not visible when i try to see the cookies.
Response.Cookies.Append("X-Access-Token", foundUser.Token
, new CookieOptions { HttpOnly = true, Domain = ".localhost:4200" }) ;

How to make SignalR Bearer Authentication Secure OR how to Not Log the Query String

When using Bearer Token Authentication, SignalR passes the JWT token through the query string. Obviously this bears the risk that the the token gets logged along with the request and thus anybody who can read the log can impersonate by copying the token.
The docs say:
If you have concerns about logging this data with your server logs, you can disable this logging entirely by configuring the Microsoft.AspNetCore.Hosting logger to the Warning level or above (these messages are written at Info level)
At this stage a side question pops into my mind: who guarantees that if the log level is Warning and something bad happens, the log won't still contain the request URL?
The docs continue with:
If you still want to log certain request information, you can write a middleware to log the data you require and filter out the access_token query string value (if present).
I guess the idea is to entirely switch off the default logging of requests and replace it with a custom logging middleware. This does not sound trivial to me. So I was wondering:
Is there any way of hooking into the logger and then customize what's actually being logged?
Or can we leverage the existing HTTP Logging for that? At the first glance it also seems to be a all-or-nothing option in regards to logging of query strings or is there a way of customizing that?
Is there a NuGet package that solves the issue?
How did others solve this problem?
I've resorted to take an approach where the JWT token does need to be sent as part of the query string, as explained here.
To summarize, when set as a cookie, the cookie will automatically be sent as part of the SignalR connection initialization by the browser:
document.cookie = `X-Authorization=${token}; path=/; secure; samesite=strict`; // https://stackoverflow.com/a/48618910/331281
const newConnection = new HubConnectionBuilder()
.withUrl('/background-queue-hub', {
skipNegotiation: true, // to avoid CORS problems (see: https://stackoverflow.com/a/52913505/331281)
transport: HttpTransportType.WebSockets,
})
...
However, this runs the risk of CSWSH. So, server-side we have to check the origin header to mitigate that. It can be done right where the cookie value is copied to the JWT Bearer authentication context:
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options => // https://stackoverflow.com/a/66485247/331281
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// for SignalR authentication we need to read the access token form the cookie
// since we don't want to pass the authentication token through the query string
// as the query string is being logged
if (context.HttpContext.Request.Path.StartsWithSegments(SignalRHubPath))
{
var allowedOrigins = Configuration["SignalR:AllowedOrigins"].Split(';');
if (allowedOrigins.Contains(context.Request.Headers["Origin"].ToString())) // see: https://www.tpeczek.com/2017/07/preventing-cross-site-websocket.html
{
context.Token = context.Request.Cookies["X-Authorization"];
}
else
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
}
}
return Task.CompletedTask;
}
};
});

User.Identity fluctuates between ClaimsIdentity and WindowsIdentity

I have an MVC site that allows logging in using both Forms login and Windows Authentication. I use a custom MembershipProvider that authenticated the users against Active Directory, the System.Web.Helpers AntiForgery class for CSRF protection, and Owin cookie authentication middle-ware.
During login, once a user has passed authentication against Active Directory, I do the following:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
var identity = new ClaimsIdentity(StringConstants.ApplicationCookie,
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
if(HttpContext.Current.User.Identity is WindowsIdentity)
{
identity.AddClaims(((WindowsIdentity)HttpContext.Current.User.Identity).Claims);
}
else
{
identity.AddClaim(new Claim(ClaimTypes.Name, userData.Name));
}
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userData.userGuid));
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
My SignOut function looks like this:
IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.SignOut(StringConstants.ApplicationCookie);
Logging in is performed via a jQuery.ajax request. On success, the Window.location is updated to the site's main page.
Logging in with both Forms and IntegratedWindowsAuthentication (IWA) works, but I've run into a problem when logging in with IWA. This is what happens:
The user selects IWA on the login page and hits the submit button. This is sent to the regular login action via an ajax request.
The site receives the request, sees the "use IWA" option and redirects to the relevant action. 302 response is sent.
The browser automatically handles the 302 response and calls the redirect target.
A filter sees that the request is headed to the IWA login action and that User.Identity.IsAuthenticated == false. 401 response is sent.
The browser automatically handles the 401 response. If the user has not authenticated using IWA in the browser yet, they get a popup to do so (default browser behavior). Once credentials have been received, the browser performs the same request with user credentials.
The site receives the authenticated request and impersonates the user to perform a check against Active Directory. If the user passes authentication, we finalize SignIn using the code above.
User is forwarded to the site's main page.
The site receives the request to load the main page. This is where things sometimes go awry.
The User.Identity at this point is of type WindowsIdentity with AuthenticationType set to Negotiate, and NOT as I would expect, the ClaimsIdentity created in the SignIn method above.
The site prepares the main page for the user by calling #AntiForgery.GetHtml() in the view. This is done to create a new AntiForgery token with the logged in user's details. The token is created with the WindowsIdentity
As the main page loads, ajax requests made to the server arrive with ClaimsIdentity! The first POST request to arrive therefore inevitably causes an AntiForgeryException where the anti-forgery token it sent is "for a different user".
Refreshing the page causes the main page to load with ClaimsIdentity and allows POST requests to function.
Second, related, problem: At any point after the refresh, once things are supposedly working properly, a POST request may arrive with WindowsIdentity and not with ClaimsIdentity, once again throwing an AntiForgeryException.
It is not any specific post request,
it is not after any specific amount of time (may be the first/second request, may be the hundredth),
it is not necessarily the first time that specific post request got called during that session.
I feel like I'm either missing something regarding the User.Identity or that I did something wrong in the log-in process... Any ideas?
Note: Setting AntiForgeryConfig.SuppressIdentityHeuristicChecks = true; allows the AntiForgery.Validate action to succeed whether WindowsIdentity or ClaimsIdentity are received, but as is stated on MSDN:
Use caution when setting this value. Using it improperly can open
security vulnerabilities in the application.
With no more explanation than that, I don't know what security vulnerabilities are actually being opened here, and am therefore loathe to use this as a solution.
Turns out the problem was the ClaimsPrincipal support multiple identities. If you are in a situation where you have multiple identities, it chooses one on its own. I don't know what determines the order of the identities in the IEnumerable but whatever it is, it apparently does necessarily result in a constant order over the life-cycle of a user's session.
As mentioned in the asp.net/Security git's Issues section, NTLM and cookie authentication #1467:
Identities contains both, the windows identity and the cookie identity.
and
It looks like with ClaimsPrincipals you can set a static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> called PrimaryIdentitySelector which you can use in order to select the primary identity to work with.
To do this, create a static method with the signature:
static ClaimsIdentity MyPrimaryIdentitySelectorFunc(IEnumerable<ClaimsIdentity> identities)
This method will be used to go over the list of ClaimsIdentitys and select the one that you prefer.
Then, in your Global.asax.cs set this method as the PrimaryIdentitySelector, like so:
System.Security.Claims.ClaimsPrincipal.PrimaryIdentitySelector = MyPrimaryIdentitySelectorFunc;
My PrimaryIdentitySelector method ended up looking like this:
public static ClaimsIdentity PrimaryIdentitySelector(IEnumerable<ClaimsIdentity> identities)
{
//check for null (the default PIS also does this)
if (identities == null) throw new ArgumentNullException(nameof(identities));
//if there is only one, there is no need to check further
if (identities.Count() == 1) return identities.First();
//Prefer my cookie identity. I can recognize it by the IdentityProvider
//claim. This doesn't need to be a unique value, simply one that I know
//belongs to the cookie identity I created. AntiForgery will use this
//identity in the anti-CSRF check.
var primaryIdentity = identities.FirstOrDefault(identity => {
return identity.Claims.FirstOrDefault(c => {
return c.Type.Equals(StringConstants.ClaimTypes_IdentityProvider, StringComparison.Ordinal) &&
c.Value == StringConstants.Claim_IdentityProvider;
}) != null;
});
//if none found, default to the first identity
if (primaryIdentity == null) return identities.First();
return primaryIdentity;
}
[Edit]
Now, this turned out to not be enough, as the PrimaryIdentitySelector doesn't seem to run when there is only one Identity in the Identities list. This caused problems in the login page where sometimes the browser would pass a WindowsIdentity when loading the page but not pass it on the login request {exasperated sigh}. To solve this I ended up creating a ClaimsIdentity for the login page, then manually overwriting the the thread's Principal, as described in this SO question.
This creates a problem with Windows Authentication as OnAuthenticate will not send a 401 to request Windows Identity. To solve this you must sign out the Login identity. If the login fails, make sure to recreate the Login user. (You may also need to recreate a CSRF token)
I'm not sure if this will help, but this is how I've fixed this problem for me.
When I added Windows Authentication, it fluctuated between Windows and Claims identities. What I noticed is that GET requests get ClaimsIdentity but POST requests gets WindowsIdentity. It was very frustrating, and I've decided to debug and put a breakpoint to DefaultHttpContext.set_User. IISMiddleware was setting the User property, then I've noticed that it has an AutomaticAuthentication, default is true, that sets the User property. I changed that to false, so HttpContext.User became ClaimsPrincipal all the time, hurray.
Now my problem become how I can use Windows Authentication. Luckily, even if I set AutomaticAuthentication to false, IISMiddleware updates HttpContext.Features with the WindowsPrincipal, so var windowsUser = HttpContext.Features.Get<WindowsPrincipal>(); returns the Windows User on my SSO page.
Everything is working fine, and without a hitch, there are no fluctuations, no nothing. Forms base and Windows Authentication works together.
services.Configure<IISOptions>(opts =>
{
opts.AutomaticAuthentication = false;
});

Owin app with WsFederation authentication stuck in infinite redirect loop

I have an Owin app that uses Ws-Federation authentication with a SSO application (not ADFS). Whenever my Owin app receives a request, it authenticates the user by first checking if it has the right cookie, and then it builds the claims and authentication ticket from that. If it doesn't have the right cookie, it redirects to the STS, which passes back a SAML token that can be used to complete the authentication.
All of this works except one part. After the token is received and validated, for some reason it redirects back to the STS, thus creating an infinite loop. I am quite sure it is because one or more of my configuration values is wrong since it's not very clear what each property is for and if it's required. I've copied my configuration code below:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType);
CookieAuthenticationOptions cookieOptions = new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
CookieName = "MyCookie",
CookiePath = "/CookiePath",
AuthenticationMode = AuthenticationMode.Active
};
// Basically the same as saying app.UseCookieAuthentication(app, cookieOptions)
app.Use(typeof(MyCustomCookieAuthenticationMiddleware), app, cookieOptions);
// Define properties for WsFederationAuthenticationOptions
var config = new Microsoft.IdentityModel.Protocols.WsFederationConfiguration
{
Issuer = "https://sts-domain.com/STS/",
TokenEndpoint = this.owinServerUrl // I don't know what this should be. I just made it the same as my owin start url
};
Saml2SecurityTokenHandler handler = new Saml2SecurityTokenHandler
{
Configuration = new SecurityTokenHandlerConfiguration
{
IssuerTokenResolver = new MyCustomSecurityTokenResolver
{
Thumbprint = somePublicKeyStr,
StoreLocation = System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine,
StoreName = "My"
}
}
};
var handlers = new SecurityTokenHandlerCollection(new List<SecurityTokenHandler>() { handler });
var wsFedOptions = new WsFederationAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
AuthenticationMode = AuthenticationMode.Passive,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType, // I'm not sure what this is exactly, but I think I've seen this used in examples
Configuration = config,
Wtrealm = this.owinServerUrl, // I'm guessing what this should be
Wreply = someString, // I have no idea what this should be -- it hasn't seemed to have any effect so far
SecurityTokenHandlers = handlers,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType, // I'm not sure whether this should be cookie or ws federation, but I don't think it's relevant to my problem
ValidIssuer = "https://sts-domain.com/STS/", // same as config.Issuer above
}
};
app.UseWsFederationAuthentication(wsFedOptions);
AuthenticateAllRequests(app, WsFederationAuthenticationDefaults.AuthenticationType);
app.Use<MyCustomMiddleware>();
}
private static void AuthenticateAllRequests(IAppBuilder app, params string[] authenticationTypes)
{
app.Use((context, continuation) =>
{
if (context.Authentication.User?.Identity?.IsAuthenticated ?? false)
{
return continuation();
}
else
{
context.Authentication.Challenge(authenticationTypes);
return Task.CompletedTask;
}
});
}
As I indicate in my code with my comments, there are some properties I am unsure of. Unfortunately, the documentation for WsFederationAuthenticationOptions does not help me out much. For example, I know that Wtrealm and Wreply are important (perhaps Wreply less so), but all the documentation says is "Gets or sets the 'wtrealm'" and "Gets or sets the 'wreply'." I found this thread that has an explanation:
wtrealm is a URI (not necessarily a URL) that identifies the RP. The STS uses this to decide whether to issue a token and what claims to give it.
wreply is the URL that the RP would like to be redirected to with the resulting token. The STS is not bound to comply with this request... sometimes the STS has a predefined address it will redirect to based on the established trust. At the very least, the STS should refuse to redirect to a different domain than the one it associates with the realm. Otherwise, the request could be a vector to send the user to a malicious site.
This makes sense, except for the fact that while I've been testing my owin app, Wreply seems to have no effect on where the STS is redirected to pass back the token; the URL I put for Wtrealm is what determines that.
All I'd like to do is for the STS to pass back the token, authenticate the user, and carry on to the route that the user specified which started all this. I'm not sure if this is relevant, but I also thought that the cookie is supposed to be set when the STS passes back the token. If this were the case, the infinite redirect wouldn't occur because when it comes back to be authenticated, the cookie authentication would find the cookie and the app would proceed as normal.
Update 1
I have changed a few of the values and have gotten different error messages, so I thought I'd share them here in case they help shed light on what could be going on. As you can tell from my post, I don't want to share real info about the app, so bear with me. Let's say the overarching web application (what contains my owin app, along with a bunch of other stuff) has the url http://localhost/app. My owin app has the server url (what I call this.owinServerUrl in the code above) http://localhost/app/owin.
When I make WsFederationConfiguration.TokenEndpoint = "http://localhost/app", I get the infinite redirect. When I make it "http://localhost/app/owin", I don't get an infinite redirect, but I do get another error (which error I get depends on other values, which I'll explain now).
I was mistaken -- Wreply does seem to have an effect. When I don't set Wreply, I get a 414 error: request URL too long. When I do set it (and as any string, whether it's a URL that I think could make sense or just gibberish), I get a 400 bad request: request too long.
Isn't it because of your AuthenticateAllRequests function? I think you think it only runs after the user is authenticated and context.Authentication.User?.Identity?.IsAuthenticated is set. However, I think it runs after the user is authenticated and before context.Authentication.User?.Identity?.IsAuthenticated is set. Since is never set, does it just invoke authentication again by calling "context.Authentication.Challenge(authenticationTypes)" ?

Categories

Resources