I'm building a web api and I have a method in my controller which gives the user a cookie. I can see it in the browser it is set, everything is fine.
[HttpGet]
[Route("[controller]/cookie")]
public IActionResult Cookie()
{
string cookieName = "av225461";
string key = $"blahblah";
HttpContext.Response.Cookies.Append(
cookieName, key,
new CookieOptions() { SameSite = SameSiteMode.Unspecified, HttpOnly = true, Expires =
DateTime.UtcNow.AddMinutes(15)/*, Secure = true*/ });
return Ok("");
}
But I am not able to read it in my Get method, if I am sending a request to my controller. The Cookies.Count is 0. Even if the cookie is set in browser and inthe requestheader of Firefox.
if (Request.Cookies.Count > 0)
{
//some code here
}
With postman sometimes it worked, and sometimes not. Someone an idea?
Related
I simply want to save cookies containing security tokens, but they are not persisted in the browser. Here is my AuthController method for saving the cookie (simplified):
[AllowAnonymous]
[HttpPost("authorize")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> AuthorizeAsync()
{
//generating access token ommited for brevity
SetTokenCookie(accessToken);
return Ok(userIdentity);
}
And SetTokenCookie method:
private void SetTokenCookie(string accessToken)
{
var options = _jwtOptions.AccessToken;
var cookieOptions = new CookieOptions()
{
HttpOnly = true,
IsEssential = true,
Secure = false,
SameSite = SameSiteMode.Strict,
Domain = options.Issuer, //http://localhost:3394 by default
Expires = DateTime.UtcNow.AddDays(14)
};
Response.Cookies.Append(options.Name, accessToken, cookieOptions);
}
Now when I analyze the response from my Api, I see that Set-Cookie header and token itself are fine:
Decoded token:
{
"id": "70132f61-4d83-4772-9685-7a77a9204685",
"name": "admin",
"email": "xyz#xyz.pl",
"role": "Administrator",
"persist": "True",
"nbf": 1646336045,
"exp": 1646336945,
"iat": 1646336045,
"iss": "http://localhost:3394",
"aud": [
"blog.webgateway",
"blog.blogging",
"blog.comments",
"blog.users"
]
}
But when I check the cookies, nothing is saved.
I know that there are many topics related to this issue, but I already ran out of ideas I could find online:
I wiped storage in my browser for localhost
I added custom entry in my hosts file and changed cookie domain accordingly
I tried setting cookie domain = null or different path
I added Cors and allowed credentials, any methods, any headers, any origin
I tried more permissive settings in my browser
I tried changing cookie options (HttpOnly, Secure, SameSite)
I removed UseHttpsRedirection() from my Startup.cs and made sure I connet via HTTP
Nothing seems to work. I'm using Firefox 97.0.1. Do you know what else I could try?
Did you try to change the Domain to localhost?
Per my test, using `` didn't work for me, and then I found that other cookies showed they belong to domain localhost, so I use localhost instead, then I can see the newly created cookie. I test to call the api by tools in chrome, so I think it should be similar to your scenario.
public string saveCookie() {
var cookieOptions = new CookieOptions()
{
HttpOnly = true,
IsEssential = true,
Secure = false,
SameSite = SameSiteMode.Strict,
Domain = "localhost", //using https://localhost:44340/ here doesn't work
Expires = DateTime.UtcNow.AddDays(14)
};
Response.Cookies.Append("testCookie", "Cookie content", cookieOptions);
return "hello world";
}
I've finally managed to solve the issue... Here are the steps I've made:
Changed the controller method to HttpGet, so it just looks like this now:
[AllowAnonymous]
[HttpGet("authorize")] // <-- notice the difference
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<IActionResult> AuthorizeAsync()
{
//generating access token ommited for brevity
SetTokenCookie(accessToken);
return Ok(userIdentity);
}
For some reason calling a Post request directly from the browser (Firefox at least in my case) doesn't seem to work for setting the cookies, even if the response looks fine, but when I changed it to Get method and accessed in a standard way (URL) it works. I will have to double-check if Post method works from the client (JavaScript).
Apply above solution from Tiny Wang, which means changing the cookie domain to localhost. Having the full URL indeed prevent cookie from being saved.
Edit: As pointed out by SSchmid in the comments, changing method to "Get" was only a temporary workaround while using Development Tools in Firefox. For further development or production it is not recommend to keep it as a "Get" method.
I managed to get it running by using the [FromBody] Attribute.
This works for me :
[HttpPost("Login")]
public async Task<IActionResult> Login([FromBody] LoginData data)
{
var user = await userManager.FindByEmailAsync(data.Email);
if (user == null || !await userManager.CheckPasswordAsync(user, data.Password))
return Unauthorized("Invalid Authentication");
await signInManager.SignInAsync(user, data.rememberMe);
var roles = (await userManager.GetRolesAsync(user)).ToList();
var sUser = new Models.User(user, roles);
return Ok(sUser);
}
apparently if u use HTTPPOST in combination with parameters it doesnt work.
Let me put the problem with a bit of structure.
Context
We have a web application build with Web Forms and hosted in an Azure Web App that authenticates the users against an Azure Active Directory using the OWIN + OpenId Connect standards.
The authentication process works like a charm and users are able to access the application without any problem.
So, whats the issue?
After struggling for many days with it I'm unable to pass any query string parameter to the application through the authentication process. For example, if I try to access the application for the first time through the URL: https://myapp.azurewebsites.net/Default.aspx?param=value. The reason I need to pass this parameter is that it triggers some specific actions in the main page.
The problem is that after the authentication redirects to the webapp's main page the original query string parameters of the request are gone.
The code
The startup class looks like this:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = Constants.ADTenant.ClientId,
Authority = Constants.ADTenant.Authority,
PostLogoutRedirectUri = Constants.ADTenant.PostLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(Constants.ADTenant.ClientId,
Constants.ADTenant.AppKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
Constants.ADTenant.ObjectIdClaimType).Value;
AuthenticationContext authContext = new AuthenticationContext(Constants.ADTenant.Authority,
new NaiveSessionCache(userObjectID));
if (HttpContext.Current != null)
{
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential,
Constants.ADTenant.GraphResourceId);
AuthenticationHelper.token = result.AccessToken;
AuthenticationHelper.refreshToken = result.RefreshToken;
}
return Task.FromResult(0);
}
}
});
And it works properly!
What I already tried
I've got access to the original request Url by adding an overwrite of the RedirectToIdentityProvider notification:
RedirectToIdentityProvider = (context) =>
{
// Ensure the URI is picked up dynamically from the request;
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.RedirectUri = appBaseUrl;
return Task.FromResult(0);
}
With this I tried to force the redirect to the main page including the original query string parameter, but then the redirection after authentication breaks and gets stuck in an infinite loop.
I've also tried with changing the redirect url of the application configuration in Azure AD without luck. Also tried to store the query string parameters somewhere else, but the Session is not accessible that early in the process.
Does anyone know what am I doing wrong? Or I'm just asking for something impossible? Any help would be appreciated.
Thank you very much in advance!
I recently had a need to do the exact same thing. My solution may not be the most sophisticated, but simple isn't always bad either.
I have two Authentication Filters...
The first filter is applied to all controllers that could potentially be hit with query string parameters prior to authorization. It checks if the principal is authenticated. If false it caches the complete url string in a cookie. If true it looks for any cookies present and clears them, just for cleanup.
public class AuthCheckActionFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if (!filterContext.Principal.Identity.IsAuthenticated)
{
HttpCookie cookie = new HttpCookie("OnAuthenticateAction");
cookie.Value = filterContext.HttpContext.Request.Url.OriginalString;
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
else
{
if (filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
cookie.Expires = DateTime.Now.AddDays(-1);
filterContext.HttpContext.Response.Cookies.Add(cookie);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
The second filter is applied only to the default landing page, or in other words where the identity server is redirecting after login. This second filter looks for a cookie and if it exists it calls response.Redirect on cookie value.
public class AutoRedirectFilter : ActionFilterAttribute, IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
if(filterContext.Principal.Identity.IsAuthenticated)
{
if(filterContext.HttpContext.Request.Cookies.AllKeys.Contains("OnAuthenticateAction"))
{
HttpCookie cookie = filterContext.HttpContext.Request.Cookies["OnAuthenticateAction"];
filterContext.HttpContext.Response.Redirect(cookie.Value);
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
I'm working on a REST API Project where users log in and make calls. In order to do that, i create a cookie where i encrypt the username. My server is deployed and something really weird is going on. From time to time i just don't receive the cookies in the response. In this case I just have to make any modification in the web.config file and it starts working again... I really don't understand why... Any ideas ?
Here's my login code :
[Route("login", Order = 1)]
[HttpPost]
[HttpGet]
public async Task<HttpResponseMessage> Login([FromUri] string userId, [FromUri] string userPassword)
{
try
{
Tuple<string, string> result = userService.Authenticate(userId, userPassword);
string sessionIds = result.Item1;
string message = result.Item2;
CookieHeaderValue cookie = CreateSessionsCookie(sessionIds);
cookie.Secure = true;
// Store username for later use
CookieHeaderValue userCookie = new CookieHeaderValue(Strings.Id, Encryption.Protect(userId, Strings.Id));
userCookie.Secure = true;
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, JsonConvert.DeserializeObject(message));
response.Headers.AddCookies(new CookieHeaderValue[] { cookie, userCookie });
return response;
}
catch (Exception ex)
{
return HandleException(ex);
}
}
It is a bit of an anti-pattern to use a cookie for a restful web service. Just include the username in the header instead.
As to why this is timing out I suspect it has to do with your session timing out.
I am calling a void function using jquery ajax in mvc3. In that function when the Session is out then also it will come to success function of ajax. I need to know whether the Session is available or not before sending the request or inside the success function of ajax.
controller Action:
protected override void Save(Query query, string queryTitle)
{
}
Why not catch the expiry of the session on the server, return an HTTP 401 Unauthorized, then check for this response in jquery and pop up a "Your session has expired, please log in again" page?
Detecting Session expiry on ASP.NET MVC
How to set HTTP status code from ASP.NET MVC 3?
How do I get the HTTP status code with jQuery?
The code you need on the initial server call is:
protected void Save(Query query, string queryTitle)
{
// would probably be better to refactor this bit out into its own method
string sCookieHeader = Request.Headers["Cookie"];
if (Context.Session != null
&& Context.Session.IsNewSession
&& sCookieHeader != null
&& sCookieHeader.IndexOf("ASP.NET_SessionId") >= 0)
{
// session has expired
if (Request.IsAuthenticated)
{
FormsAuthentication.SignOut();
}
Response.StatusCode = 401
}
else
{
// we're authenticated, so do the save
}
}
and on the client:
$.ajax(serverUrl, {
data: dataToSave,
statusCode: {
200: function(response) {
// all good, continue
401: function (response) {
// session expired!
// show login box
// make ajax call to reauthenticate
// call save method again
},
});
Your reauthentication call would look something like this:
public ActionResult Reauthenticate(username, password)
{
if (IsValidUser(username, password))
{
// sometimes used to persist user roles
string userData = string.Join("|",GetCustomUserRoles());
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, // ticket version
username, // authenticated username
DateTime.Now, // issueDate
DateTime.Now.AddMinutes(30), // expiryDate
isPersistent, // true to persist across browser sessions
userData, // can be used to store additional user data
FormsAuthentication.FormsCookiePath); // the path for the cookie
// Encrypt the ticket using the machine key
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
// Add the cookie to the request to save it
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
cookie.HttpOnly = true;
Response.Cookies.Add(cookie);
}
}
(Adapted from How to set HTTP status code from ASP.NET MVC 3?)
Why not try this ?
public void Save()
{
if (Session.IsNewSession)
{
throw new Exception("This session was just created.");
}
//Go on with save matter...
}
This should return a status 500 on your AJAX function and should cause the response to fall in the fail method you defined.
Another way is to setInterval() on the client that continually sends a dummy request to the server to keep the session alive, at least when the user is editing. This might be the best way to prevent them user from losing work. You could also use this to detect loss connectivity.
When the page is loaded first time , pass the current SessionId to the client side and assign to the local javascript variable.Next have a method which will return the current SessionId before making the Save method call from the AJAX , compare the local variable against the Session Id you have received.
public string GetCurrentSessionId(){
return HttpContext.Current.Session.SessionId;
}
Javascript function
$.ajax({
Url : 'GetCurrentSessionId',
success : function(result){
if(result === "LOCALSESSIONID")
CALL THE SAVE METHOD
else
alert('Session is expired');
}
});
Working on ACS SSO, and the signout process works perfectly fine. The problem is users remain on the same page that calls the logout action, no matter what I have it set to redirect to
public ActionResult Logout()
{
//Load identity configuration
FederationConfiguration config = FederatedAuthentication.FederationConfiguration;
//Get wtrealm from WSFederationConfiguration Section
string wtrealm = config.WsFederationConfiguration.Realm;
string wreply = wtrealm;
//Read ACS Ws-Federation endpoint from web.config
string wsFederationEndpoint = ConfigurationManager.AppSettings["ida:Issuer"];
SignOutRequestMessage signoutRequestMessage = new SignOutRequestMessage(new Uri(wsFederationEndpoint));
signoutRequestMessage.Parameters.Add("wreply", wreply);
signoutRequestMessage.Parameters.Add("wtrealm", wtrealm);
string signoutUrl = signoutRequestMessage.WriteQueryString();
FederatedAuthentication.WSFederationAuthenticationModule.SignOut();
return this.Redirect(signoutUrl);
}
To execute this action, a page has a click handler
$('#logout').click(function () {
$.post('#Url.Action("Logout", "Home", new { area = "" })');
});
A click is correctly handled, the Logout action is called and executed, but the site remains on the calling page. The network log shows the call to logout, but no evidence that the Redirect is even being attempted. In an effort to debug, I have commented out the Redirect to signoutURL and tried a RedirectToAction, a RedirectToRoute, and a javascript call to set window.location, none of which seem to fare any better at transferring the user to a different page. What am I missing?
What I ended up doing was moving the Logout logic into a Model class
public class LogoutHandler
{
public string Signout()
{
// Load Identity Configuration
FederationConfiguration config = FederatedAuthentication.FederationConfiguration;
// Get wtrealm from WsFederationConfiguation Section
string wtrealm = config.WsFederationConfiguration.Realm;
string wreply;
wreply = wtrealm;
// Read the ACS Ws-Federation endpoint from web.Config
string wsFederationEndpoint = ConfigurationManager.AppSettings["ida:Issuer"];
SignOutRequestMessage signoutRequestMessage = new SignOutRequestMessage(new Uri(wsFederationEndpoint));
signoutRequestMessage.Parameters.Add("wreply", wreply);
signoutRequestMessage.Parameters.Add("wtrealm", wtrealm);
FederatedAuthentication.SessionAuthenticationModule.SignOut();
return signoutRequestMessage.WriteQueryString();
}
}
And having my logout action call that class to return the URL, and then Redirect to it. All worked well then, though I don't know if there is anything to take into account with mobile
public ActionResult Logout()
{
string signoutUrl = new LogoutHandler().Signout();
return Redirect(signoutUrl);
}