ASP.net Identity stops handing out External Cookie after removing external account - asp.net-mvc

I have a site setup with four 3rd-party login services, Microsoft, VS, Github, and Linkedin. Everything seems to work great, I can log in/out, add/remove external accounts with no problem.
Randomly however, it seems to stop working. When I try to login using any of the 3rd-party services, it just kicks me back to the login page.
Looking at the ExternalLoginCallback it appears that the AuthenticateResult.Identity is null and it can't get the external login info. Looking at it on the client-side it looks like they never got the external signin cookie.
I still can't consistently reproduce this error, so it's really hard to determine what might be happening. Any help would be great.
Update 1: I was able to identify the steps to reproduce:
Login to an account with more than 1 associated login
Remove one of the logins
In a new browser or a private session, try to log in with any of the 3rd-party accounts and you will be returned to login without an external cookie.
After hitting the error it won't hand out a cookie to any new sessions until IIS is restarted.
Update 2: Looks like it has something to do with setting a Session variable.
On the removeLogin action I was adding a value to the session. I'm not sure why but when I stopped doing that, I stopped having my problem. Time to figure out why... Update 3: Looks like this problem has been reported to the Katana Team
Update 4: Looks like someone else already ran into this problem. Stackoverflow post. They didn't give all of the code you needed to solve it, so I'll include that here as an answer.
Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app) {
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(appContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions {
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString("/Login"),
LogoutPath = new PathString("/Logout"),
Provider = new CookieAuthenticationProvider {
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User, int>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager),
getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))
)
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process.
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
// Enables the application to remember the second login verification factor such as phone or email.
// Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from.
// This is similar to the RememberMe option when you log in.
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
// Uncomment the following lines to enable logging in with third party login providers
app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["MSA:Id"],
ClientSecret = ConfigurationManager.AppSettings["MSA:Secret"],
Caption = "Microsoft"
});
app.UseVisualStudioAuthentication(new VisualStudioAuthenticationOptions(){
AppId = ConfigurationManager.AppSettings["VSO:Id"],
AppSecret = ConfigurationManager.AppSettings["VSO:Secret"],
Provider = new VisualStudioAuthenticationProvider(){
OnAuthenticated = (context) =>{
context.Identity.AddClaim(new Claim("urn:vso:access_token", context.AccessToken, XmlSchemaString, "VisualStudio"));
context.Identity.AddClaim(new Claim("urn:vso:refresh_token", context.RefreshToken, XmlSchemaString, "VisualStudio"));
return Task.FromResult(0);
}
},
Caption = "Visual Studio"
});
app.UseGitHubAuthentication(new GitHubAuthenticationOptions{
ClientId = ConfigurationManager.AppSettings["GH:Id"],
ClientSecret = ConfigurationManager.AppSettings["GH:Secret"],
Caption = "Github"
});
app.UseLinkedInAuthentication(new LinkedInAuthenticationOptions {
ClientId = ConfigurationManager.AppSettings["LI:Id"],
ClientSecret = ConfigurationManager.AppSettings["LI:Secret"],
Caption = "LinkedIn"
});
}

OWIN and asp.net handle cookies/session differently. If you authorize with OWIN before you initialize a session, anyone after a session is initialized will not be able to login.
Workaround: Add the following to your Global.asax
// Fix for OWIN session bug
protected void Application_AcquireRequestState() {
Session["Workaround"] = 0;
}
}
Long term: The way OWIN and asp.net handle sessions/cookies will be merged in vNext, use the work around until then...

Related

Issue with Azure B2C Reset Password user flow

I have recently developed an ASP.net MVC web application which uses Azure B2C to authenticate users.
I have been asked to enable the Reset Password User flow to enable users to reset via self-service.
I created the user flow within the portal (using the correct identity provider and setting Reset password using email address) and added the code from the microsoft example here however every time I click reset password, it just directs me back to the login screen and it never reaches the reset password page.
When I click the forgot password link the method below is called , it steps through the code fine, but then loads the login page.
Reset Password code
public void ResetPassword(string redirectUrl)
{
// Let the middleware know you are trying to use the reset password policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Startup.PasswordResetPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
The policy ID is correct in both azure and in the code as I step through and the values are all pulling through correctly (see below):
Policy ID string (as used above)
public static string PasswordResetPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
In Web.config where the policy is defined
<add key="ida:ResetPasswordPolicyId" value="B2C_1_UserApp_ResetPassword" />
I have provided all the code samples I have added for the reset function to work, the rest of the code is all included in the Microsoft Web App example.
Has anyone else experienced something similar? As I said previously, when you click the forgot password link it does exactly as it should and goes to the correct controller/method, but then goes back to the login screen.
Searching through my code, I found that the line
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(PasswordResetPolicyId));
was missing from ConfigureAuth. Once added this has fixed the issue.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebCookieManager()
});
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(SignInPolicyId));
/////////////////
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(PasswordResetPolicyId));
}

