How to make sure the user's original request URL is used after WS Federated authentication in OWIN app - c#

I have an OWIN app that authenticates users with cookie authentication, and if that fails then it tries WS-Federated authentication. In other words, if the correct cookie isn't there in the initial request, then go to the STS and fetch the security token.
The problem I'm having is making sure the user's original URL request is fulfilled if federated authentication is used. Thanks to this post, I realized that the redirect URL from the STS back to the OWIN app contains the "original" URL in the wctx query parameter. The issue is that this value is set, as far as I can tell, using WsFederationAuthenticationOptions.Wtrealm (perhaps Wreply?). This is a problem because these values are set in the initial configuration. I don't want a hard coded value -- I just want the URL the user originally used (i.e. IOwinContext.Request.Uri). The documentation for Wtrealm and Wreply does not help explain what the values should be.
I thought I could sneakily set Wtrealm to the user's request URL before redirecting to the STS, but apparently it's already set by the time RedirectToIdentityProvider notification is raised:
RedirectToIdentityProvider = context =>
{
context.Options.Wtrealm = context.Request.Uri.ToString();
}
Does anyone know what the correct approach is? Is there a way to make Wtrealm in the initial configuration the user's request URL? Or is Wtrealm not what I think it is, and I should be approaching this in a different way?

I believe I figured it out. Wtrealm, as I suspected, isn't what's causing the issue; that is used to determine where the STS should reenter to finish the federated authentication, but there is another redirect URL that determines where to go after authenticating.
I dug through the Microsoft.Owin.Security.WsFederation source code using ILSpy, and I figured out that there was a property I was not setting: AuthenticationProperties.RedirectUri. This RedirectUri property needs to be set before redirecting to the STS, as it needs to be used later in InvokeAsync after authenticating. I have an AuthenticateAllRequests method and I initialize this AuthenticationProperties object there:
private static void AuthenticateAllRequests(IAppBuilder app, params string[] authenticationTypes)
{
app.Use((context, continuation) =>
{
if (context.Authentication.User?.Identity?.IsAuthenticated ?? false)
{
return continuation();
}
else
{
AuthenticationProperties authProps = new AuthenticationProperties
{
RedirectUri = context.Request.Uri.ToString()
};
context.Authentication.Challenge(authProps, WsFederationAuthenticationDefaults.AuthenticationType);
return Task.CompletedTask;
}
});
}
To summarize more concretely: let's say my OWIN startup URL is http://myapp.com/owin. So then I set Wtrealm = "http://myapp.com/owin". Let's say a user goes to http://myapp.com/owin/coolstuff, and let's say they are signed into the authentication server with SSO, but they don't have the cookie for my app. In this case WS-Federated authentication needs to be used. http://myapp.com/owin/coolstuff is set as AuthenticationProperties.RedirectUri before redirecting to the STS so that it is put in the wctx query parameter, and can therefore be used after returning back to the OWIN app.
I was getting confused before because after returning from the STS, I would see IOwinRequest.Uri = "http://myapp.com/owin", and I would assume that was the final URL, and that the user's original request was lost. Now it makes a lot more sense that that is the URL the STS redirects to in order to finish authentication, but the final redirect URL, http://myapp.com/owin/coolstuff, is stored for redirecting after authentication finishes.
I am pretty sure this is how it works, but I may be wrong and if anyone has anything to add I'd love to hear it.

Related

How do I generate an assertion token for Sharepoint Access on behalf of another user in C#

