How does WIF (WSFederationAuthentication) know which user I am? - asp.net

I've been put in charge of figuring out a way to allow users to authenticate into our forms based site. Our corporate IT has set up a development adfs server, and the relying party trust has been set up to one of our dev environments.
I've been reading and looking for tutorials for about two weeks, so I'm by no means an expert, and I can't seem to understand how the STS (our ADFS server) is supposed to figure out which user is requesting authentication.
I've been following Wiktor Zychla's blog because it actually includes code examples and messes less with the web.config as opposed to most other tutorials. (http://www.wiktorzychla.com/2014/11/simplest-saml11-federated-authentication.html)
Here's the pipeline so far as I can see it.
User comes to the log in page.
Page_Load automatically redirects user to ADFS
User gets authenticated and redirected back to the log in page
In the Page_Load I then consume some token and use that to authorize
the user.
Redirect the user to a page that requires authentication.
Code and Web.config changes I've made to the site:
protected void Page_Load(object sender, System.EventArgs e)
{
var sam = FederatedAuthentication.SessionAuthenticationModule;
var fam = new WSFederationAuthenticationModule();
fam.FederationConfiguration = FederatedAuthentication.FederationConfiguration;
var request = new HttpContextWrapper(this.Context).Request;
if (UseADFS)
{
// is this the response from the STS
if (!fam.IsSignInResponse(request))
{
// no
// the STS
fam.Issuer = WsFederationIssuerName;
// the return address
fam.Realm = WsRealm;
fam.Reply = WsReply;
var req = fam.CreateSignInRequest(string.Empty, null, false);
// go to STS
Response.Redirect(req.WriteQueryString());
}
// is this the response from the STS
else
{
// yes, get the SAML token
var securityToken = fam.GetSecurityToken(request);
var config = new SecurityTokenHandlerConfiguration
{
CertificateValidator = X509CertificateValidator.None,
IssuerNameRegistry = new CustomIssuerNameRegistry()
};
config.AudienceRestriction.AudienceMode = AudienceUriMode.Never;
var tokenHandler = new SamlSecurityTokenHandler
{
CertificateValidator = X509CertificateValidator.None,
Configuration = config
};
// validate the token and get the ClaimsIdentity out of it
var identity = tokenHandler.ValidateToken(securityToken);
var principal = new ClaimsPrincipal(identity);
var token = sam.CreateSessionSecurityToken(principal, string.Empty,
DateTime.Now.ToUniversalTime(), DateTime.Now.AddMinutes(20).ToUniversalTime(), false);
sam.WriteSessionTokenToCookie(token);
if (identity[0].IsAuthenticated)
{
//string email = principal.Claims.Where(x => x.Type == ClaimTypes.Email).Select(x => x.Value).SingleOrDefault();
string name = principal.Claims.Where(x => x.Type == ClaimTypes.Name).Select(x => x.Value).SingleOrDefault();
CustomClaimsObject claim = new CustomClaimsObject(name, principal);
doSigninWork(true, claim);
}
}
}
else if (!this.IsPostBack && !fam.IsSignInResponse(request))
{
Session.Abandon();
}
}
<configSections>
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
</configSections>
<httpModules>
<add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler"/>
</httpModules>
<modules>
<add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler"/>
</modules>
<system.identityModel>
</system.identityModel>
<system.identityModel.services>
<federationConfiguration>
<cookieHandler requireSsl="false" />
</federationConfiguration>
</system.identityModel.services>
<authentication mode="Forms">
<forms loginUrl="Public/invalidlogin.aspx?err=sessiontimeout" protection="All" requireSSL="false" slidingExpiration="true"/>
</authentication>
How is the STS supposed to figure out who is requesting authentication? I'm not posting any relevant user info to it. Am I supposed to add something to my request? Or is the context somehow supposed to have relevant data? Or does the ADFS box need to have a log in where the user enters their credentials?
Right now when I navigate to the user to (by way of the button click) https://xxx-xxx.xxxxx.xxxxxx.com/adfs/ls/ I get this error.
Encountered error during federation passive request.
Additional Data
Protocol Name:
wsfed
Relying Party:
Exception details:
Microsoft.IdentityServer.Web.CookieManagers.InvalidContextException: MSIS7001: The passive protocol context was not found or not valid. If the context was stored in cookies, the cookies that were presented by the client were not valid. Ensure that the client browser is configured to accept cookies from this website and retry this request.
at Microsoft.IdentityServer.Web.Protocols.GenericProtocolRequest.ParseEncodedRequestParts(String[] encodedRequestParts)
at Microsoft.IdentityServer.Web.Protocols.GenericProtocolRequest..ctor(String encodedGenericRequest)
at Microsoft.IdentityServer.Web.Protocols.WSFederation.WSFederationProtocolHandler.GetOriginalRequestFromResponse(ProtocolContext context)
at Microsoft.IdentityServer.Web.PassiveProtocolListener.ProcessProtocolRequest(ProtocolContext protocolContext, PassiveProtocolHandler protocolHandler)
at Microsoft.IdentityServer.Web.PassiveProtocolListener.OnGetContext(WrappedHttpListenerContext context)
Can someone kick me in the right direction? I'm sure I'm missing something obvious to anyone with experience.
Update:
Login with adfs working. But now I'm struggling a bit with the logout.
My logout code is below. Essentially whats happening is the authentication on my claims identity is still true even after I've attempted to "logout" with the code below. The only claim I'm sending from ADFS to my relying party is en E-mail claim. Not sure if that makes a difference.
WSFederationAuthenticationModule authModule = FederatedAuthentication.WSFederationAuthenticationModule;
string signoutUrl = (WSFederationAuthenticationModule.GetFederationPassiveSignOutUrl(WsFederationIssuerName, eURL.BuildURL(), null));
WSFederationAuthenticationModule.FederatedSignOut(new Uri(signoutUrl), null);
Update 2:
So I've realized the sign out is working correctly. However it seems ADFS is redirecting to my sign in page before it redirects to my log out page. This is causing the Sign in code to be run again which fires another claim to ADFS. Is there a way to stop this? Or a way to know if I'm coming from the adfs logout?
Update 3:
Solved the issue by wrapping an if statement around the if(UseADFS) clause to check to see where I'm coming from.
if(request.UrlReferrer.Authority.Contains(authority))
I just check to see if the place I'm coming from is my server (your adfs domain) vs (your website domain). This together with a parameter placed in my redirect url was enough to decipher whether the user was logging out or logging in.

Related

WebForms FormsAuthentication - Redirect after authentication results in Login Page to be called again

I have 2 ASP.NET WebForms projects under 2 different domain names. The first is merely a file server where important documents are stored. I am busy implementing FormsAuthentication so that if someone enters the URL of a document in a web browser, a redirect is done to a login page of the second WebForms project under a seperate domain name. I have pasted the Web.config content of the first File Server project and then the code from the second project to Authorize the download of the document. You will see that the code is not yet complete. What happens that the redirect is done to the login page. When authenticated, the redirect is done to the file, but the file server or first app sees this as a new attempt of accessing the file and redirects again to the login page. It is as if the cookie is not received from the file server.
Help will so much be appreciated.
App 1 - Just to store files.
<system.webServer>
<modules>
<remove name="FormsAuthentication" />
<remove name="DefaultAuthentication" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="" />
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="" />
</modules>
</system.webServer>
App 2 - Doing authentication and directs to the file on the fileserver (Note that both apps are on different domains)
protected void btnLogin_Click(object sender, EventArgs e)
{
bool Validated = true; // Still to be implemented
if (Validated)
{
FormsAuthenticationTicket Ticket = new FormsAuthenticationTicket(1, ".ASPXSAFECYTE", DateTime.Now, DateTime.Now.AddMinutes(30), true, "");
string CookieString = FormsAuthentication.Encrypt(Ticket);
HttpCookie Cookie = new HttpCookie(FormsAuthentication.FormsCookieName, CookieString);
Cookie.Expires = Ticket.Expiration;
Cookie.Path = FormsAuthentication.FormsCookiePath;
Response.Cookies.Add(Cookie);
if (Request["ReturnUrl"] == null)
Response.Redirect("~/Login.aspx", true);
else
{
string ReturnUrl = SystemProperties.FilesServerAddress.TrimEnd(new char[] { '/' }) + Request["ReturnUrl"];
Response.Redirect(ReturnUrl, true);
}
}
}
Kind regards,
Jaco
For security purposes, Cookies can only be created for the hosting domain and/or it's subdomains. Therefore, creating a Cookie from one domain for another will not work.

SignalR /negotiate is making requests to /Account/Login - I have no Account/Login endpoint

I'm seeing lots of entries in my logs from this request:
/signalr/negotiate
The error is:
The controller for path '/Account/Login' was not found or does not implement IController
I have a JS client connecting to an AppHub that requires authentication:
[Authorize]
[HubName("appHub")]
public class AppHub : Hub
{
// content
}
This is happening because there's an 'signalr` session alive with an expired cookie attempting to connect:
I'm not sure why the request is automatically seeking out this page. It's not specified anywhere in my web.config, routes, or elsewhere. Why is this happening?
I'd like to prevent the signalR client from attempting to connect if the user is unauthenticated. How can this be achieved?
If I understand your issue correctly then you are going to want to create your own custom class to handle this by inheriting the AuthorizeAttribute class: https://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute(v=vs.118).aspx
For example:
public class MyCustAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new HttpUnauthorizedResult();
}
else
{
//modify this to do whatever you want to happen if unauthorized
filterContext.Result = new RedirectResult("/session/noaccess/");
}
}
}
Then you can decorate it with your custom class instead of the default authorize
(example is from a mvc controller but should be able to function the same on your hub)
So this:
[Authorize]
public class AdminController : Controller
{
Becomes this:
[MyCustAuthorize]
public class AdminController : Controller
{
I believe the /Account/Login is the default path for forms auth so that is why it is directing there if it is not defined within your config file.
Alternatively you could insert the specific url to redirect to if that is what you are searching for by placing the following loginUrl attribute value in your auth section > forms element in the web.config:
It looks like this may be similar to these other answers to questions already asked here and these may provide your solution:
Stackoverflow 1
Stackoverflow 2
Have you tried stopping the connection in the client when they are no longer authorized?
$.connection.hub.stop();
Your app is using FormsAuthentication, so the Authorize attribute is redirecting to the login page by default when it fails to authorize.
You can disable this by adding the following to your web.config:
<modules runAllManagedModulesForAllRequests="true">
<remove name="FormsAuthentication" />
</modules>
This will remove all the default behaviors.
You might have something in your app.config that looks like
<membership defaultProvider="ClientAuthenticationMembershipProvider">
<providers>
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
</providers>
</membership>
Which is what is adding these default behaviors for you.

Need help getting [Authorize(Roles="Admin")] and User.isInRole("Admin") working in Asp.Net identity 1

I just don't get how these two potentially incredibly useful functions are supposed to work.
[Authorize(Roles="Admin")] //Method A
and
User.isInRole("Admin") //Method B
They are clearly not working right now for me. I did a few hours of research:
I read that you need to implement System.Web.Security.RoleProvider, then set it up in the web config:
<roleManager defaultProvider="OdbcRoleProvider"
enabled="true"
cacheRolesInCookie="true"
cookieName=".ASPROLES"
cookieTimeout="30"
cookiePath="/"
cookieRequireSSL="false"
cookieSlidingExpiration="true"
cookieProtection="All" >
<providers>
<clear />
<add
name="OdbcRoleProvider"
type="Samples.AspNet.Roles.OdbcRoleProvider"
connectionStringName="OdbcServices"
applicationName="SampleApplication"
writeExceptionsToEventLog="false" />
</providers>
</roleManager>
This caused the RoleProvider I implemented to by constructed, but the role checking functions were certainly not calling any of the methods in there.
I then read that Asp.NET Identity does away with the RoleProvider, now you need to do this:
<modules runAllManagedModulesForAllRequests="true">
<remove name="FormsAuthenticationModule" />
<remove name="RoleManager" />
</modules>
And I did that.
I have a custom UserManager that connects to my postgres backend. The problem is that whenever I use it, I need to instantiate one. It seems to me that if Functions A and B are going to work, then the UserManager I have implemented needs to be referenced in some sort of config file so Asp.NET knows about it implicitly. This would be just like the RoleManager in the past.
How does ASP.NET identity alter how Functions A and B check the roles from the old RoleProvider using behavior?
I figured it out.
When you call the login code like this:
var user = await UserManager.FindAsync(model.Email, model.Password);
if (user != null && user.PasswordRequiresReset == false)
{
await SignInAsync(user, model.RememberMe); //triggers IUserRoleStore.GetRolesAsync
return RedirectToLocal(returnUrl);
}
SignInAsync triggers GetRolesAsync from the IUserRoleStore:
public Task<IList<string>> GetRolesAsync(TUser user) //where TUser is an ApplicationUser
{
Func<object, IList<string>> getRoles = (object user2) =>
{
TUser user3 = (TUser)user2;
return user3.Roles.Select(x => x.Role.Name).ToList();
};
return Task.Factory.StartNew(getRoles, user);
}
In the above, I chose to simply generate the roles from the roles I already loaded from the db and stored in the ApplicationUser object when I created it in FindAsync.
The roles from GetRolesAsync must be loaded in the cookie somewhere where they can be accessed quickly and easily by Functions A and B.

Lock users out of Active Directory with ASP.NET application

We have an intranet ASP.NET 4.0 application and use forms authentication where employees authenticate against Active Directory to log in.
We need to lock users out of AD after too many failed password attempts (number is set in domain policy).
As it works now, users get locked out of the application only but not out of AD. We need to lock them in AD and they will need to call help desk to unlock them.
I saw this http://msdn.microsoft.com/en-us/library/ms998360.aspx, where it is stated under "Account Lockout" that ActiveDirectoryMembershipProvider locks users out of provider but not out of AD.
But how do we lock users in AD then?
web.config:
<membership defaultProvider="MyADMembershipProvider">
<providers>
<add name="MyADMembershipProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="ADConnectionString"
connectionUsername="administrator"
connectionPassword="passw0rd"
attributeMapUsername="sAMAccountName" />
</providers>
</membership>
Login.aspx:
<asp:Login ID="Login1" runat="server" DisplayRememberMe="False" FailureText="Wrong user name or password." DestinationPageUrl="~/User.aspx" OnLoggedIn="Login1_LoggedIn" OnLoginError="Login1_LoginError">
Login.aspx.cs
protected void Login1_LoginError(object sender, EventArgs e)
{
string userName = Login1.UserName;
if (!string.IsNullOrEmpty(userName))
{
// Get information about this user
MembershipUser usr = Membership.GetUser(userName);
if (usr != null)
{
// check to see if the error occurred because they are not approved
if (!usr.IsApproved)
{
Login1.FailureText = "Your account has not yet been approved by an administrator.";
}
// check to see if user is currently locked out
else if (usr.IsLockedOut)
{
Login1.FailureText = "You have been locked out of your account due to too many failed passwords. Call help desk to unlock it.";
}
}
}
}

Share ADFS/WIF federated claims cookie on multiple virtual applications (but the same domain)

I'm trying to (re)use ADFS 2/WIF claims based authentication cookies for multiple different applications running on the same domain.
So I have these applications/virtual directories that I would like to reuse the same authentication cookie:
https://domain.local/portal
https://domain.local/myapp
In the portal, I'd like to include (client side authenticated) content from myapp, so I don't want every app to be authenticated separately with a redirect to STS/ADFS.
I thought this would be pretty straightforward as they could both access the same cookie as they reside on the same domain, but the cookie is only valid for the application it was created in (FedAuth and FedAuth1 cookie paths are restricted to "/portal/")
When I set the 'path' in the cookieHandler settings to "/", I will get an exception:
[SecurityTokenException: ID4291: The security token 'System.IdentityModel.Tokens.SessionSecurityToken' is not scoped to the current endpoint.]
System.IdentityModel.Tokens.SessionSecurityTokenHandler.ValidateToken(SessionSecurityToken token, String endpointId) +1008632
System.IdentityModel.Services.SessionAuthenticationModule.ValidateSessionToken(SessionSecurityToken sessionSecurityToken) +351
System.IdentityModel.Services.SessionAuthenticationModule.SetPrincipalFromSessionToken(SessionSecurityToken sessionSecurityToken) +91
System.IdentityModel.Services.SessionAuthenticationModule.AuthenticateSessionSecurityToken(SessionSecurityToken sessionToken, Boolean writeCookie) +66
System.IdentityModel.Services.SessionAuthenticationModule.OnAuthenticateRequest(Object sender, EventArgs eventArgs) +929
System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +165
I've tried to use the Microsoft.Owin.Security.WsFederation beta packages mentioned in this article as an alternative, no success getting this running:
http://blogs.msdn.com/b/webdev/archive/2014/02/21/using-claims-in-your-web-app-is-easier-with-the-new-owin-security-components.aspx
Before I'm going to try to override methods in the SessionSecurityTokenHandler, is it even possible what I'm trying to achieve?
Thanks in advance!
Change cookieHandler as below in system.identityModel.services --> federationConfiguration
<federatedAuthentication>
<cookieHandler requireSsl="true" path="/" />
</federatedAuthentication>
It was actually pretty simple to do it, by replacing MachineKeySessionSecurityTokenHandler with a custom implementation that get's rid of the token validation:
public class SharedSecurityTokenHandler : MachineKeySessionSecurityTokenHandler
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SessionSecurityToken token, string endpointId)
{
if (token == null) throw new ArgumentNullException("token");
if (endpointId == null) throw new ArgumentNullException("endpointId");
return ValidateToken(token);
}
}
the just registering it here in the web.config:
<system.identityModel>
<identityConfiguration>
<securityTokenHandlers>
<add type="Security.Web.SharedSecurityTokenHandler, Security.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</securityTokenHandlers>
</identityConfiguration>
</system.identityModel>
I've been trying to achieve the same thing and found that SessionAuthenticationModule.ValidateSessionToken(SessionSecurityToken sessionSecurityToken) calls:
securityTokenHandler.ValidateToken(sessionSecurityToken, this.CookieHandler.Path)
..where the second argument is endpointId. Therefore, configuring both my apps with:
<system.identityModel.services>
<federationConfiguration>
<cookieHandler domain="example.com" path="/" />
...
</federationConfiguration>
</system.identityModel.services>
..allowed the validation in MachineKeySessionSecurityTokenHandler to pass.

Resources