locked
anonymous User with password-protected link in asp.net core 3.1 - is ClaimsPrinciple/cookie the best way to do this? RRS feed

  • Question

  • User-1632036263 posted

    ok - so im trying to do something like vimeo.com
    where a private video can be accessed by just inputting a password
    so for example if you go here:
    https://vimeo.com/392083444
    you get a simple password box and submit button

    i came to the conclusion to use claims
    since the user is anonymous - i didnt want to use Identity
    in addition the video password is saved in the db with the video metadata

    oh and btw just like vimeo or youtube -
    there is a proper Identity setup bc the profile is governed by a proper Identity login

    so the first question is:
    is going with ClaimsPrinciple the best strategy to do this?
    or am i making too much out of it ?
    i mean pre-Core i would have gone with session vars but thats not a thing now in core

    heres what ive got so far

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> LinkLogin([Bind("ID, Guid, Password")] LinkLoginVM vm)
        {
            if (String.IsNullOrEmpty(vm.Password))
            {
                return ViewComponent("Error", new { errorcode = 1});
            }
            var c = await db.Vids.SingleAsync(c => c.Guid == vm.Guid);
    
            // create and add guid
            if (ModelState.IsValid)
            {
                if (vm.Password == c.Password)
                {
                    // give user a claim
                    ApplicationUser user = await um.GetUserAsync(HttpContext.User);  <-- this doesnt really return anything
                    var claims = new List<Claim>() { 
                        new Claim(ClaimTypes.Name, "Password Guest"),
                        new Claim(JwtRegisteredClaimNames.Sub, vm.Guid),
                        new Claim(JwtRegisteredClaimNames.AuthTime, DateTime.Now.ToString())
                    };
    
                    // not sure what im doing here
                    var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
                    await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProps);
                }
            }
            else
            {
                // put debugger here if problematic
                Console.WriteLine("ERR: ModelState not valid");
                var errors = ModelState
                    .Where(x => x.Value.Errors.Count > 0)
                    .Select(x => new { x.Key, x.Value.Errors })
                    .ToArray();
            }
    
            return RedirectToAction("Vids", new { id = vm.Guid });
        }

    in my startup im sure i messed something up -
    cause i feel like all im reading a bunch of spaghetti code in the articles
    and with the constant version changes even some articles from a year ago are out of date

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
            {
                options.LoginPath = "/View/LinkLogin/";
                options.LogoutPath = "/Account/Logout/";
                //options.Cookie.ExpireTimeSpan = TimeSpan.FromMinutes(30);
                options.Cookie.HttpOnly = true;
                options.Cookie.SecurePolicy = environment.IsDevelopment() ? Microsoft.AspNetCore.Http.CookieSecurePolicy.None : Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
                options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
            });
    
            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.ExpireTimeSpan = TimeSpan.FromMinutes(35);
    
                //could be - 
                //options.LoginPath = "/Identity/Account/Login";
                //options.LogoutPath = "/Identity/Account/Logout";
                //options.AccessDeniedPath = "/Identity/Account/AccessDenied";
                options.LoginPath = $"/Identity/Account/Login";
                options.LogoutPath = $"/Identity/Account/Logout";
                options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
                options.SlidingExpiration = true;
            });
    
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
                .AddRazorPagesOptions(options =>
                {
                    // deprecated  in3.1?
                    // options.AllowAreas = true;
                    options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
                    options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
                    options.Conventions.AuthorizeFolder("/View");
                });

    and then later

            // routing and security
            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>  etc...

    im referencing these articles:
    https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1
    https://www.yogihosting.com/aspnet-core-identity-claims/
    https://www.red-gate.com/simple-talk/dotnet/net-development/using-auth-cookies-in-asp-net-core/

    the claims get processed but they dont get stored
    is it even possible to store these claims with an anonymous user
    if yes where should i be looking for them with an anonymous user?
    if no what should i be doing next?

    Monday, March 23, 2020 2:33 AM

