2

I've managed to configure JWT authentication for my ASP.NET Core Web API. It works when using Postman.

I have also built an MVC admin section, that I want to log into. The guide I'm following to create the Admin section uses cookies and not JWT authentication for the log in page.

It doesn't work, I get 401 authentication error after the login. It redirects me to the correct page and you can see the Identity cookie in the browser but I'm not authenticated.

Login Page

Admin Page

I'm out of my depth here haha

Can I also use cookies as well as JWT authentication? JWT for any mobile phone app that wants to access the WebAPI but Cookies and sessions for logging in through the WebAPI's admin page?

My middleware Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
        // Tell Entity how to connect to the SQL Server
        services.AddDbContext<ApplicationDbContext>(options => 
        {
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
        });

        // Configure Identity
        services.Configure<IdentityOptions>(options =>
        {
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
            options.Lockout.MaxFailedAccessAttempts = 5;
            options.Lockout.AllowedForNewUsers = true;
            options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
            options.SignIn.RequireConfirmedEmail = false;                   // Set to true for production, test it
            options.User.RequireUniqueEmail = false;                        // Set to true for production
        });

        services.Configure<PasswordHasherOptions>(options =>
        {
            // First byte of the hashed password is 0x00 = V2 and 0x01 = V3
            options.CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV3;        // Default IdentityV2 is used, it uses SHA1 for hashing, 1000 iterations.
            options.IterationCount = 12000;                                                // With IdentityV3 we can use SHA256 and 12000 iterations.
        });

        // We need to add the IdentityUser to Entity and create a token for authentication.
        services.AddIdentity<User, IdentityRole>(options =>
        {
            options.Password.RequireDigit = true;
            options.Password.RequireLowercase = true;
            options.Password.RequireUppercase = true;
            options.Password.RequiredLength = 6;

        }).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();


        // JWT Authentication Tokens
        services.AddAuthentication(auth =>
       {
           // This will stop Identity using Cookies and make it use JWT tokens by default.
           auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
           auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
       }).AddJwtBearer(options =>
       {
           options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
           {
               ValidateIssuer = true,
               ValidateAudience = true,
               ValidAudience = "http://mywebsite.com",
               ValidIssuer = "http://mywebsite.com",
               ValidateLifetime = true,
               RequireExpirationTime = true,
               ValidateIssuerSigningKey = true,
               IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rsvgy555262gthsdfrthga"))
           };
           options.RequireHttpsMetadata = true;                    // Use HTTPS to transmit the token.
       });

        // Admin Login Cookie
        services.ConfigureApplicationCookie(options =>
        {
            options.LoginPath = "/Admin/Login";                             // Url for users to login to the app
            options.Cookie.Name = ".AspNetCore.Identity.Application";
            options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
            options.SlidingExpiration = true;
        });

        services.AddControllers();
        services.AddControllersWithViews();

}

My AdminController:

public class AdminController : Controller
{
    private UserManager<User> userManager;                  // Manage user accounts in DB
    private IPasswordHasher<User> passwordHasher;           // Hash user passwords
    private SignInManager<User> signInManager;              // Login

    // Constructor
    public AdminController(UserManager<User> usrMgr, IPasswordHasher<User> passwordHash, SignInManager<User> signinMgr)
    {
        userManager = usrMgr;
        passwordHasher = passwordHash;
        signInManager = signinMgr;
    }

    // Admin Login Page
    [AllowAnonymous]
    public IActionResult Login(string returnUrl)
    {
        Login login = new Login();
        return View(login);
    }

    // Admin Login Module
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Login(Login login)
    {
        if (ModelState.IsValid)
        {
            User loginUser = await userManager.FindByEmailAsync(login.Email);

            if (loginUser != null)
            {
                // Sign out any user already signed in
                await signInManager.SignOutAsync();

                // Sign in the new user
                Microsoft.AspNetCore.Identity.SignInResult result = await signInManager.PasswordSignInAsync(loginUser, login.Password, false, false);
                if (result.Succeeded)
                {
                    return Redirect("/Admin"); // Send user to localhost/Admin after login
                }
            }

            ModelState.AddModelError(nameof(login.Email), "Login Failed: Invalid Email or password");
        }

        return View(login);
    }

