A_developer
A_developer

Reputation: 125

Authenticating .net console applications with .net core web API

I have a .net core 3.1 web API ,which was built with JWT authentication and it is integrated with Angular UI and it working as expected.

following is my JWT authentication middleware


services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.IncludeErrorDetails = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,       
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))    
    };
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
            {
                context.Response.Headers.Add("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});

Now i need to create some more Web API methods which will be consumed by Angular UI as well as some existing scheduled tasks (.net console applications which will be consuming the web api methods) which are created for internal operations and will be running in the background.

My API controllers are decorated with [Authorize] attribute. It is working fine with Angular UI where the authentication and authorization are implemented using JWT bearer token.The problem is now with the integration of scheduled tasks which does not have logic for getting the tokens.

How to integrate these console apps with .net core web API in terms of authentication? the easiest option (which i thought) is to create a user login with like username "servicetask" and obtain token based on that username and do the API operation (but this requires more effort since no.of console apps are more and there and some apps from other projects also).

Is there any way to handle authentication in this case?

  1. Is it good practice to pass some API key from console application and by pass the authentication in web API ? is that possible ? then how to handle the request in .net core web api?

  2. Is it possible to create any JWT role or claims for these service account and validate them?

Please help.

Upvotes: 3

Views: 5085

Answers (5)

  1. You can create a login password configuration on appsettings, db or somewhere to send a token (web api).

Worker.cs (console app)

public struct UserLogin
{
  public string user;
  public string password;
}
// ...
private async Task<string> GetToken(UserLogin login)
{
  try {
    string token;
    var content = new StringContent(JsonConvert.SerializeObject(login), Encoding.UTF8, "application/json");
    using (var httpClient = new HttpClient())
      using (var response = await httpClient.PostAsync($"{api}/login", content))
      {
        var result = await response.Content.ReadAsStringAsync();
        var request = JsonConvert.DeserializeObject<JObject>(result);
        token = request["token"].ToObject<string>();
      }
    return token;
  }
  catch (Exception e)
  {
    throw new Exception(e.Message);
  }      
}
  1. Give your console a jwt token without an expiration date or one that gives you enough time. If you need to invalidate the token follow this link. Add the jwt on appsettings.json and read the token as follows:

appsettings.json

{
  //...
  "Worker" : "dotnet",
  "Token": "eyJhbGciOiJIUzI1Ni...",
  "ApiUrl": "http://localhost:3005",
  //...
}

Worker.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;

public Worker(ILogger<Worker> _logger, IConfiguration _cfg)
{
  logger = _logger;
  //...
  api = _cfg["ApiUrl"];
  token = _cfg["Token"];
}

private async Task SendResult(SomeModel JobResult)
{
  var content = new StringContent(JsonConvert.SerializeObject(JobResult), Encoding.UTF8, "application/json");
  using (var httpClient = new HttpClient())
  {
    httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
    using (var response = await httpClient.PostAsync($"{api}/someController", content))
    {
      var result = await response.Content.ReadAsStringAsync();
      var rs = JsonConvert.DeserializeObject(result);
      Console.WriteLine($"API response {response.ReasonPhrase}");
    }
  }
}

Update:

If you need to control requests:

Startup.cs

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
    options.TokenValidationParameters = new TokenValidationParameters()
    {
      // ...
    };
    options.Events = new JwtBearerEvents
    {
      OnTokenValidated = TokenValidation
    };
  });
private static Task TokenValidation(TokenValidatedContext context)
{
  // your custom validation
  var hash = someHashOfContext();
  if (context.Principal.FindFirst(ClaimTypes.Hash).Value != hash)
  {
    context.Fail("You cannot access here");
  }
  return Task.CompletedTask;
}

Upvotes: 1

nighthawk
nighthawk

Reputation: 842

Best approach would be to allow both bearer token and API key authorization, especially since you are allowing access for users and (internal) services.