How does ADFS keep a user authenticated?

I'm using WsFed to implement ADFS SSO into an app. If I try to run [Authorize] methods, I'm taken to the sign in page. When I sign in, a cookie with encrypted information is created and I'm able to run [Authorize] methods. The cookie has option ExpireTimeSpan = TimeSpan.FromSeconds(10);. So far, this works as expected and an unauthorized user cannot access the app.
The confusion begins when the cookie expires, is altered, or deleted from the browser. When this happens, if I run an [Authorized] method I'm automatically signed in again without needing to reenter my credentials and the cookie is recreated. However, if I explicitly sign out using return SignOut(... method, then I am required to reenter my credentials.
Why does ADFS re-authenticate me if I delete the cookie, and how does it know to do so? It doesn't do it if I explicitly sign out. Shouldn't remaining authenticated depend on the cookie being present with the correct values?
Authentication setup in Startup.ConfigureServices:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = Configuration["AppSettings:wsfed:realm"];
options.MetadataAddress = Configuration["AppSettings:wsfed:metadata"];
options.UseTokenLifetime = false;
})
.AddCookie(options =>
{
options.Cookie.Name = "AuthenticationCookie";
options.LoginPath = "/signin-wsfed";
options.LogoutPath = "/NameController/Logout";
options.ExpireTimeSpan = TimeSpan.FromSeconds(10);
options.SlidingExpiration = true;
});
Login action:
[AllowAnonymous]
[HttpGet]
public IActionResult Login()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/Index",
};
return Challenge(authProperties, WsFederationDefaults.AuthenticationScheme);
}
Logout action:
[AllowAnonymous]
[HttpGet]
public IActionResult SignOutOfADFS()
{
return SignOut(
new AuthenticationProperties
{
RedirectUri = "https://app:1234/NameController/AfterLogout"
},
CookieAuthenticationDefaults.AuthenticationScheme,
WsFederationDefaults.AuthenticationScheme);
}
The AD FS is an identity provider that is commonly used for single sign-on purposes. As part of that, a key feature is that the AD FS does remember the signed-in user in order to authenticate them for another website. It does that by remembering the user using a separate session persisted using a cookie for the AD FS website.
When you sign out locally from your application, then all you are doing is clearing your local cookie. So when you try to authenticate again and the user is challenged to authenticate with the identity provider, the AD FS is able to sign the user in without asking them again for their credentials. For the AD FS your application is then just like a third website which is asking for authentication after the user already signed in into the AD FS.
In order to sign out completely, you will have to do a WSFederation sign out. As part of that process, the local cookie is cleared and then the user is redirected to an AD FS signout page where the AD FS authentication cookie is also cleared. On a subsequent authentication attempt, the AD FS then cannot remember the user anymore (since there’s no cookie) so they have to authenticate again with their credentials. That is what you are doing in your SignOutOfADFS action.
The WSFederation protocol supports a way for the authenticating application to require the user to reauthenticate with the identity provider by passing the wfresh=0 parameter with the authentication request. This is also supported in current AD FS versions. Unfortunately, I don’t think this parameter is currently supported by the WSFederation authentication handler for ASP.NET Core. It wouldn’t really prevent the user from reusing their authentication though, so you wouldn’t be able to use this a security feature.

OWIN WsFederation authentication with in-app authorization

The scenario:
A ASP.NET MVC Web app for a company's internal users, configured for authentication against the company's ADFS, using Microsoft.Owin.Security.WsFederation
The company's ADFS contains several users, but only a few of them should be able to log in to the application.
I therefore have a database table containing these users' email addresses
The web app should check if the email claim received from ADFS exists in the DB table, and only issue a log in token for those users.
Other users should be redirected to a "Sorry you are not authorized to use this application"-page.
My question:
Where is the correct place to put the authorization logic that checks if an user should be allowed in?
Here's the code in my Startup.Configuration method:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
MetadataAddress = "https://.../FederationMetadata.xml",
Wtrealm = "...",
}
);
You have two options to achieve what you want:
1. One is to configure this at the very AD FS. I personally think this is the right way to do this, since the AD FS is the IdP and it should be the one controlling whether or not its users have access to the application. In this case the company should or should not allow somebody to use some of its resources (of course there are anti-arguments). This can be easily done at the Domain Controller, through the AD FS Management GUI. The following answer greatly describes this:
https://serverfault.com/a/676930/321380
2. Second is to use the Notifications object at the OWIN WSFed middleware in this way:
Notifications = new WsFederationAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
//extract claims' values and check identity data against your own authorization logic
bool isAuthorized = CheckForUnauthorizedAccess();
if (!isAuthorized)
{
throw new SecurityTokenValidationException("Unauthorized access attemp by {some_identifier}");
}
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
if (context.Exception is an unauthorized exception)
{
context.OwinContext.Response.Redirect("<unauthorized_redirect_url>");
}
context.HandleResponse(); // Suppress the exception
//exception logging goes here
return Task.FromResult(0);
}
}

