OWIN Cookies not being removed by IE - asp.net-mvc

I'm experiencing a problem where the following code does not appear to be working as expected:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
When this is executed the above line of code I would expect the following property to be false
AuthenticationManager.User.Identity.IsAuthenticated
But it is not, it remains true (FOR IE ONLY, I'm using IE 11)
I'm exposing my Owin context through the following property:
public IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.Current.GetOwinContext().Authentication;
}
}
I am also attempting to manually remove the authentication cookie using the following code:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var appCookie = HttpContext.Current.Request.Cookies[".AspNet.AuthCookie"];
if (appCookie != null)
{
appCookie = new HttpCookie(".AspNet.AuthCookie");
appCookie.Expires = DateTime.Now.AddYears(-1);
HttpContext.Current.Response.Cookies.Add(appCookie);
}
Which, again, does not work in IE. It works in Chrome.
FYI, i am manually setting the name of the cookie in my Startup.Auth.cs
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ExpireTimeSpan = TimeSpan.FromMinutes(Config.AuthenticationCookieTimeOutMinutes),
CookieHttpOnly = true,
CookieName = ".AspNet.AuthCookie",
LoginPath = new PathString("/Account/Login"),
CookieSecure = CookieSecureOption.SameAsRequest
});
There is plenty online about the Signout() method not working properly, but most solutions are to remove the cookie manually, which i am having no joy with. Why would this not work in IE 11?
UPDATE
So I was unable to fix the issue with cookies in IE, however was able to overcome this issue by overriding the HandleUnauthorizedRequest virtual of the AuthorizeAttribute to see if the user has been authenticated, then double checks the CurrentUser held in session
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
UserAccount currentUser = (UserAccount)filterContext.HttpContext.Session[SessionStrings.CurrentUser];
if (filterContext.HttpContext.Request.IsAuthenticated && currentUser != null)
{
...
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}

Related

MSAL: Getting null everytime when AcquireTokenSilent in ASP.NET MVC Classic

I am working on ASP.Net MVC 4.7.2 (classics). Using Azure AD Authenticaiton (Microsoft Identity platform) for authentication and for Web Api authorization.
When using code flow to get the api resource I wan to store the token and get it silently when needed. I got the code from MSAL team from Git. But code doesn't work. Whenever I acquire the code silently the I get the error. When I debugged the issue I found that IAccounet is return null see the following line that returns null.
IAccount account =
_MsalAppBuilder.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId()).Result;
Since account is null therefore the next line of code throws error. Therefore nt aoo us unable to work
AuthenticationResult result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
As I debugged the issue, I could not find any reason why it is happening and even after extensive search. However what I found that in the Startup class, the method AcquireAccessToken never hit, thus the token does not save.
Can something help understand it please.
MSAL class:
public static class MsalAppBuilder
{
public static string GetAccountId(this ClaimsPrincipal claimsPrincipal)
{
string oid = claimsPrincipal.GetObjectId();
string tid = claimsPrincipal.GetTenantId();
return $"{oid}.{tid}";
}
private static IConfidentialClientApplication clientapp;
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder.Create(Globals.clientId)
.WithClientSecret(Globals.clientSecret)
.WithRedirectUri(Globals.redirectUri)
.WithAuthority(new Uri(Globals.authority))
.Build();
// In-memory distributed token cache
clientapp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = true;
});
});
}
return clientapp;
}
//this was commented already
/*
// Could also use other forms of cache, like Redis
// See https://aka.ms/ms-id-web/token-cache-serialization
clientapp.AddDistributedTokenCache(services =>
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "SampleInstance";
});
});
*/
public static async Task RemoveAccount()
{
BuildConfidentialClientApplication();
var userAccount = await clientapp.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId());
if (userAccount != null)
{
await clientapp.RemoveAsync(userAccount);
}
}
startup class:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
//app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// CookieManager = new SystemWebCookieManager()
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
//ResponseType = OpenIdConnectResponseType.CodeIdToken,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var scopes = Globals.scopeTravelAuthApi;
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { scopes}, context.Code).ExecuteAsync().ConfigureAwait(true);
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
/// <summary>
/// Handle failed authentication requests by redirecting the user to the home page with an error in the query string
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}
By the way, I have checked all the settings related to Azure AD they ar correct. In the same app I am accessing Graph API which is working fine only issue is getting the access token silently.
My project is .ASP.Net 4.8. In order to fix the issue, I did not update the code, my code remain as is. I just upgraded the NuGet Packages whatever that make sence or needed to be upgraded for ASP.Net 4.8 but specifically the following:
Microsoft.Identity.Client, Microsoft.Identity.Client.Extensions.Msal, Microsoft.Identity.Web.TokenCache, and owin Nuget packages.