Add API key middleware (I personally use this, it's simple to use - package name is AspNetCore.Authentication.ApiKey) with custom validation (store API keys in database along with regular user data or in config, whatever you prefer). Modify [Authorize] attributes on controllers so both Bearer and ApiKey authorization can be used. Angular app continues to use Bearer authentication and any service/console apps (or any other client, including Angular client if needed in some case) sends X-Api-Key header containing API key assigned to that app.

Middleware configuration should look something like this:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddApiKeyInHeader(ApiKeyDefaults.AuthenticationScheme, options =>
{
    options.KeyName = "X-API-Key";
    options.SuppressWWWAuthenticateHeader = true;
    options.Events = new ApiKeyEvents
    {
        // A delegate assigned to this property will be invoked just before validating the api key. 
        OnValidateKey = async (context) =>
        {
            var apiKey = context.ApiKey.ToLower();
            // custom code to handle the api key, create principal and call Success method on context. apiUserService should look up the API key and determine is it valid and which user/service is using it
            var apiUser = apiUserService.Validate(apiKey);
            if (apiUser != null)
            {
                ... fill out the claims just as you would for user which authenticated using Bearer token...
                var claims = GenerateClaims();
                context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                context.Success();
             }
             else
             {
                 // supplied API key is invalid, this authentication cannot proceed
                 context.NoResult();
             }
         }
        };
})
// continue with JwtBearer code you have
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => ...

This sorts out Startup.cs part.

Now, in controllers, where you want to enable both Bearer and ApiKey authentication, modify attribute so it looks like this:

[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "ApiKey, Bearer")]
public class SomeController : ControllerBase

Now, Angular client will still work in the same way but console app might call API like this:

using (HttpClient client = new HttpClient())
{
    // header must match definition in middleware
    client.DefaultRequestHeaders.Add("X-API-Key", "someapikey");
    client.BaseAddress = new Uri(url);
    using (HttpResponseMessage response = await client.PostAsync(url, q))
    {
        using (HttpContent content =response.Content)
        {
            string mycontent = await content.ReadAsStringAsync();              
        }        
    }
}

This approach in my opinion makes best use of AuthenticationHandler and offers cleanest approach of handling both "regular" clients using JWT and services using fixed API keys, closely following something like OAuth middleware. More details about building custom authentication handler if someone wants to build something like this from scratch, implementing basically any kind of authentication.

Downside of course is security of those API keys even if you are using them for internal services only. This problem can be remedied a bit by limiting access scope for those API keys using Claims, not using same API key for multiple services and changing them periodically. Also, API keys are vulnerable to interception (MITM) if SSL is not used so take care of that.

Upvotes: 7

Aldine Rutury
Aldine Rutury