I have a web application with a number of modules. One of the modules grabs a number of excel files from SharePoint directories, and then combines the data in them. So far, I have been just mapping the folders to OneDrive and accessing them that way. But this always uses my OneDrive credentials, which need to be refreshed from time to time. The right way to do this is to access them directly from Sharepoint on behalf of the user logged into my web application. I have the delegated API permission things set up in Azure, and I have the client ID and secret, etc.. I've been reading a number of articles on how to do this. All of them talk about how to get the token on behalf of someone else. These articles also talk about the assertion token needing to be passed in order to get the on behalf of token. However, they don't tell you how to get the assertion token in the first place. Here is the code I currently have:
'''var client = new RestClient("https://login.microsoftonline.com/XXXX/oauth2/v2.0/token");
var request = new RestRequest();
request.Method = Method.Post;
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("client_id", "MYCLIENTID", ParameterType.GetOrPost);
request.AddParameter("client_secret", "MYSECRET", ParameterType.GetOrPost);
request.AddParameter("scope", "https://MYTenent.sharepoint.com/.default", ParameterType.GetOrPost);
request.AddParameter("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer", ParameterType.GetOrPost);
request.AddParameter("requested_token_use", "on_behalf_of", ParameterType.GetOrPost);
RestResponse response = client.Execute(request);'''
The result of this is of course an error that the assertion was not supplied. I didn't supply any more code, because I can't even get passed this. The rest of my code takes the token and passes it to an auth provider, which is then used to instantiate the GraphServiceClient. Based on what I've read, that client is then used to get the lists, files, etc...
So, my question is, how do I get the assertion token in the first place? I'm hoping the code I have written so far is in the correct direction and all I'm missing is the assertion token.
UPDATE:
I've gotten one answer that really didn't help. I pretty much copied and pasted the code (replacing the clientID, etc..) and I received an error> I was going to copy and paste it from the solution comments provided in the answer, but I guess you can't do that while editing.
Someone also asked if I was able to get the auth code from the URL. The answer to that is no. We use 2 factor authentication, and I tried to manually look at the URLS as I was logging in, while using break points to slow things down a bit. And I did not see an auth code. I did put a break point directly after the line of code:
var info = await _signInManager.GetExternalLoginInfoAsync();
And when I look at the info variable, I can see 4 tokens. One of them is an access token and another is an ID token. The last one is an expiration date. I don't see an auth code, and from what I understand, by the time I see the access code, it's too late. The auth code was already used to get the access code.
UPDATE 2:
I know that OBO is not what I want. I also know that in order to use delegated permissions, I need to use the Auth Code flow and not client credentials. I can't seem to get the auth code from the users initial log in. And I don't know how to get it otherwise.
For those of you that might be thinking "Does he need to be spoon fed?", the answer is yes, I do. I need a simple code example that will get me the auth code, so I can use it in the rest of the code I already have. If anyone can paste that code into an answer and not provide a link, that would be great. I'm sorry, but the links I have been given, just go to microsoft learn sites that go through the explaination, but don't give any code samples.
The OBO flow is obviously not applicable in your context, and if you're going to get an access token on behalf of a logged in user, then you should focus on auth code flow or ROPC flow.
The corresponding C# code segment is:
using Microsoft.Graph;
using Azure.Identity;
var scopes = new[] { "https://{tenant-name}.sharepoint.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "tenant id";
// Values from app registration
var clientId = "client id";
var clientSecret = "client secret";
// For authorization code flow, the user signs into the Microsoft
// identity platform, and the browser is redirected back to your app
// with an authorization code in the query parameters
var authorizationCode = "authorization code ";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.authorizationcodecredential
var authCodeCredential = new AuthorizationCodeCredential(
tenantId, clientId, clientSecret, authorizationCode, options);
var accessToken = await authCodeCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes) { });
Console.WriteLine(accessToken.Token);
//var graphClient = new GraphServiceClient(authCodeCredential, scopes);

Redirect after cookie validation fails in ASP.NET Core