Dynamic database connection using Asp.Net identity

I am working on a multi-tenant application that uses multiple databases. There is one master database that contains user information and then each tenant database also has their own users for that tenant (which are a subset of the users in the master database).
The user will log in which will check the master database, then based on their details (i.e. which tenant they belong to) it will log them into the application using the user details on their tenant database.
I am using the method described in this thread (Dynamic database connection using Asp.net MVC and Identity2) to set the database for UserManager each time because at the point that the application starts it will not know what database to use therefore the following code in "Startup.Auth" would be setting the incorrect database:
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
This seems to be working well for most things but one problem I have is with the user getting logged out after the time set in "validateInterval" shown in the code below (this has been set to 20 seconds for testing):
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
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, ApplicationUser>(
validateInterval: TimeSpan.FromSeconds(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I think the problem might be because when the code above is called in the "Startup.Auth" file it does not know what database to use however I have not confirmed this.
If I debug the "GenerateUserIdentityAsync" code I can see that it is getting the correct "securityStamp" for the user from the client database which makes me think it is finding the correct database but I cannot work out why it is still logging out the user after the time set for "validateInterval".
Can anyone offer any advice on how this can be resolved or at least possible ways to try and debug what the problem might be?
I have experienced the same issue on my multi-tenant ASP.NET MVC app.
If your goal is to set an expiration time for the logged-in user just remove the code in CookieAuthenticationProvider and set the ExpireTimeSpan property in the parent CookieAuthenticationOptions.
Your code should be:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
ExpireTimeSpan = TimeSpan.FromMinutes(15), //cookie expiration after 15 mins of user inactivity
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
}
});
Hope this helps.
Okay this is the full solution I have come up with which partly uses what #jacktric suggested but also allows for validating the security stamp if a users password has been changed elsewhere. Please let me know if anyone can recommend any improvements or see any downfalls in my solution.
I have removed the OnValidateIdentity section from the UseCookieAuthentication section as follows:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
I then have the following IActionFilter that is registered in the FilterConfig.cs which checks if the user is logged in (I have parts of the system that can be accessed by anonymous users) and whether the current security stamp matches the one from the database. This check is made every 30 minutes using sessions to find out when the last check was.
public class CheckAuthenticationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext filterContext)
{
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
try
{
// If not a child action, not an ajax request, not a RedirectResult and not a PartialViewResult
if (!filterContext.IsChildAction
&& !filterContext.HttpContext.Request.IsAjaxRequest()
&& !(filterContext.Result is RedirectResult)
&& !(filterContext.Result is PartialViewResult))
{
// Get current ID
string currentUserId = filterContext.HttpContext.User.Identity.GetUserId();
// If current user ID exists (i.e. it is not an anonymous function)
if (!String.IsNullOrEmpty(currentUserId))
{
// Variables
var lastValidateIdentityCheck = DateTime.MinValue;
var validateInterval = TimeSpan.FromMinutes(30);
var securityStampValid = true;
// Get instance of userManager
filterContext.HttpContext.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var userManager = filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
// Find current user by ID
var currentUser = userManager.FindById(currentUserId);
// If "LastValidateIdentityCheck" session exists
if (HttpContext.Current.Session["LastValidateIdentityCheck"] != null)
DateTime.TryParse(HttpContext.Current.Session["LastValidateIdentityCheck"].ToString(), out lastValidateIdentityCheck);
// If first validation or validateInterval has passed
if (lastValidateIdentityCheck == DateTime.MinValue || DateTime.Now > lastValidateIdentityCheck.Add(validateInterval))
{
// Get current security stamp from logged in user
var currentSecurityStamp = filterContext.HttpContext.User.GetClaimValue("AspNet.Identity.SecurityStamp");
// Set whether security stamp valid
securityStampValid = currentUser != null && currentUser.SecurityStamp == currentSecurityStamp;
// Set LastValidateIdentityCheck session variable
HttpContext.Current.Session["LastValidateIdentityCheck"] = DateTime.Now;
}
// If current user doesn't exist or security stamp invalid then log them off
if (currentUser == null || !securityStampValid)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "Controller", "Account" }, { "Action", "LogOff" }, { "Area", "" } });
}
}
}
}
catch (Exception ex)
{
// Log error
}
}
}
I have the following extension methods for getting and updating claims for the logged in user (taken from this post https://stackoverflow.com/a/32112002/1806809):
public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return;
// Check for existing claim and remove it
var existingClaim = identity.FindFirst(key);
if (existingClaim != null)
identity.RemoveClaim(existingClaim);
// Add new claim
identity.AddClaim(new Claim(key, value));
// Set connection string - this overrides the default connection string set
// on "app.CreatePerOwinContext(DbContext.Create)" in "Startup.Auth.cs"
HttpContext.Current.GetOwinContext().Get<DbContext>().Database.Connection.ConnectionString = DbContext.GetConnectionString();
var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = true });
}
public static string GetClaimValue(this IPrincipal currentPrincipal, string key)
{
var identity = currentPrincipal.Identity as ClaimsIdentity;
if (identity == null)
return null;
var claim = identity.Claims.FirstOrDefault(c => c.Type == key);
return claim.Value;
}
And finally anywhere that the users password is updated I call the following, this updates the security stamp for the user whose password is being edited and if it is the current logged in users password that is being edited then it updates the securityStamp claim for the current user so that they will not get logged out of their current session the next time the validity check is made:
// Update security stamp
UserManager.UpdateSecurityStamp(user.Id);
// If updating own password
if (GetCurrentUserId() == user.Id)
{
// Find current user by ID
var currentUser = UserManager.FindById(user.Id);
// Update logged in user security stamp (this is so their security stamp matches and they are not signed out the next time validity check is made in CheckAuthenticationFilter.cs)
User.AddUpdateClaim("AspNet.Identity.SecurityStamp", currentUser.SecurityStamp);
}