Reputation: 61

  1. Don`t bypass authentication. You can pass appKey (key to identify the app instance) to webapi endpoint that is responsible for identifying your dotnet console apps. If appkey is part of your registered appkeys list, let the webapi endpoint get token on behalf of the console app by subsequent authentication step with your webapi auth service and return a JWT token to the console app. In my case I have console apps running on dotnet 4.5, I mention this because HttpClient is not available in previous versions. With HttpClient you can then do the following in your console app.
HttpClient client = new HttpClient(); 

client.BaseAddress = new Uri("localhost://mywebapi/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/text"));

HttpResponseMessage response= client.GetAsync("api/appidentityendpoint/appkey").GetAwaiter().GetResult();

var bytarr = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
string responsemessage = Encoding.UTF8.GetString(bytarr);

res = JsonConvert.DeserializeObject<Authtoken>(responsemessage);

Authtoken object can be as simple as

public class Authtoken
{
  public string JwtToken{ get; set; }
}

and once you have your token you add it to your HttpClient headers for subsequent calls to your protected endpoints

  client.DefaultRequestHeaders.Add("Authorization", "Bearer " + res.JwtToken);

  client.GetAsync("api/protectedendpoint").GetAwaiter().GetResult();

Error handling is obviously required to handle reauthentication in case of token expiry etc

On server side, a simplified example is as follows

 [Produces("application/json")]
 [Route("api/Auth")]
 public class AuthController : Controller
 { 
    
    private readonly IAppRegService _regAppService;
    public AuthController(IAppRegService regAppService){
       _regAppService = regAppService;
    };
   //api/auth/console/login/585
   [HttpGet, Route("console/login/{appkey}")]
   public async Task<IActionResult> Login(string appkey)
   {
      
      // write logic to check in your db if appkey is the key of a registered console app.
      // _regAppService has methods to connect to db or read file to check if key exists from your repository of choice
       var appkeyexists = _regAppService.CheckAppByAppKey(appkey);
       if(appkeyexists){       
       //create claims list
       List<Claim> claims = new List<Claim>();
       claims.Add(new Claim("appname", "console",ClaimValueTypes.String));
       claims.Add(new Claim("role","daemon",ClaimValueTypes.String));

      //create a signing secret
       var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey"));
       var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);  
       //create token options
       var tokenOptions = new JwtSecurityToken(
                            issuer: "serverurl",
                            audience:"consoleappname",
                            claims: claims,
                            expires: DateTime.Now.AddDays(5),
                            signingCredentials: signinCredentials
                        );
        //create token
        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
      //return token
       return new OkObjectResult(new Authtoken { JwtToken= tokenString });
                       
       } else {
          return Unauthorized();
       }
   }
 }

Upvotes: 2

Grizzlly
Grizzlly

Reputation: 596

I will present how I do JWT Auth from a WebAssembly app to a .NET Core API. Everything is based on this YouTube video. It explains everything you need to know. Down below is a sample of code from the video to give you an idea of what you have to do.

This is my Auth Controller:

// A bunch of usings

namespace Server.Controllers.Authentication
{
    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> userManager;
        private readonly RoleManager<IdentityRole> roleManager;
        private readonly IConfiguration _configuration;
        private readonly AppContext appContext;

        public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, AppContext appContext)
        {
            this.userManager = userManager;
            this.roleManager = roleManager;
            this._configuration = configuration;
            this.appContext = appContext;
        }

        [HttpPost]
        [Route("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
        {
            ApplicationUser user = await userManager.FindByNameAsync(loginModel.Username);

            if ((user is not null) && await userManager.CheckPasswordAsync(user, loginModel.Password))
            {
                IList<string> userRoles = await userManager.GetRolesAsync(user);

                List<Claim> authClaims = new()
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(ClaimTypes.NameIdentifier, user.Id),
                    new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(ClaimTypes.AuthenticationMethod, "pwd")
                };

                foreach (string role in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, role));
                }

                SymmetricSecurityKey authSigningKey = new(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
                //SymmetricSecurityKey authSigningKey = Startup.SecurityAppKey;

                JwtSecurityToken token = new(
                    issuer: _configuration["JWT:ValidIssuer"],
                    //audience: _configuration["JWT:ValidAudience"],
                    expires: DateTime.Now.AddHours(3),
                    claims: authClaims,
                    signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                    );

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }

            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        [AllowAnonymous]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            ApplicationUser userExists = await userManager.FindByNameAsync(model.Username);

            if (userExists != null)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
            }

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };

            IdentityResult result = await userManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
            }

            await userManager.AddToRoleAsync(user, UserRoles.User);

            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }
    }
}

When a user registers it is automatically added to the User Role. What you should do is to create accounts for each of your console apps, or even a global account for all internal apps, and then assign it to a custom role.

After that, on all API endpoints that are only accessible by your internal apps add this attribute: [Authorize(Roles = UserRoles.Internal)]

UserRoles is a static class that has string properties for each role.

More info about Role-based authorization can be found here.

Upvotes: 1

Shahriar Morshed
Shahriar Morshed

Reputation: 112

1.IMO, no , this won't be good idea. 2. Yes you can use claims for this scenario . Use a BackgroundService to run your task and inject claims principle on this class.

This sample is for service provider account claims: serviceAccountPrincipleProvider.cs

 public class ServiceAccountPrincipalProvider : IClaimsPrincipalProvider
{
    private readonly ITokenProvider tokenProvider;

    public ServiceAccountPrincipalProvider(ITokenProvider tokenProvider)
    {
        this.tokenProvider = tokenProvider;
    } 

    public ClaimsPrincipal CurrentPrincipal
    {
        get
        {
            var accessToken = tokenProvider.GetAccessTokenAsync().GetAwaiter().GetResult();
            if (accessToken == null)
                return null;

            var identity = new ClaimsIdentity(AuthenticationTypes.Federation);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accessToken.Subject));
            identity.AddClaim(new Claim(AppClaimTypes.Issuer, accessToken.Issuer));
            identity.AddClaim(new Claim(AppClaimTypes.AccessToken, accessToken.RawData));

            return new ClaimsPrincipal(identity);
        }
    }
}

This is your IClaimsProvider interface:

public interface IClaimsPrincipalProvider
{
    ClaimsPrincipal CurrentPrincipal { get; }
}

Upvotes: 2

Related Questions