I have implemented a cookie validator as described in the ASP.NET docs:
public static class CookieValidator
{
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
...
if (invalidCookie)
{
context.RejectPrincipal();
await context.HttpContext.Authentication.SignOutAsync("MyCookieScheme");
}
}
}
It seems to be working correctly and gets into the invalidCookie block, rejecting the principal and signing out. After that I would like to redirect to a different URL. How do I have it redirect if I invalidate the cookie?
It should already return a 302 and add a 'location' header to your response with either the 'AccessDeniedPath' or 'LoginPath' when you reject principal, however lets say you want to add a header change the status code, or other customization.
In the invalidCookie block you can set custom headers with the following.
context.httpContext.Response.Headers["forceRedirect"] = httpContext.Request.Host.ToString();
Then when you get the response back on the client you need to check if this header is present, if it is then you redirect to the link given. This allows you to at least set headers, however if you changed the status code in the same place the framework will still override the status code when you reject principal (after you are out of the ValidateAsync function).
In order to do this, and stop the framework from overriding your custom response you have to declare an onRedirectToAccessDenied funtion.
In startup.cs: ConfigureServices
services.ConfigureApplicationCookie(options => { //this just makes checks against the cookie, so if the user deletes the cookie then ValidateAsync never fires
// Cookie settings ...
options.Events.OnRedirectToAccessDenied = CookieValidator.overrideRedirect;
options.Events.OnValidatePrincipal = CookieValidator.ValidateAsync;
});
Then in CookieValidator:
It is worth noting that if you do this then other rejections will execute this code too. Like if you also have a Roles authorization then when the user fails that authorization it will call this. This can be frustrating if you want the code to return different responses based upon why auth failedI am solving this by using policies and having them return the custom responses because it knows exactly why it failed then have overrideRedirect empty so the framework doesn't change my response.
internal static async Task overrideRedirect(RedirectContext<CookieAuthenticationOptions> context) {
...
//In here you can customize the response anyway you want and it will not be changed
}
It should be noted that ValidateAsync only is called if the cookie is present in the request, so if the user deletes the cookie then they can skip this validation. In my case I got around this by validating the cookie here, but still had other auth code (i added policy requirements in an IAuthorizationHandler) later that verified that the user had a claim. If the user deleted their cookie then the claim wouldn't be there and they would still lose access.

How to make ASP.NET create authenticated session with Owin OpenId Connect library?

