I do have an Asp.Net MVC Application (version 6.0.0-rc1-final) with custom role and user stores. After some struggling I finally could create a working login mechanism. However I do have now troubles to create a clean logout. What my logout code in the controller currently looks like:
public async Task<ActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
await SignInManager.SignOutAsync();
}
return RedirectToAction("Index", "App");
}
The problem with this code is, that one cookie is not deleted: .AspNet.Microsoft.AspNet.Identity.Application
As long as I don't delete the cookie manually the application is in a dirty state and throws null pointer exceptions because User.Identity is null.
I have found a question on stackoverflow describing a similar scenario. But the solution there is not appropriate for me because I am using MVC 6 which does not have System.Web any more.
I do also have a sample solution which just works fine. In this solution the mentioned cookie is never created. Perhaps the right solution is not to delete the cookie after logout, but rather to prevent somehow the creation of the cookie.
The problem is that your RedirectToAction overwrites the redirect to the Identity Server endsession URL that SignOutAsync issues.
(The same explanation for the same problem is given here by Microsoft's HaoK.)
Edit: The solution is to send a redirect URL in an AuthenticationProperties object with the final SignOutAsync:
// in some controller/handler, notice the "bare" Task return value
public async Task LogoutAction()
{
// SomeOtherPage is where we redirect to after signout
await MyCustomSignOut("/SomeOtherPage");
}
// probably in some utility service
public async Task MyCustomSignOut(string redirectUri)
{
// inject IHttpContextAccessor to get "context"
await context.SignOutAsync("Cookies");
var prop = new AuthenticationProperties()
{
RedirectUri = redirectUri
});
// after signout this will redirect to your provided target
await context.SignOutAsync("oidc", prop);
}
I could fix the dirty state of my application after the logout by manually delete the cookie after the logout action:
public async Task<ActionResult> Logout()
{
if (User.Identity.IsAuthenticated)
{
await SignInManager.SignOutAsync();
}
foreach (var key in HttpContext.Request.Cookies.Keys)
{
HttpContext.Response.Cookies.Append(key, "", new CookieOptions() { Expires = DateTime.Now.AddDays(-1) });
}
return RedirectToAction("Index", "App");
}
As cookies cannot deleted from the server directly I just overwrite the existing cookies with an already passed expiry date.
In addition to everything already mentioned, also make sure you are not omitting the scheme argument in the calls to SignInAsync and SignOutAsync, and that you are passing the same value to both. For example:
HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
and
HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
So in this example the scheme is CookieAuthenticationDefaults.AuthenticationScheme. In my case I was forgetting to pass this to SignOutAsync, and while obvious after the fact, it took longer than I'd like to admit for me to track down.
Another gotcha that could leave the identity server cookies on the client is a logout failure. One typical cause of logout failures is a misconfiguration of the client’s PostLogoutRedirectUris.
The logout failures are not visible from the client side, the endsession call returns 200 OK, as well as the logout call.
There will however be traces on your identity server logs that the logout failed.
Related
I am trying to understand this condition in IdentityServer4 quickstart:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
if (button != "login")
{
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (context != null)
{
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
return Redirect(model.ReturnUrl);
}
else
{
return Redirect("~/");
}
}
As far as I understand, if login form is not submitted by pressing login button (<button type=submit value=login>) but by another post request (?) what exactly is going to happen?
What is GetAuthorizationContextAsync doing? I think it may extract some Authorization code from Query string and Authorize. Correct?
Thanks!
The QuickStart example contains also comments in the code that explain what the method is doing:
if (button != "login")
{
// the user clicked the "cancel" button
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
Authorization context is described in documentation:
IdentityServer will pass a returnUrl parameter (configurable on the
user interaction options) to the consent page which contains the
parameters of the authorization request. These parameters provide the
context for the consent page, and can be read with help from the
interaction service. The GetAuthorizationContextAsync API will return
an instance of AuthorizationRequest.
This trick with the named button value is a commonly used trick to have multiple buttons to submit the same form. Either clicking cancel or login button will trigger submission of the login form, but the handling of the submission will be handled differently.
For your second question: this related to the configured clients within the IdentityServer configuration. Based on the return URL, the correct client is retrieved from the IdentityServer configuration. While getting this context, there is also validation triggered to see if the return URL is a known configured return URL.
This is later used to determine the correct ClientId, and wether PKCE validation is required or not for the configured client, to properly handle the login request (either cancelled, or not).
This is essentially the same question as this one:
ASP.NET MVC - Preserve POST data after Authorize attribute login redirect except it isn't asked 7 years ago, and it's about ASP.NET Core, which is pretty different. I am using the [Authorize] attribute to do my most basic access authentication, really just to check to see if there is a user logged in at all. If not, then it kicks them back to the login page. Here's the services setup for that.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "loginId";
});
services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/Logout");
Here is my Logout action.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Logout(string returnUrl = "/cgi-s/admin.cgi")
{
await HttpContext.SignOutAsync();
if (HttpContext.Request.Cookies.TryGetValue("loginId", out string loginId))
{
User auth = _context.Users.Where(a => a.LoginId.Equals(loginId)).FirstOrDefault();
if (auth != null)
{
auth.LoginId = string.Empty;
await _context.SaveChangesAsync();
}
}
return Redirect("/Account/Login?returnUrl="+returnUrl);
}
Right now I am just using the default behavior with a return url string to get back to the attempted page after a successful login. Is there a way to set this up so that POST data is also preserved?
I've tried a couple different things, like a custom middleware that stores post data which then gets retrieved on login, but I haven't come up with anything that I haven't found security holes in afterward.
Is there an easy way to do this that I'm just missing?
Thanks.
PS. Please ignore the weirdness going on in the Logout action. We are a two man team working on a 20 year old Perl CGI site, slowly transitioning over to .NET while trying to also keep up with new features and bug fixes, so everything is weird while we run Perl CGI alongside some .NET code on IIS with Postgres. Hopefully we will eventually get everything transitioned over.
Have you tried httpContext.Request.EnableBuffering?
See this question here and be sure to look at the comments: Read the body of a request as string inside middleware
httpContext.Request.EnableBuffering();
using(StreamReader streamReader = new StreamReader(httpContext.Request.Body,
...,leaveOpen: true))
{
//Do something here:
httpContext.Request.Body.Position = 0;
}
I'm presently developing an application with an ASP.NET Core (2.1) API server/backend and an Angular Single Page Application frontend.
For user authentication, I'm using ASP.NET Core Identity, along with several of the default methods provided within AccountController for login, logout, register, forgot password, etc. My Angular application accesses this API to perform these actions.
The Angular client files are hosted directly out of wwwroot in the ASP.NET Core application. I am hosting using IIS Express (I have tried IIS as well to no avail) under localhost:5000.
Authentication is configured in ASP.NET Core's Startup class with the following:
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = false;
config.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
// Prevent API from redirecting to login or Access Denied screens (Angular handles these).
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.CompletedTask;
};
});
Register and Forgot Password both work without a hitch. The API is called and the appropriate actions are taken.
Login appears to work: The method is invoked within Angular:
#Injectable()
export class AuthService extends BaseService {
constructor(private httpClient: HttpClient) {
super(httpClient, `${config.SERVER_API_URL}/account`);
}
// Snip
login(login: Login): Observable<boolean> {
return this.httpClient.post<boolean>(`${this.actionUrl}/Login`, login)
.catch(this.handleError);
}
// Snip
}
The ASP.NET Core API gets the call, logs the user in, and returns success to indicate to Angular that login worked and they should proceed to redirect the user:
namespace ApartmentBonds.Web.Api.Account
{
[Route("api/[controller]/[action]")]
public class AccountController : APIControllerBase
{
// Snip
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginViewModel model, string returnUrl = null)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberMe, false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return Ok(true);
}
}
return Ok(false);
}
// Snip
}
}
The session cookie is created successfully, and I can see it within the browser (using Firefox Quantum in this screenshot, but have also tried in IE, in Firefox incognito, etc):
To test, I have another API method which simply returns whether or not the calling user is signed in:
// Angular
isLoggedIn(): Observable<boolean> {
return this.httpClient.get<boolean>(`${this.actionUrl}/IsLoggedIn`)
.catch(this.handleError);
}
// ASP.NET Core API
[HttpGet]
[AllowAnonymous]
public IActionResult IsLoggedIn()
{
return Ok(_signInManager.IsSignedIn(User));
}
I make this call within the browser, and it returns false. (All other aspects of the site that depend on the user being logged in also show the user is not logged in -- this method is just a quick and dirty way to verify this problem.) Note that the client is correctly sending the Identity cookie along with the request:
Since the only site in question here is localhost:5000 I'm not sure why this is not working.
Things I've tried
Within services.ConfigureApplicationCookie
Setting options.Events.Cookie.Domain to localhost:5000 explicitly
Setting options.Events.Cookie.Path to /api
Clearing all cookies and trying incognito/other browsers (to rule out other localhost site cookies potentially clobbering the request)
Hosting the site via IIS (localhost:8888) after Publishing the Core Application
Anyone have any ideas?
In order for authentication handlers to run on the request, you need app.UseAuthentication(); in the middleware pipeline (before middleware that requires it like MVC).
This is a longshot, but maybe the token needs: "Bearer " (with the space) in front of the actual token.
I think you shouldn't use User property from controller when you are using AllowAnonymousAttribute.
Try to do request to some authorize endpoint. If you are not sign in, then you will get status code 401(Unathorized).
I'm working on a .NET Core 1.1 ASP.NET app with cookies authentication and I noticed something a bit weird. I'm noticing that when I hit my Login page it says that I'm not authenticated even if though that's the only thing the action does. If I refresh the page or navigate to another page I'm authenticated. It almost seems as if the view is returned before the async sign in happens.
Controller Method
public async Task<IActionResult> Login()
{
ClaimsPrincipal userPrincipal = new ClaimsPrincipal(/*Setup the principal*/);
await HttpContext.Authentication.SignInAsync("Cookie", userPrincipal, new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMinutes(5),
IsPersistent = false,
AllowRefresh = false
});
return View();
}
View
#if (this.User.Identity.IsAuthenticated == true)
{
<span>You're authenticated!</span>
}
else
{
<span>You're not authenticated.</span>
}
If I navigate to /Controller/Login I get that I'm not authenticated, but if I go to another view with the same code it will then say I'm authenticated. This isn't a big issue but I'm worried I may have set something up incorrectly.
This is by design. You must perform a redirect, such as RedirectToAction, after the call to SignInAsync since the User object will not be reconstructed until the next request to the server.
We have an old classic asp app that is used to manage and launch our other web applications.
The way it launches apps is as follows:
<form name="frmMain" action="http://xxxx/mvc3app/Index" target=_self method=post>
<script language="javascript">
frmMain.submit();
</script>
User login and password is passed through as part of the request.
To Authenticate the user in the ASP.NET app I call the following AuthenticateUser function:
public bool AuthenticateUser()
{
var userName = Context.Request["txtName"];
var password = Context.Request["txtPassword"];
if (Membership.ValidateUser(userName, password))
{
FormsAuthentication.SetAuthCookie(userName, true);
}
}
I assumed that the correct place to call AuthenticateUser would be in the Session_Start() method in global.asax but it doesn't seem that this method is called when submitting "frmMain". It seems to work intermittently - if I close IE completely, try again and then enter the URL manually.
void Session_Start(object sender, EventArgs e)
{
Log("In Session Start");
AthenticateUser();
}
Where would be the correct place to in my ASP.NET app to authenticate users?
Here is a screeny from dev tools of the forms auth failing - Session_Start() isn't called.
EDIT
Looks like this wasn't working because the IsAuthenticated property is only set on subsequent requests which was causing auth to fail on the index action.
I'll test this now but see Who sets the IsAuthenticated property of the HttpContext.User.Identity
Solution:
First Error was not redirecting after calling SetAuthCookie which was causing the Index view to fail auth.
I also realised there is no need to place this in global.asax but I could rather redirect to a LogOn action instead of going directly to the index action:
public ActionResult LogOn()
{
var userName = Context.Request["txtName"];
var password = Context.Request["txtPassword"];
if (Membership.ValidateUser(userName, password))
{
FormsAuthentication.SetAuthCookie(userName, false);
return RedirectToAction("Index", "Index");
}
else
{
return RedirectToAction("IncorrectLogin", "Index");
}
}
I think a controller action would be the best place, its a good idea to keep controller actions to a minimum so it doesn't get bloated. But if the action isn't touching too many layers it seems like a good fit.
If you were doing any "weird" stuff with session manipulation you can still use Session_Start though. But avoid it if you can, nothing like magic happening all over the place to confuse you on your own application exec path :D