    // Admin Logout
    public async Task<IActionResult> Logout()
    {
        await signInManager.SignOutAsync();
        return RedirectToAction("Index");
    }

    // Admin Index Page
    [Authorize]
    public IActionResult Index()
    {
        return View(userManager.Users);
    }
}

Thanks, any help getting the cookies to work would be appreciated.

Isma
  • 14,604
  • 5
  • 37
  • 51
Scottish Smile
  • 455
  • 7
  • 22

1 Answers1

3

I found 2 methods of accomplishing this: Token Biased, and Cookie Biased (prefered).

I'm using ASP.NET Core 5.0, by the way, this might not work for 3.1.

Token Biased Solution

services.AddAuthentication(x => {
    // Set Jwt Bearer as default auth scheme.
    // Token found in Authorization header by default (Authorization: Bearer <JWT_TOKEN>)
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
    x.SaveToken = true;

    // Tap into the event lifecycle
    x.Events = new JwtBearerEvents {

        // Called when default authentication fails
        // This is where we validate add cookie authentication
        OnAuthenticationFailed = ctx => {
            string token = ctx.HttpContext.Request.Cookies["auth"];
            
            if (string.IsNullOrEmpty(token)) {
                // Tells ASP.NET that authentication failed
                ctx.Fail("Invalid token");

            } else {

                // Validate token
                if (JwtManager.ValidateToken(token, config)) {

                    // Set the principal
                    // Will throw error if not set
                    ctx.Principal = JwtManager.GetPrincipal(token);

                    // Tells ASP.NET that the authentication was successful
                    ctx.Success();

                    // Add the principal to the HttpContext for easy access in any the controllers (only routes that are authenticated)
                    ctx.HttpContext.Items.Add("claims", principal);
                } else {
                    ctx.Fail("Invalid Token");
                }
            }
            return Task.CompletedTask;
        }
    };

    ...
});

JwtManager is just a class that handles JWT operations. Essentially it holds static methods for generating, validating, and decoding JWT tokens. There are different ways to do each, depending on the library.

This event is only called when the default token validation fails.

NOTE 1: With this solution, you still need to have Authoritization: Bearer <SOMETHING> in the headers.

Cookie Biased Solution It's essentially the same thing, but in the OnMessageRecieved event

services.AddAuthentication(x => {
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
    x.SaveToken = true;
    x.Events = new JwtBearerEvents {
        // Same as before, but called before the default JWT bearer authentication
        OnMessageReceived = ctx => {
            string token = ctx.HttpContext.Request.Cookies["auth"];
            
            if (!string.IsNullOrEmpty(token)) {
                if (JwtManager.ValidateToken(token, config)) {
                    var principal = JwtManager.GetClaims(token);
                    ctx.Principal = principal;
                    ctx.Success();
                    ctx.HttpContext.Items.Add("claims", principal);
                }
            }
            
            return Task.CompletedTask;
        },
    };
});

This is called on every request, so it could have some overhead, but it works nonetheless.

NOTE 2: These solution might not be production ready, or even the best way to do so. Use with a grain of salt.

kaptcha
  • 68
  • 2
  • 10
  • You saved my day! I was searching for how I could do that for more than 1 day! And finally this is the easiest solution that I've ever seen. Thanks Man! :) – Majid M. Mar 09 '22 at 12:57
  • I will just add that one can simply set `ctx.Token` in `OnMessageReceived` and let the validation be handled like usual. – FreeLine Aug 30 '22 at 19:58
  • @FreeLine I never knew about that! Does it still authenticate the token and everything? – kaptcha Aug 31 '22 at 21:01
  • @kaptcha What happens is somewhat documented in the [source code](https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs#L54). As far as I can tell, the token property is for this exact use case; getting the token from a different source. – FreeLine Sep 01 '22 at 08:49