After searching through lots of examples online I'm struggling with what seems to be a fairly simple requirement.
I'm trying to extend an existing ASP.NET Application that uses Form Authentication today so that it can use OpenID Connect for authentication as well as some role information coming from the Identity Provider. In particular I'm integrating with an existing hosted Identity Provider that I do not have control over.
I'm using ASP.NET MVC with the Owin components for OpenIdConnect. Namely,
Microsoft.Owin.Security
Microsoft.Owin.Security.Cookies
Microsoft.Owin.Security.OpenIdConnect
I am successfully able to:
In a web browser -- navigate to a controller method that is secured with the [Authorize] attribute
The Owin components properly redirect me to the Identity Provider where I can authenticate and then and I'm redirected back to my app (NOTE: my Identity Provider requires that a redirect_uri be passed in, so I'm currently setting that as part of the OpenIdConnectAuthenticationOptions startup configuration.)
When the redirect back to my app happens, I'm able to see the access_token and the id_token as part of the query string. Additionally, I've been able to use the access_token to call into the user info endpoint and properly derive information about the user using that token.
So far so good! HOWEVER.
What I'm failing to grasp and what most Owin examples I've seen don't seem to explain: what, if any, extra configuration is required to get ASP.NET to actually create an authenticated session in my application based on the redirect from the Identity Provider back to my application.
The general feeling I get from the documentation is that I should NOT have to do extra configuration within the Owin libraries -- that once I've configured the system to use cookie authentication and the OpenId Connect libraries -- that it should just work. However, this doesn't seem to be as easy as it looks. I'm guessing I'm missing something.
Some specific considerations/observations:
Many examples I've found don't require the RedirectUri to be set in the OpenIdConnectAuthenticationOptions, but my Identity Provider requires this parameter to be set each time.
Very few examples that I've found explain whether the controller method that fires as a result of the RedirectUri being hit should be secured with [Authorize] or left anonymous. In my testing, if I mark it as [Authorize] I get into an infinite redirect loop. If I leave it anonymous, I'm able to see the tokens in the request info but the ASP.NET Session is never created. For example, Request.IsAuthenticated is always false.
As a test I've set breakpoints inside several of the OpenIdConnectAuthenticationNotifications() events and currently I'm only seeing my code break into the RedirectToIdentityProvider event, and NONE of the others seem to hit -- which leads me to believe I'm not configuring this right.
Per suggestions I've found, I've set the authentication node this way in the web.config, but it doesn't seem to make a difference if I exclude this node.
<system.web>
<authentication mode="None" />
</system.web>
To summarize:
Do I need to specifically write code to handle the returning redirect from the Identity Provider to manually set up the ASP.NET Session (cookie etc.) for the given user? and
If so, should this code go in the controller method that is called as a result of RedirectUri being hit, or should the code go into one of the "Notifications" events available within OpenIdConnectAuthenticationNotifications()?
Lastly, if I'm NOT supposed to be setting up the Authenticated session manually after redirect from the Identity Provider (if it's supposed to work automatically), any suggestions for common mistakes on this configuration?
For completeness:
My Owin pipeline Startup Configuration method:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//no problems on these as far as I can tell
ClientId = "client_id_string",
ClientSecret = "client_secret_string",
Authority = "url_to_identity_provider",
Scope = "email name etc",
//I'm properly redirected to this URL but not sure
//if I should need to create the session manually
RedirectUri = "http://mymachine/mymvcapp/authorize",
//this causes the redirection to come with the access_token,
//which is valid
ResponseType = "token",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
//I'm able to break into this method
return Task.FromResult(0);
},
MessageReceived = (context) =>
{
//doesn't seem to run this line
return Task.FromResult(0);
},
SecurityTokenReceived = (context) =>
{
//doesn't seem to run this line
return Task.FromResult(0);
},
SecurityTokenValidated = (context) =>
{
//doesn't seem to run this line
return Task.FromResult(0);
},
AuthorizationCodeReceived = (context) =>
{
//doesn't seem to run this line
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
//doesn't seem to run this line
return Task.FromResult(0);
},
},
});
}
My secured method that properly initiates the login flow:
[Authorize]
public class HomeController : Controller
{
//I'm sent to the login flow the first time this is hit
public ActionResult Index()
{
return View();
}
}
My method at the RedirectUri that does get called but does indicate that the ASP.NET authenticated session was created:
public class AuthorizeController : Controller
{
// [Authorize] -- currently this Authorize attribute is turned off
//so the method is anonymous.
//If I turn that back on, I get infininte redirect loops to
//the Identity Provider
public ActionResult Index()
{
//the incoming request to this controller method from the
//identity provider DOES include valid access_token and id_token
//(which can be used against the user info endpoint) but does not
//create a valid ASP.NET session for my web app
//Request.IsAuthenticated is always false
//should there be a manual creation of the ASP.NET
//session/cookie information in this controller method?
//note: to me it would make most sense if this attribute was not
//anonymous since it's unlikely that the Request would ever appear
//as IsAuthenticated == true, but if you read the entire question
//it will be clear why I'm trying this method with anonymous access
return View();
}
}
As you found out, you can't put an [Authorize] attribute on the method the external server uses to notify you the user was authorized - the session isn't authorized yet, you're just being notified that it should be.
Fortunately, creating that session is not difficult:
How can I manually create a authentication cookie instead of the default method?
(I'm pretty sure you have to do this yourself with the basic Microsoft Owin stuff - and you always can do it yourself if you want.)

Is validating a Realm sufficient security?

I'm working on an OpenId Provider for an SSO setup - it's basically a web application portal that shares credentials with any of the "applications" the user has access to. I have the Provider set up and everything is working fine, but I have a question about security.
I want to do some permissions checking on the Provider before it sends a positive assertion to the RP; namely that the user actually has permissions to the application which is making the request.
Here's the Provider code I've got at the moment (just a snippet, can add more if necessary):
private bool AutoRespondIfPossible(out ActionResult response)
{
if (ProviderEndpoint.PendingRequest.IsReturnUrlDiscoverable(OpenIdProvider.Channel.WebRequestHandler) == RelyingPartyDiscoveryResult.Success
&& User.Identity.IsAuthenticated && this.RealmIsValid(ProviderEndpoint.PendingAuthenticationRequest.Realm)) {
if (ProviderEndpoint.PendingAuthenticationRequest != null) {
if (ProviderEndpoint.PendingAuthenticationRequest.IsDirectedIdentity
|| this.UserControlsIdentifier(ProviderEndpoint.PendingAuthenticationRequest)) {
ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated = true;
response = this.SendAssertion();
return true;
}
}
//we don't want anon requests
if (ProviderEndpoint.PendingAnonymousRequest != null) {
ProviderEndpoint.PendingAnonymousRequest.IsApproved = false;
response = this.SendAssertion();
return true;
}
}
response = null;
return false;
}
Basically what I'm doing is validating that the realm of the request (in the RealmIsValid method) matches to a hostname in my list of acceptable hostnames, and then I'm comparing the user permissions based on the hostname.
What I'm wondering is: How accurate is ProviderEndpoint.PendingAuthenticationRequest.Realm? If I understand correctly, the realm is set by the relying party - is it possible that the endpoint could receive a request from a URI other than the realm specified in that request? Or am I safe to assume that the realm will always be accurate (that is: match the URI of the relying party)?
Yes, the OpenID realm is reliable, due to two steps OpenID 2.0 and DotNetOpenAuth takes:
The OpenID return_to URL must be a derivative of the realm URL. So while anyone can formulate an OpenID request as if it came from any relying party, the alleged relying party will always be the one to actually receive the response, so an attacker operating another RP will not get the response.
Some "open redirector" attacks might allow the attacker to use a return_to URI that is based on a legitimate Realm URL, but happens to be a URL that will redirect to the attacker's web site, thus delivering the assertion to the attacker. This is mitigated by "RP Discovery" which your code snippet includes with its call to the IsReturnUrlDiscoverable method. The RP should explicitly list the allowed return_to URLs in its RP Discovery XRDS document, so that open redirector endpoints are not allowed.
That all said, OpenID is mostly about identifying the user -- not authorizing them to specific RPs. So while what you're doing may be fine, it's a bit off the beaten track for OpenID use, so please consider the security implications carefully (as it sounds like you're doing now).

How do I use the cookie container with RestSharp and ASP.NET sessions?

I'd like to be able to call an authentication action on a controller and if it succeeds, store the authenticated user details in the session.
However, I'm not sure how to keep the requests inside the session as I'm using RestSharp as a detached client. I need to somehow get a key back from the server on successful authorisation and then for each future call, check the key with that stored in the session.
How do I ensure the RestClient in RestSharp sends all future requests with the cookie set correctly so inside service calls, the session variable can be retrieved correctly?
I've been looking at the cookie container with HttpFactory but there doesn't seem to be any documentation on this anywhere.
If someone is having a similar problem, please note that the above is not quite required for a simple "store my cookies after each request" problem.
Jaffas approach above works, but you can simply attach a CookieStore to your RestClient and have the cookies be stored automatically.
I know this is not a solution for everyone, since you might want to store dedicated cookies only. On the other hand it makes your life easier for testing a REST client!
(I used Jaffas variables for ease):
CookieContainer _cookieJar = new CookieContainer();
var client = new RestClient("http://<test-server>/letteron"); //test URL
client.CookieContainer = _cookieJar;
I worked this out in the end.
Basically create a cookie container, then add the session cookie from the response into the cookie container. All future requests will then contain this cookie.
var sessionCookie = response.Cookies.SingleOrDefault(x => x.Name == "ASP.NET_SessionId");
if (sessionCookie != null)
{
_cookieJar.Add(new Cookie(sessionCookie.Name, sessionCookie.Value, sessionCookie.Path, sessionCookie.Domain));
}

Categories