Answers

  • User475983607 posted

    toytoy

    im basically copying the from the example on the link

    would love to know how to do it better

    and as a last note - as of this writing the documentation does not fully account for core 3.1

    for example ExpiresUTC is no longer an option rather being replaced by ExpireTimeSpan

    so hence my running about with the documentation wondering whether its missing something or its just me

    would apprec any insight you can give

    I followed the steps in the Use cookie authentication without ASP.NET Core Identity link to create an auth cookie.  The auth cookie is used to access a secured Action PlayVideo in an MVC project.

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
    
                services.AddControllersWithViews();
    
                services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
    
                services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthentication();
                app.UseAuthorization();
                
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    public class AccountController : Controller
        {
            private readonly ILogger _logger;
            public AccountController(ILogger<AccountController> logger)
            {
                _logger = logger;
            }
    
            public IActionResult Index(int id)
            {
                ViewBag.VideoId = id;
                return View();
            }
    
            [Authorize]
            public IActionResult PlayVideo(int id)
            {
                ViewBag.VideoId = id;
                return View();
            }
    
            [HttpGet]
            public async Task<IActionResult> Login(string returnUrl = null)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    
                ViewBag.ReturnUrl = returnUrl;
                return View();
            }
    
            [HttpPost]
            public async Task<IActionResult> Login(string password, string returnUrl = null)
            {
                var userName = await AuthenticateUser(password);
                if (userName == null)
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return View();
                }
    
                List<Claim> claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userName),
                    new Claim("VideoUrl", returnUrl),
                    new Claim(ClaimTypes.Role, "Anonymous")
                };
    
    
                var claimsIdentity = new ClaimsIdentity(
                    claims, CookieAuthenticationDefaults.AuthenticationScheme);
    
                var authProperties = new AuthenticationProperties();
    
                await HttpContext.SignInAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme,
                    new ClaimsPrincipal(claimsIdentity),
                    authProperties);
    
                _logger.LogInformation("User {Email} logged in at {Time}.", userName, DateTime.UtcNow);
    
                return Redirect(returnUrl);
    
            }
    
            private async Task<string> AuthenticateUser(string password)
            {
    
                await Task.Delay(500);
    
                if (password == "password")
                {
                    return "Anonymous";
                }
                else
                {
                    return null;
                }
            }
        }

    Video Index

        ViewData["Title"] = "Index";
    }
    
    <h1>Video List</h1>
    
    <div>
        <a asp-action="PlayVideo" asp-route-id="123">Play 123 Video</a>
    </div>

    Login View

    @{
        ViewData["Title"] = "AnonymousLoginAsync";
    }
    
    <h1>Login</h1>
    
    <div>
        <form method="post">
            <input id="password" type="text" name="password" value="password" />
            <input id="ReturnUrl" type="hidden" name="ReturnUrl" value="@ViewBag.ReturnUrl" />
            <input id="Submit1" type="submit" value="submit" />
        </form>
    
    </div>
    
    

    PlayVideo View

    @{
        ViewData["Title"] = "PlayVideo";
    }
    
    <h1>Play Video</h1>
    
    
    <div>
        Now Playing @ViewBag.VideoId
    </div>

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, March 23, 2020 8:30 PM

