1
votes

My setup: I've created and have running a WebAPI solution that performs the authentication of a username and password against a source (currently a db). This generates the JWT token and returns it to the requesting app (a ASP.NET Core 2.2 app).

Most solutions talk of securing the WebAPI exposed methods but my approach is to only do the authentication through WebAPI. The individual apps need to accept the token so they can determine authorization.

Now the question: what is the best approach to reading the token from the WebAPI (which I've done already), validating it, and then storing it for any/all controllers to know there is an authenticated user (via Authorize attribute) so long as the token is valid?

Debugging this more, it seems my token is not being added to the headers. I see this debug message:

Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'

Code Update2 - code that gets the JWT:

        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = new Uri(_configuration.GetSection("SecurityApi:Url").Value);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        //login
        Task<HttpResponseMessage> response = ValidateUserAsync(client, username, password);
        Task<Core.Identity.TokenViewModel> tokenResult = response.Result.Content.ReadAsAsync<Core.Identity.TokenViewModel>();

        if (!response.Result.IsSuccessStatusCode)
        {
            if (tokenResult != null && tokenResult.Result != null)
            {
                ModelState.AddModelError("", tokenResult.Result.ReasonPhrase);
            }
            else
            {
                ModelState.AddModelError("", AppStrings.InvalidLoginError);
            }
            return View();
        }

        JwtSecurityToken token = new JwtSecurityToken(tokenResult.Result.Token);
        int userId;

        if (int.TryParse(token.Claims.First(s => s.Type == JwtRegisteredClaimNames.NameId).Value, out userId))
        {
            //load app claims
            Core.Identity.UserInfo userInfo = Core.Identity.UserLogin.GetUser(_identityCtx, userId);
            Core.Identity.UserStore uStore = new Core.Identity.UserStore(_identityCtx);
            IList<Claim> claims = uStore.GetClaimsAsync(userInfo, new System.Threading.CancellationToken(false)).Result;
            claims.Add(new Claim(Core.Identity.PowerFleetClaims.PowerFleetBaseClaim, Core.Identity.PowerFleetClaims.BaseUri));

            ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme);
            ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);

            //complete
            AuthenticationProperties authProperties = new AuthenticationProperties();
            authProperties.ExpiresUtc = token.ValidTo;
            authProperties.AllowRefresh = false;
            authProperties.IsPersistent = true;

            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, tokenResult.Result.Token);
            //var stuff = HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, principal, authProperties);
        }
        else
        {
            ModelState.AddModelError("", AppStrings.InvalidLoginError);
            return View();
        }

        return RedirectToAction("Index", "Home");

Startup:

private void ConfigureIdentityServices(IServiceCollection services)
    {
        services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");

        //authentication token
        services.AddAuthentication(opt =>
        {
            opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddCookie(opt =>
        {
            opt.LoginPath = "/Login";
            opt.LogoutPath = "/Login/Logoff";
            opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
        }).AddJwtBearer(options =>
        {
            options.SaveToken = true;
            options.RequireHttpsMetadata = false;

            options.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidateAudience = true,
                ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
                ValidateIssuer = true,
                ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
                ValidateLifetime = true
            };
        });

        Core.Startup authStart = new Core.Startup(this.Configuration);
        authStart.ConfigureAuthorizationServices(services);
    }

Auth:

public void ConfigureAuthorizationServices(IServiceCollection services)
    {
        services.AddDbContext<Identity.IdentityContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SecurityConn")));
        services.AddScoped<DbContext, Identity.IdentityContext>(f =>
        {
            return f.GetService<Identity.IdentityContext>();
        });

        services.AddIdentityCore<Identity.UserInfo>().AddEntityFrameworkStores<Identity.IdentityContext>().AddRoles<Identity.Role>();
        services.AddTransient<IUserClaimStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
        services.AddTransient<IUserRoleStore<Core.Identity.UserInfo>, Core.Identity.UserStore>();
        services.AddTransient<IRoleStore<Core.Identity.Role>, Core.Identity.RoleStore>();

        services.AddAuthorization(auth =>
        {
            auth.AddPolicy(JwtBearerDefaults.AuthenticationScheme, new AuthorizationPolicyBuilder().AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build());
            auth.AddPolicy(PFBaseClaim, policy => policy.RequireClaim(Identity.PFClaims.BaseUri));
        });
    }
1
So essentially your WebAPI is an Identity Provider. In your other APIs, you need to set up some sort of Bearer Token Authentication. This configuration will have to have everything it needs to validate the token. There's several ways to do this.. BUT. If this is a new project, I'd highly suggest you take a look at Identity Server. It is an Identity Provider that offers everything you need, plus it implements OAuth and OpenId Connect, so you are covered. Then addin authentication on your other APIs is very easy.jpgrassi
This show some examples on how to enable bearer token authentication developer.okta.com/blog/2018/03/23/…jpgrassi
Not quite an Identity Server, just to authenticate. No other API's for security. This is an existing DB from over 10 years ago that did not use any ASPNET identity tables. It even has its own claims generation. What I'm trying to do is write a more modern version of authentication using JWT and be able to pick up the claims with existing procedures.Mike H
If you already provided a way of returning jwt tokens after a user is authenticated, it's just a matter of, in the other APIs validating that token. docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/…jpgrassi
I must be missing something obvious because controllers that are marked off for Authorized do not pick up anything showing a user has been authenticated. Does the token get put into a header? Cookie? How can I verify the app picked up the token and has it to use?Mike H

1 Answers

0
votes

In the end, my approach was to use a secure cookie and a base claim to prove the user authenticated.

private void ConfigureAuthentication(IServiceCollection services) { services.ConfigureApplicationCookie(options => options.LoginPath = "/Login");

        //authentication token
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
        {
            opt.LoginPath = "/Login";
            opt.AccessDeniedPath = "/Login";
            opt.LogoutPath = "/Login/Logoff";
            opt.Cookie.Name = Configuration.GetSection("SecurityApi:CookieName").Value;
        }).AddJwtBearer(options =>
        {
            options.SaveToken = true;

            options.TokenValidationParameters = new TokenValidationParameters()
            {
                ValidateAudience = true,
                ValidAudience = Configuration.GetSection("SecurityApi:Issuer").Value,
                ValidateIssuer = true,
                ValidIssuer = Configuration.GetSection("SecurityApi:Issuer").Value,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetSection("SecurityApi:Key").Value)),
                ValidateLifetime = true
            };
        });
    }

And at login:

            AuthenticationProperties authProperties = new AuthenticationProperties();
        authProperties.ExpiresUtc = token.ValidTo;
        authProperties.AllowRefresh = false;
        authProperties.IsPersistent = true;

        HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, userStore.CreateAsync(user).Result, authProperties);

        return RedirectToAction("Index", "Home");