I am not able to store a cookie value in a cookie. Here is my code:
namespace CookieCreation.Controllers
{
public class CookieController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult WriteCookie(String CookieName, String CookieValue, bool IsPersistent)
{
if (IsPersistent)
{
CookieOptions cookies = new CookieOptions();
cookies.Expires = DateTime.Now.AddDays(1);
Response.Cookies.Append(CookieName, CookieValue, cookies);
}
else
{
Response.Cookies.Append(CookieName, CookieValue);
}
ViewBag.Message = ("Sucessful");
return View("Index");
}
public IActionResult ReadCookie()
{
ViewBag.CookieValue = Request.Cookies["CookieName"];
return View();
}
}
}
How do you determine that the cookie is not stored? Use Chrome developer tools, Application/Cookies to see if the cookie is created.
I used your code with the Index.cshtml below and everything worked, cookie is stored and read using your code:
<h1>Index</h1>
<p>Last operation: #ViewBag.Message</p>
#{
using(Html.BeginForm("WriteCookie", "Cookie", new { CookieName = "CookieName", IsPersistent = false}, FormMethod.Post )) {
<input type="text" value="42" name="CookieValue" />
<input type="submit" value="post"/>
}
}
Related
I am trying to implement on a Blazor-Server side application a simple login against LDAP server and use cookie to store user claims. I have the MainLayout set to Authorized, if the user is not authenticated it will be re-direct to Login page. I have already tested the LDAP connection and it works properly, the problem is no matter what I do the cookie doesn't get created in the browser. When I run the POST command I see the HttpStatusCode.OK but the cookie it's not created and the browser re-direct again to login page of course.
Can someone please tell me what am I doing wrong? My code:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
AuthenticationController.cs
[ApiController]
public class AuthenticationController : Controller
{
[HttpPost]
[Route("authentication/login")]
public async Task<ActionResult> Login([FromBody]UserCredentials credentials)
{
string path = "LDAP://serveraddress.xxx";
try
{
using DirectoryEntry entry = new(path, credentials.Username, credentials.Password);
using DirectorySearcher searcher = new(entry);
searcher.Filter = $"(&(objectclass=user)(objectcategory=person)(samaccountname={credentials.Username}))";
var result = searcher.FindOne();
if (result != null)
{
List<Claim> claims = new();
claims.Add(new Claim(ClaimTypes.Name, credentials.Username));
//Get Groups
ResultPropertyCollection fields = result.Properties;
foreach (var group in result.Properties["memberof"])
{
var distinguishedName = new X500DistinguishedName(group.ToString());
var commonNameData = new AsnEncodedData("CN", distinguishedName.RawData);
var commonName = commonNameData.Format(false);
if (!string.IsNullOrEmpty(commonName))
{
claims.Add(new Claim(ClaimTypes.Role, commonName));
}
}
//Get Emails
foreach (var email in result.Properties["mail"])
{
claims.Add(new Claim(ClaimTypes.Email, email.ToString()));
}
ClaimsIdentity claimsIdentity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme);
AuthenticationProperties authProperties = new()
{
AllowRefresh = true,
IssuedUtc = DateTime.Now,
ExpiresUtc = DateTimeOffset.Now.AddDays(1),
IsPersistent = true,
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok();
}
else
{
return NotFound("User Not Found!");
}
}
catch (Exception)
{
return NotFound("Login credentials is incorrect!");
}
}
[HttpPost]
[Route("authentication/logout")]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Ok();
}
}
Login.razor
#page "/login"
#page "/login/{ErrorMessage}"
#layout CenteredBlockLayout
#attribute [AllowAnonymous]
<MudPaper Elevation="25" Class="pa-8" Width="100%" MaxWidth="500px">
<MudItem><img src="/images/logo.svg" alt="Logo" style="width:400px; height:50px;" /></MudItem>
<MudText Typo="Typo.h4" GutterBottom="true">Sign In</MudText>
<MudTextField #bind-Value="#Username" T="string" Label="Username"/>
<MudTextField #bind-Value="#Password" T="string" Label="Password"/>
<MudButton OnClick="(() => PerformLoginAsync())">Sign In</MudButton>
</MudPaper>
#if (!string.IsNullOrEmpty(ErrorMessage))
{
<MudAlert Severity="Severity.Error">#ErrorMessage</MudAlert>
}
Login.razor.cs
public partial class Login
{
public string Username { get; set; }
public string Password { get; set; }
[Parameter]
public string ErrorMessage { get; set; }
[Inject]
HttpClient Client { get; set; }
[Inject]
private NavigationManager NavMan { get; set; }
private async Task PerformLoginAsync()
{
if (!string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password))
{
UserCredentials cred = new UserCredentials
{
Username = Username,
Password = Password
};
var serialized = JsonConvert.SerializeObject(cred);
var stringContent = new StringContent(serialized, Encoding.UTF8, "application/json");
using var result = await Client.PostAsync($"NavMan.BaseUri}authentication/login", stringContent);
if (result.StatusCode == System.Net.HttpStatusCode.OK)
{
NavMan.NavigateTo("/", true);
}
else
{
ErrorMessage = await result.Content.ReadAsStringAsync();
}
}
}
}
I believe you need to append the cookie to the response. I haven't tested this with your code but it should work something like this:
HttpContext.Response.Cookies.Append("my_cookie", claimsString, new CookieOptions()
{
Domain = "mydomain.com",
SameSite = SameSiteMode.Lax,
Secure = true,
Path = "/",
Expires = DateTime.UtcNow.AddDays(1)
}
(These cookie options are just an example, of course. Tailor them to your specific needs.)
Keep in mind that you'll need to convert your claims to a string so that you can store it as the value in a cookie. In our case we store claims in a JWT, so that's what gets stored in the cookie. Here's how I do it:
public string CreateJWT(HttpContext httpContext, User user)
{
var handler = new JwtSecurityTokenHandler();
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName),
new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim(ClaimTypes.Email, user.Email),
}),
Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins),
Issuer = Config.Issuer,
Audience = Config.Audience,
SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256)
};
var token = handler.CreateJwtSecurityToken(descriptor);
var accessToken = handler.WriteToken(token);
httpContext.Response.Cookies.Append("my_cookie", accessToken, new CookieOptions()
{
Domain = Config.CookieDomain,
SameSite = SameSiteMode.Lax,
Secure = true,
Path = "/",
Expires = DateTime.UtcNow.AddMinutes(Config.AccessExpMins)
});
return accessToken;
}
As for parsing the JWT, I'm sure there are a number of ways to go about it. The one that worked for me was this one.
I don't know how/which cookies are coming from which website. Because of that, I can't set manually the cookie names.
How can I get third party cookies to paste to a WKWebview? Here is my code but no chance.
My webview;
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty =
BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(Uri),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
My custom renderer (Shouldn't necessary an event per request? This method fires once in the first request);
[assembly: ExportRenderer(typeof(CustomWebView), typeof(HTMobile.iOS.WebViewRenderer))]
namespace HTMobile.iOS
{
public class WebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
// Cookie
var cookieUrl = new Uri("abc.com");
NSHttpCookieStorage.SharedStorage.AcceptPolicy = NSHttpCookieAcceptPolicy.Always;
var cookieJar = NSHttpCookieStorage.SharedStorage;
cookieJar.AcceptPolicy = NSHttpCookieAcceptPolicy.Always;
foreach (var aCookie in cookieJar.Cookies)
{
cookieJar.DeleteCookie(aCookie);
}
var jCookies = UserInfo.CookieContainer.GetCookies(cookieUrl);
IList<NSHttpCookie> eCookies =
(from object jCookie in jCookies
where jCookie != null
select (Cookie)jCookie
into netCookie
select new NSHttpCookie(netCookie)).ToList();
cookieJar.SetCookies(eCookies.ToArray(), cookieUrl, cookieUrl);
// WebView Instance
webView = new WKWebView(Frame, new WKWebViewConfiguration());
SetNativeControl(webView);
if (e.NewElement != null)
{
Control.LoadRequest(new NSUrlRequest(new NSUrl("abc.com")));
}
}
}
}
}
I think an event should be fired per request and I should be able to get a cookie list for the visited page and then set it to my WebView.
Advise, please.
You can have a try with getting cookie by invoking DecidePolicy method from WKNavigationDelegate .
public class NavigationDelegate : WKNavigationDelegate
{
NSMutableArray multiCookieArr = new NSMutableArray();
public override void DecidePolicy(WKWebView webView, WKNavigationResponse navigationResponse, [BlockProxy(typeof(Action))]Action<WKNavigationResponsePolicy> decisionHandler)
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
WKHttpCookieStore wKHttpCookieStore = webView.Configuration.WebsiteDataStore.HttpCookieStore;
Console.WriteLine("wKHttpCookieStore is :" + wKHttpCookieStore.GetDebugDescription());
wKHttpCookieStore.GetAllCookies(cookies => {
if(cookies.Length > 0)
{
foreach (NSHttpCookie cookie in cookies)
{
//NSHttpCookieStorage.SharedStorage.SetCookie(cookie);
Console.WriteLine("cookie is :" + cookie);
}
}
});
}
else
{
NSHttpUrlResponse response = navigationResponse.Response as NSHttpUrlResponse;
NSHttpCookie[] cookiesAll = NSHttpCookie.CookiesWithResponseHeaderFields(response.AllHeaderFields, response.Url);
foreach (NSHttpCookie cookie in cookiesAll)
{
Console.WriteLine("Here is the cookie inside wkwebview is :" + cookie);
NSArray cookieArr = NSArray.FromObjects(cookie.Name, cookie.Value, cookie.Domain, cookie.Path);
multiCookieArr.Add(cookieArr);
}
Console.WriteLine("cookie is :" + cookiesAll);
}
decisionHandler(WKNavigationResponsePolicy.Allow);
//base.DecidePolicy(webView, navigationResponse, decisionHandler);
}
In addition , using Renderer to Customizing a WebView you can refer to this doc .
public class HybridWebViewRenderer : WkWebViewRenderer
{
public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
{
}
public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
{
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//...
}
if (e.NewElement != null)
{
this.NavigationDelegate = new NavigationDelegat();
}
}
}
I am working on a project with IdentityServer4, Asp.Net.Identity and all is done in MVC and Razor. I have most of it working, the only thing I am struggling with is the RememberMe capability. Currently Idsrv sets the cookie even though RememberMe is false when logging in.
Here is my code:
The LoginModel challenges Idsrv with the oidc Authentication Scheme.
public class LoginModel : PageModel {
public IActionResult OnGet() {
return Challenge(new AuthenticationProperties {
RedirectUri = "/" }, "oidc");
}
}
}
Here is my startup extension method for the client authentication
public static void AddClientAuthentication(this IServiceCollection services) {
services.AddAuthentication(options => {
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options => {
options.SignInScheme = "Cookies";
options.Authority = AuthorityUrl;
options.RequireHttpsMetadata = true;
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.ClientId = "*ClientId*";
options.ClientSecret = "*ClientSecret*";
options.Scope.Add("profile");
options.Scope.Add("openid");
});
}
And here is my login-logic:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model) {
if (!ModelState.IsValid) {
return View(model);
}
SignInResult result = await signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberLogin, true);
if (result.Succeeded) {
ApplicationUser user = await userManager.FindByEmailAsync(model.Email);
await events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
return Redirect("https://localhost:5002/home");
}
The issue is, that cookies are set even though model.RememberLogin is false. Does anyone have a solution for that? Thank you in advance!
So, it was not a bug in my code. I didn't know that there is a cookie set for the duration of the browser session.
(Using ASP.NET Identity 2.1, Microsoft.Owin.Security.Facebook 3.0.1 in a Web API project)
From here: https://developers.facebook.com/docs/facebook-login/login-flow-for-web/v2.2
This is because once someone has declined a permission, the Login Dialog will not re-ask them for it unless you explicitly tell the dialog you're re-asking for a declined permission.
You do this by adding the auth_type: rerequest flag to your FB.login() call:
FB.login(
function(response) {
console.log(response);
},
{
scope: 'user_likes',
auth_type: 'rerequest'
}
);
When you do that, the Login Dialog will re-ask for the declined permission. The dialog will look very much like the dialog in the section on re-asking for permissions but will let you re-ask for a declined permission.
So, using ASP.NET Identity's integration with Facebook login, I know how to pass in the requested scope, but if the user declines the permission, I need to pass in the extra parameter "auth_type" : 'rerequest." How do I do that?
You first add your custom FacebookAuthenticationProvider
public class FacebookProvider : FacebookAuthenticationProvider
{
public override void ApplyRedirect(FacebookApplyRedirectContext context)
{
//To handle rerequest to give some permission
string authType = string.Empty;
if (context.Properties.Dictionary.ContainsKey("auth_type"))
{
authType = string.Format("&auth_type={0}", context.Properties.Dictionary["auth_type"]);
}
//If you have popup loggin add &display=popup
context.Response.Redirect(string.Format("{0}{1}{2}", context.RedirectUri, "&display=popup", authType));
}
}
now in the startup you need to use this provider
var options = new FacebookAuthenticationOptions
{
AppId = "appid",
AppSecret = "secret",
Provider = new FacebookProvider
{
OnAuthenticated = async context =>
{
foreach (var x in context.User)
{
if (x.Key == "birthday")
{
context.Identity.AddClaim(new Claim("dateofbirth", x.Value.ToString()));
}
else
{
context.Identity.AddClaim(new Claim(x.Key, x.Value.ToString()));
}
}
context.Identity.AddClaim(new Claim("fb_accecctoken", context.AccessToken));
await Task.FromResult(context);
}
}
};
options.Scope.Add("public_profile");
options.Scope.Add("email");
options.Scope.Add("user_birthday");
options.Scope.Add("user_location");
app.UseFacebookAuthentication(options);
and finally in your account controller you need to set auth_type when you need
private const string XsrfKey = "xsrfkey";
internal class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null, false)
{
}
public ChallengeResult(string provider, string redirectUri, string userId, bool isRerequest)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
IsRerequest = isRerequest;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public bool IsRerequest { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
if (IsRerequest)
{
properties.Dictionary["auth_type"] = "rerequest";
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
I had the same issue when I wanted to ensure the user had accepted all my permissions. As you probably know this can be detected by calling the /me/permissions url.
So I eventually solved it by simply deleting my app from the user's account.
You can do so by doing a DELETE request on the /me/permissions URL as documented here.
This will remove all permissions you requested from the user, so next time you try authenticating him through Facebook, the prompt appears again.
Facebook requires a public url to POST to in order to deauthorize your app for a user.
I am behind a firewall and do not wish to test this on my production environment, therefore has anyone managed to mimic this POST in a c# (or other) integration test?
It would involve building up a form with a field signed_request and encrypted to base64 and the posting it to my local web app to handle...
Here is how I do it in PHP:
<?php
function generateSignedRequest($data, $secret) {
$payload = base64UrlEncode(json_encode($data));
$signature = base64UrlEncode(hash_hmac('sha256', $payload, $secret, $raw = true));
return "$signature.$payload";
}
function base64UrlEncode($input) {
$input = rtrim(base64_encode($input), '=');
return strtr($input, '+/', '-_');
}
$app_secret = "YOUR_APP_SECRET_HERE";
$data = array(
'algorithm' => 'HMAC-SHA256',
'issued_at' => 1332162396,
'user' => array(
'country' => 'bg',
'locale' => 'en_US'
),
'user_id' => '100003387159594'
);
$signed_request = generateSignedRequest($data, $app_secret);
?>
<form action="deauthorize_callback.php" method="post">
<input type="text" name="signed_request" value="<?php echo $signed_request?>" size="255" />
<input type="submit" value="Send Request" />
</form>
If you have PHP in your local environment you can try submiting the form to your backend and see if it works. You just have to replace your app secret and user id in the data array.
Hope this helps, it worked for me so far.
I managed to find something similar for building the payload in Ruby which I've converted, here it is with an integration test:
This class builds the string that you will POST as request.form["signed_request"]
public class SignedRequest
{
public static string SignedRequestFor(string applicationSecret, string json)
{
// encode payload
var payload = ToUrlBase64String(json);
// create sig
var Hmac = SignWithHmac(UTF8Encoding.UTF8.GetBytes(payload), UTF8Encoding.UTF8.GetBytes(applicationSecret));
var HmacBase64 = ToUrlBase64String(Hmac);
// concat and return
return HmacBase64.Concatenate(".", payload);
}
private static byte[] SignWithHmac(byte[] dataToSign, byte[] keyBody)
{
using (var hmacAlgorithm = new HMACSHA256(keyBody))
{
hmacAlgorithm.ComputeHash(dataToSign);
return hmacAlgorithm.Hash;
}
}
private static string ToUrlBase64String(string input)
{
byte[] encodedBytes = Encoding.UTF8.GetBytes(input);
return ToUrlBase64String(encodedBytes);
}
private static string ToUrlBase64String(byte[] input)
{
return Convert.ToBase64String(input).Replace("=", String.Empty)
.Replace('+', '-')
.Replace('/', '_');
}
}
Small value object used by the test:
public class RequestEnvelope
{
public long user_id { get; set; }
public string algorithm = "HMAC-SHA256";
}
The test:
[Test]
public void CanDeauthorizeUser()
{
var appSecret = ConfigurationManager.AppSettings["NewsletterSubscriptionFacebookAppSecret"];
var envelope = new RequestEnvelope { user_id = 123456 };
var signedRequest = SignedRequest.SignedRequestFor(appSecret, JsonConvert.SerializeObject(envelope));
var postData = "signed_request=" + signedRequest;
byte[] postBytes = Encoding.UTF8.GetBytes(postData);
var request = WebRequest.Create(ConfigurationManager.AppSettings["AppWebUrl"] + "/SubscriptionHandler.ashx?a=deactivate");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = postBytes.Length;
using (var newStream = request.GetRequestStream())
{
// Send the data.
newStream.Write(postBytes, 0, postBytes.Length);
newStream.Close();
}
// Get the response.
using (var response = request.GetResponse())
{
var status = ((HttpWebResponse) response).StatusDescription;
// Get the stream containing all content returned by the requested server.
using (var dataStream = response.GetResponseStream())
{
// Open the stream using a StreamReader for easy access.
using (var reader = new StreamReader(dataStream))
{
// Read the content fully up to the end.
string responseFromServer = reader.ReadToEnd();
Console.WriteLine(responseFromServer);
Console.WriteLine("status: " + status);
}
}
}
}
Snippet from the Handler that I post to:
string signedRequestValue = HttpContext.Current.Request.Form["signed_request"];
var items = SignedRequestParser.Parse(ConfigurationManager.AppSettings["NewsletterSubscriptionFacebookAppSecret"], signedRequestValue);
And the code for parsing that (taken/modified from the codeplex facebook API):
public class SignedRequestParser
{
/// <summary>
/// Parses the signed request string.
/// </summary>
/// <param name="appSecret">The unique app hash known the provider and the developer</param>
/// <param name="signedRequestValue">The encoded signed request value.</param>
/// <returns>The valid signed request.</returns>
public static Dictionary<string, string> Parse(string appSecret, string signedRequestValue)
{
Check.Require(!String.IsNullOrEmpty(signedRequestValue));
Check.Require(signedRequestValue.Contains("."), "Invalid Signed Request");
string[] parts = signedRequestValue.Split('.');
var encodedValue = parts[0];
if (String.IsNullOrEmpty(encodedValue))
{
throw new InvalidOperationException("Invalid Signed Request");
}
var sig = Base64UrlDecode(encodedValue);
var payload = parts[1];
using (var cryto = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(appSecret)))
{
var hash = Convert.ToBase64String(cryto.ComputeHash(Encoding.UTF8.GetBytes(payload)));
var hashDecoded = Base64UrlDecode(hash);
if (hashDecoded != sig)
{
throw new InvalidOperationException("Invalid Signed Request.");
}
}
var payloadJson = Encoding.UTF8.GetString(Convert.FromBase64String(Base64UrlDecode(payload)));
var data = JsonConvert.DeserializeObject<Dictionary<string, string>>(payloadJson);
return data;
}
/// <summary>
/// Converts the base 64 url encoded string to standard base 64 encoding.
/// </summary>
/// <param name="encodedValue">The encoded value.</param>
/// <returns>The base 64 string.</returns>
private static string Base64UrlDecode(string encodedValue)
{
Check.Require(!String.IsNullOrEmpty(encodedValue));
encodedValue = encodedValue.Replace('+', '-').Replace('/', '_').Trim();
int pad = encodedValue.Length % 4;
if (pad > 0)
{
pad = 4 - pad;
}
encodedValue = encodedValue.PadRight(encodedValue.Length + pad, '=');
return encodedValue;
}
}