All replies

  • User475983607 posted

    the claims get processed but they dont get stored
    is it even possible to store these claims with an anonymous user
    if yes where should i be looking for them with an anonymous user?
    if no what should i be doing next?

    Cookie authentication caches claims in the cookie.  You've configured cookie authentication but did not follow the documentation and assign claims to the cookie.   Just follow the official docs; https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1.

    or am i making too much out of it ?
    i mean pre-Core i would have gone with session vars but thats not a thing now in core

    Yes, you are over complicating the cookie auth and Session does exist in ASP.NET Core; https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-3.1

    Monday, March 23, 2020 10:54 AM
  • User-1632036263 posted

    thank you for your response mgebhard

    if you read my OP youll notice that i did indeed follow that article and only got as far as i did

    hence my motivation for writing the post

    so reposting the link back to me doesnt help me solve this problem

    perhaps i can be a little more explicit with how its failing

    you will see in my screenshot the var claimsIdentity is indeed getting set - good

    in the next line where its supposed to attach the claim to the cookie heres what i get:

    it looks like no claim has been attached to the ClaimsPrinciple (ie, User)

    and then when i go to check the claim just to make sure im not crazy i get this

    the claim is indeed empty 

    i hope this explains my conundrum better - what am i missing?

    and when you say im overcomplicating the cookie auth can you explain how to simplify

    im basically copying the from the example on the link

    would love to know how to do it better

    and as a last note - as of this writing the documentation does not fully account for core 3.1

    for example ExpiresUTC is no longer an option rather being replaced by ExpireTimeSpan

    so hence my running about with the documentation wondering whether its missing something or its just me

    would apprec any insight you can give

    Monday, March 23, 2020 5:57 PM
  • User475983607 posted

    toytoy

    im basically copying the from the example on the link

    would love to know how to do it better

    and as a last note - as of this writing the documentation does not fully account for core 3.1

    for example ExpiresUTC is no longer an option rather being replaced by ExpireTimeSpan

    so hence my running about with the documentation wondering whether its missing something or its just me

    would apprec any insight you can give

    I followed the steps in the Use cookie authentication without ASP.NET Core Identity link to create an auth cookie.  The auth cookie is used to access a secured Action PlayVideo in an MVC project.

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
    
                services.AddControllersWithViews();
    
                services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
    
                services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthentication();
                app.UseAuthorization();
                
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    public class AccountController : Controller
        {
            private readonly ILogger _logger;
            public AccountController(ILogger<AccountController> logger)
            {
                _logger = logger;
            }
    
            public IActionResult Index(int id)
            {
                ViewBag.VideoId = id;
                return View();
            }
    
            [Authorize]
            public IActionResult PlayVideo(int id)
            {
                ViewBag.VideoId = id;
                return View();
            }
    
            [HttpGet]
            public async Task<IActionResult> Login(string returnUrl = null)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    
                ViewBag.ReturnUrl = returnUrl;
                return View();
            }
    
            [HttpPost]
            public async Task<IActionResult> Login(string password, string returnUrl = null)
            {
                var userName = await AuthenticateUser(password);
                if (userName == null)
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return View();
                }
    
                List<Claim> claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userName),
                    new Claim("VideoUrl", returnUrl),
                    new Claim(ClaimTypes.Role, "Anonymous")
                };
    
    
                var claimsIdentity = new ClaimsIdentity(
                    claims, CookieAuthenticationDefaults.AuthenticationScheme);
    
                var authProperties = new AuthenticationProperties();
    
                await HttpContext.SignInAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme,
                    new ClaimsPrincipal(claimsIdentity),
                    authProperties);
    
                _logger.LogInformation("User {Email} logged in at {Time}.", userName, DateTime.UtcNow);
    
                return Redirect(returnUrl);
    
            }
    
            private async Task<string> AuthenticateUser(string password)
            {
    
                await Task.Delay(500);
    
                if (password == "password")
                {
                    return "Anonymous";
                }
                else
                {
                    return null;
                }
            }
        }

    Video Index

        ViewData["Title"] = "Index";
    }
    
    <h1>Video List</h1>
    
    <div>
        <a asp-action="PlayVideo" asp-route-id="123">Play 123 Video</a>
    </div>

    Login View

    @{
        ViewData["Title"] = "AnonymousLoginAsync";
    }
    
    <h1>Login</h1>
    
    <div>
        <form method="post">
            <input id="password" type="text" name="password" value="password" />
            <input id="ReturnUrl" type="hidden" name="ReturnUrl" value="@ViewBag.ReturnUrl" />
            <input id="Submit1" type="submit" value="submit" />
        </form>
    
    </div>
    
    

    PlayVideo View

    @{
        ViewData["Title"] = "PlayVideo";
    }
    
    <h1>Play Video</h1>
    
    
    <div>
        Now Playing @ViewBag.VideoId
    </div>

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, March 23, 2020 8:30 PM
  • User-1632036263 posted

    hey mgebhard - 

    thank you for the clear concise code - its helpful to see it all in one go

    to be clear for any future readers - the code is good if you dont have Identity installed as well

    if you have Identity installed as well the [Authorize] annotation will send you off to the Identity Login

    in the end - it did not work for my code - still not sure why 

    the claims are still not sticking to the cookie in the HttpContext.SigninAsync

    i suspect it has to do with my startup because the actual claims code is very simple and obvious

    and worked when i built your mini-version (with Identity installed)

    my actual project is a little more complicated with options setup for both Identity and Cookie coexistance

    but im thinking one option is stomping over something else

    not quite getting the nuances yet of all that

    regardless - youre answer got me a step further and therefore is legit as an answer

    Tuesday, March 24, 2020 4:16 AM