How to do programmatic sign in using aspnet Identity Framework v2?

I'm cobbling together snippets of code from blogs and different places to try to get this to work. Normally, I'd refer to the reference documentation, but I can't find it here or anywhere else. It's just videos and demos for specific use cases that include user management or facebook or twitter.
I have a proprietary authentication service that I'm using. User accounts are not managed inside my application. So I need to be able to sign in a user that's completely constructed at run time.
Here's what I'm trying now in my MVC app.
using System.Security.Claims;
public class HomeController : Controller {
public ActionResult Scratch() {
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
var identity = new ClaimsIdentity(claims);
var authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignIn(identity);
return Content(
$"authentication manager type: {authenticationManager.GetType()} \n"
+ $"authenticated: {HttpContext.User.Identity.IsAuthenticated} \n"
+ $"user name: {HttpContext.User.Identity.Name} \n",
"text/plain");
}
}
The output is
authentication manager type: Microsoft.Owin.Security.AuthenticationManager
authenticated: False
user name:
Questions:
Why does the output show that the user has not been authenticated? What more do I have to do to get this user authenticated?
Where is the documentation for this framework?
Update
Startup.cs
public partial class Startup {
public void Configuration(IAppBuilder app) {
ConfigureAuth(app);
ConfigureAnalyticContext(app);
}
}
Startup.Auth.cs:
(there is actually much more, but all the rest has been commented out, in search of finding a minimal configuration that works)
public partial class Startup {
public void ConfigureAuth(IAppBuilder app) {
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
AnalyticContext.Auth.cs
(this is my Entity Framework context, I doubt it's related to this problem)
public partial class Startup {
public void ConfigureAnalyticContext(IAppBuilder app) {
app.CreatePerOwinContext(() => CentoAnalyticsContext.Create());
}
}
Well, it seems that you are not using ASP.NET Identity. ASP.NET Identity is new membership system of asp.net, which automatically creates database tables for storing users, encrypting password, etc.
What you are trying to do is to use the new authentication system provided by OWIN, which replaces the old FormsAuthentication style.
To make it work, you have to create the cookie authentication. Like this:
public static class AuthConfig
{
public const string DefaultAuthType = "DefaultAppCookie";
public const string LoginPath = "/System/SignIn";
public static void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthType,
LoginPath = new PathString(LoginPath)
});
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; //or whatever
}
}
In the login action:
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
ClaimsIdentity identity = new ClaimsIdentity(claims, AuthConfig.DefaultAuthType);
IAuthenticationManager authManager = Request.GetOwinContext().Authentication;
authManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
I think that should be enough to make it work in your app. A few days ago I answered a similar question MVC Authentication - Easiest Way, take a look, it might be helpful.
I recently have added Active Directory authentication, constructed ClaimsPrincipal myself and signed-in the same way you do.
And you are indeed missing .UseCookieAuthentication in your ConfigureAuth(IAppBuilder app)
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "MyAuthenticationName", // <-- this must match the AuthenticatioType name when you do sign-out
LoginPath = new PathString("/MyLoginPath"),
CookieName = "MyCookieName",
CookieHttpOnly = true,
});
}
And you don't need UseExternalSignInCookie.
Request.IsAuthenticated will be false with in the same request flow.
I think you still need to update the current security principal if you need to check IsAuthenticated for the request as authenticationManager.SignIn only validates the user against data store and sets the OWIN cookie which when sent back in subsequent request sets the security principal , usually a redirect takes care of this as in most cases there will be redirection in home page or something. If you still need to check with in the same request you can do something like below depending on your requirement
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "somename"),
new Claim(ClaimTypes.NameIdentifier, "someidentifier"),
new Claim("foo", "bar"),
};
var identity = new ClaimsIdentity(claims,DefaultAuthenticationTypes.ApplicationCookie,
ClaimTypes.Name, ClaimTypes.Role);
var principal = new ClaimsPrincipal(identity);
System.Threading.Thread.CurrentPrincipal = principal;
if (System.Web.HttpContext.Current != null)
System.Web.HttpContext.Current.User = principal;
Hope this helps.