CookieAuthenticationOptions.LoginPath value not used when also using app.UseOpenIdConnectAuthentication

I am using OWIN middleware for cookie authentication and openIdConnect. Before I added openIdConnect authentication to my startup auth code the cookie authentication option, LoginPath was used as the destination for redirecting unauthenticated users. This worked really well and is the functionality I would like to keep.
However, when I added app.UseOpenIdConnectAuthentication to my project, it started automatically redirecting unauthenticated users to my OpenIdConnect Authority (https://login.windows.net/).
Is there a way I can disable OpenIdConnectAuthentication setting the redirect path for unauthenticated users and rely on the LoginPath set for cookie authentication? My current work around is to manually set the redirect path in my authorize attribute, but I would like to let OWIN middleware handle this if possible.
Thanks.
Code:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseCookieAuthentication(cookieOptions);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
}
I'm not sure if you were able to resolve this issue, but what you want to do it is add
AuthenticationMode = AuthenticationMode.Passive
To your authentication options. This will make the OpenIdConnect authentication rely solely on your code to make calls to it. I believe this is what you intend to happen.
So your new code should look like this:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseCookieAuthentication(cookieOptions);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
AuthenticationMode = AuthenticationMode.Passive,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
}
EDIT: This looked like it fixed my problems but it caused a more serious issue than the one it fixed. If I set use cookies after use open Id connect then my openidconnect notifications will have a null sessions HttpContext.Current.Session and after authentication my authentication result is not stored in the cookie.
Whichever authentication is added last becomes the authoritative source for setting the redirect path for unauthenticated uses.
By moving
app.UseCookieAuthentication();
After app.UseOpenIdConnectAuthentication() the desired behavior was achieved.
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
var cookieOptions = new CookieAuthenticationOptions();
cookieOptions.AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie;
cookieOptions.LoginPath = new PathString("/Account/Login");
app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = FranchiseAuthType,
ClientId = franchiseClientId,
Authority = FranchiseAuthority,
PostLogoutRedirectUri = postLogoutRedirectUri,
});
//move this after UseOpenIdConnectAuthentication and the LoginPath
//value is used for redirecting unauthenticated users
app.UseCookieAuthentication(cookieOptions);
}

Using cookies to stay signed in with third party login providers and Microsoft.AspNet.Identity.Owin 2.0

I've followed this tutorial in an attempt to use several third party login providers with a simple ASP.NET MVC SPA application I am writing. While configuration is simple enough (I've actually enabled Twitter and Microsoft), and the sign-in process works correctly, the user credentials are stored in a browser session cookie only and do not persist across browser sessions.
I've also tried using the alpha-1 sample project from NuGet (with the same basic configuration applied) and it also does not work (at least in my environment).
The web application is only hosted locally (as I do not have an Azure account in which to test).
I thought the setting ExpireTimeSpan would affect it, but it does not:
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(GetCookieAuthenticationOptions());
private static CookieAuthenticationOptions GetCookieAuthenticationOptions()
{
var options = new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieSecure = CookieSecureOption.SameAsRequest,
SlidingExpiration = true,
CookieName = "MYSECURITY",
ExpireTimeSpan = TimeSpan.FromDays(45.0),
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
};
return options;
}
Cookies (I changed the default name of the cookie intentionally to validate that the code was executing -- it doesn't work with the default either):
The MVC Single Page Application project template in Visual Studio contains the following method in the AccountController which forces all all external logins to not be persistent across browser sessions:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
// ...
await SignInAsync(user, isPersistent: false);
// ...
}
If you are comfortable with the security implications of trusting an identity that has been authenticated by an external provider across browser sessions, you could set isPersistent = true when calling SignInAsync.
Also be aware that any persistent login will be made non-persistent once the SecurityStampValidator fires the regenerateIdentity callback (which will occur after 20 minutes in your sample code above). See the question ExpireTimeSpan ignored after regenerateIdentity / validateInterval duration in MVC Identity (2.0.1) for discussion on this behavior.

Resources