WsFederation and local user mixed authentication

I'm trying make my user login with Azure AD credentials (using OWIN WsFederation plugin) or using a local user account with microsoft asp.net identity in a MVC 5.1 Web App.
Login with local users work fine, login using a federated account works only once, and I need to restart my app to make it work again.
I suppose the problem is with the response from Microsoft login page not processed correctly
Infact, using two differente browsers (chrome+ie) in private mode and Fiddler, I can see that my cookie is set on first request but not on a subsequent request made from a different browser
First request
Second request
This is my ConfigureAuth
public void ConfigureAuth(IAppBuilder app)
{
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.SetDefaultSignInAsAuthenticationType("ExternalCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
});
// these two lines of code are needed if you are using any of the external authentication middleware
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
app.UseWsFederationAuthentication(new Microsoft.Owin.Security.WsFederation.WsFederationAuthenticationOptions()
{
MetadataAddress = "https://login.windows.net/XXXXXXX.onmicrosoft.com/federationmetadata/2007-06/federationmetadata.xml",
Wtrealm = "https://MYREALM",
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
});
}
This is part of the account controller
//
// POST: /Account/ExternalLogin
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public ActionResult ExternalLoginCallback(string returnUrl)
{
var ctx = Request.GetOwinContext();
var result = ctx.Authentication.AuthenticateAsync("ExternalCookie").Result;
if (result != null) //null on request other than the first (!!!)
{
ctx.Authentication.SignOut("ExternalCookie");
var claims = result.Identity.Claims.ToList();
claims.Add(new Claim(ClaimTypes.AuthenticationMethod, "External Account"));
var email = claims.Where(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name").SingleOrDefault().Value;
var ci = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
ctx.Authentication.SignIn(ci);
}
return RedirectToLocal(returnUrl);
}
In the ConfgureAuth set AuthenticationMode to Passive. It worked in my workflow which seems very similar to yours.
app.UseWsFederationAuthentication(new Microsoft.Owin.Security.WsFederation.WsFederationAuthenticationOptions()
{
MetadataAddress = "https://login.windows.net/XXXXXXX.onmicrosoft.com/federationmetadata/2007-06/federationmetadata.xml",
Wtrealm = "https://MYREALM",
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
AuthenticationMode = AuthenticationMode.Passive
});
http://msdn.microsoft.com/en-us/library/microsoft.owin.security.authenticationmode%28v=vs.113%29.aspx

MVC 5 OWIN - IsAuthenticated is false on external login (QQ Connect)

I hope someone can help me out with this problem - it's driving me mad! :)
I'm trying to use external login through QQ Connect (OAuth 2.0) using tinysnake's QQ Connect provider: https://github.com/tinysnake/microsoft-owin-security-qq
Everything seems to be going great - I can sign in via my QQ account and I get posted back to my ExternalLoginCallBack-method with the appropriate claims etc.
I use these values to sign the user in through the IAuthenticationManager - all goes well. However - when I redirect the user to another page and checks if he's logged in - then I get a false value from the IsAuthenticated value... and I can't read any of the claims I set earlier.
It might be a simple fix - but I just can't see it right now :)
Some code:
AuthConfig:
public static void ConfigureAuthentication(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Normal cookie sign in
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
AuthenticationMode = AuthenticationMode.Active
});
// QQ CONNECT
app.UseQQConnectAuthentication(
appId: "XXXXXX",
appSecret: "XXXXXXXXXXXXXXXXX");
}
AccountController:
//
// POST: /Account/ExternalLogin
[System.Web.Mvc.HttpPost]
[System.Web.Mvc.AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
//
// GET: /Account/ExternalLoginCallback
[System.Web.Mvc.AllowAnonymous]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var ctx = Request.GetOwinContext();
var result = ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie).Result;
var claims = result.Identity.Claims.ToList();
var name = claims.First(i => i.Type == "urn:qqconnect:name");
claims.Add(new Claim(ClaimTypes.AuthenticationMethod, "QQ"));
claims.Add(new Claim(ClaimTypes.Name, name.Value));
var ci = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ExternalCookie);
ctx.Authentication.SignIn(ci);
// DO OTHER STUFF HERE
return Redirect("~/");
}
All seems to be going well so far...
HomeController:
public ActionResult Index()
{
var model = new HomeViewModel();
var ctx = Request.GetOwinContext();
if (ctx.Authentication.User.Identity.IsAuthenticated) // <-- THIS RETURNS FALSE
{
var claimsIdentity = User.Identity as ClaimsIdentity;
model.Name = claimsIdentity.FindFirst(ClaimTypes.Name).Value;
model.IsAuthenticated = true;
}
return View(model);
}
When I check the ctx.Authentication.User.Identity.IsAuthenticated, I get a false value... and I can't retrieve any of the claims either.
Am I missing something?
Any help would be greatly appreciated :)
UPDATE
I got my code working by doing this in my AccountController:
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var ctx = Request.GetOwinContext();
var result = ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie).Result;
if (result.Identity.IsAuthenticated)
{
// Signed in successfully
var claims = result.Identity.Claims.ToList();
var name = claims.First(i => i.Type == "urn:qqconnect:name");
//claims.Add(new Claim(ClaimTypes.AuthenticationMethod, "QQ"));
claims.Add(new Claim(ClaimTypes.Name, name.Value));
var id = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
}
return Redirect("~/");
}
But the way I see it - here I'm using the ApplicationCookie and NOT the ExternalCookie for signing in... or am I missing something entirely?
This solution works for me - but I'd like to know if this is the right way to be doing this?
From my understanding, what you are experiencing is expected. Extremely oversimplifying:
The app gets the external information and uses it to create an external cookie
the external cookie is sent to your app with the assumption that it is just a temporary cookie that will be used to look up any additional local information about the user and then converted to a local [application] cookie
See UseCookieAuthentication vs. UseExternalSignInCookie for a somewhat more thorough breakdown.

Resources