Jwt Authentication For ASP.NET Web API

JWT authentication for ASP.NET Web API

I answered this question: How to secure an ASP.NET Web API 4 years ago using HMAC.

Now, lots of things changed in security, especially that JWT is getting popular. In this answer, I will try to explain how to use JWT in the simplest and basic way that I can, so we won't get lost from jungle of OWIN, Oauth2, ASP.NET Identity, etc..

If you don't know about JWT tokens, you need to take a look at:

https://www.rfc-editor.org/rfc/rfc7519

Basically, a JWT token looks like this:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

A JWT token has three sections:

  1. Header: JSON format which is encoded in Base64
  2. Claims: JSON format which is encoded in Base64.
  3. Signature: Created and signed based on Header and Claims which is encoded in Base64.

If you use the website jwt.io with the token above, you can decode the token and see it like below:

A screenshot of jwt.io with the raw jwt source and the decoded JSON it represents

Technically, JWT uses a signature which is signed from headers and claims with security algorithm specified in the headers (example: HMACSHA256). Therefore, JWT must be transferred over HTTPs if you store any sensitive information in its claims.

Now, in order to use JWT authentication, you don't really need an OWIN middleware if you have a legacy Web Api system. The simple concept is how to provide JWT token and how to validate the token when the request comes. That's it.

In the demo I've created (github), to keep the JWT token lightweight, I only store username and expiration time. But this way, you have to re-build new local identity (principal) to add more information like roles, if you want to do role authorization, etc. But, if you want to add more information into JWT, it's up to you: it's very flexible.

Instead of using OWIN middleware, you can simply provide a JWT token endpoint by using a controller action:

public class TokenController : ApiController
{
// This is naive endpoint for demo, it should use Basic authentication
// to provide token or POST request
[AllowAnonymous]
public string Get(string username, string password)
{
if (CheckUser(username, password))
{
return JwtManager.GenerateToken(username);
}

throw new HttpResponseException(HttpStatusCode.Unauthorized);
}

public bool CheckUser(string username, string password)
{
// should check in the database
return true;
}
}

This is a naive action; in production you should use a POST request or a Basic Authentication endpoint to provide the JWT token.

How to generate the token based on username?

You can use the NuGet package called System.IdentityModel.Tokens.Jwt from Microsoft to generate the token, or even another package if you like. In the demo, I use HMACSHA256 with SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
/// var hmac = new HMACSHA256();
/// var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
var symmetricKey = Convert.FromBase64String(Secret);
var tokenHandler = new JwtSecurityTokenHandler();

var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}),

Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(symmetricKey),
SecurityAlgorithms.HmacSha256Signature)
};

var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);

return token;
}

The endpoint to provide the JWT token is done.

How to validate the JWT when the request comes?

In the demo, I have built
JwtAuthenticationAttribute which inherits from IAuthenticationFilter (more detail about authentication filter in here).

With this attribute, you can authenticate any action: you just have to put this attribute on that action.

public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}

You can also use OWIN middleware or DelegateHander if you want to validate all incoming requests for your WebAPI (not specific to Controller or action)

Below is the core method from authentication filter:

private static bool ValidateToken(string token, out string username)
{
username = null;

var simplePrinciple = JwtManager.GetPrincipal(token);
var identity = simplePrinciple.Identity as ClaimsIdentity;

if (identity == null || !identity.IsAuthenticated)
return false;

var usernameClaim = identity.FindFirst(ClaimTypes.Name);
username = usernameClaim?.Value;

if (string.IsNullOrEmpty(username))
return false;

// More validate to check whether username exists in system

return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
string username;

if (ValidateToken(token, out username))
{
// based on username to get more information from database
// in order to build local identity
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
// Add more claims if needed: Roles, ...
};

var identity = new ClaimsIdentity(claims, "Jwt");
IPrincipal user = new ClaimsPrincipal(identity);

return Task.FromResult(user);
}

return Task.FromResult<IPrincipal>(null);
}

The workflow is to use the JWT library (NuGet package above) to validate the JWT token and then return back ClaimsPrincipal. You can perform more validation, like check whether user exists on your system, and add other custom validations if you want.

The code to validate JWT token and get principal back:

public static ClaimsPrincipal GetPrincipal(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

if (jwtToken == null)
return null;

var symmetricKey = Convert.FromBase64String(Secret);

var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
};

SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

return principal;
}
catch (Exception)
{
//should write log
return null;
}
}

If the JWT token is validated and the principal is returned, you should build a new local identity and put more information into it to check role authorization.

Remember to add config.Filters.Add(new AuthorizeAttribute()); (default authorization) at global scope in order to prevent any anonymous request to your resources.

You can use Postman to test the demo:

Request token (naive as I mentioned above, just for demo):

GET http://localhost:{port}/api/token?username=cuong&password=1

Put JWT token in the header for authorized request, example:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

The demo can be found here: https://github.com/cuongle/WebApi.Jwt

Can API Key and JWT Token be used in the same .Net 6 WebAPI

Yes it is possible.

The solution I recommend is to setup multiple authentication methods in asp.net core 6 using two authentication schemes that you have to specify inside Authorize attribute.
Here a simple implementation of ApiKey authentication:

namespace MyAuthentication;

public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private enum AuthenticationFailureReason
{
NONE = 0,
API_KEY_HEADER_NOT_PROVIDED,
API_KEY_HEADER_VALUE_NULL,
API_KEY_INVALID
}

private readonly Microsoft.Extensions.Logging.ILogger _logger;

private AuthenticationFailureReason _failureReason = AuthenticationFailureReason.NONE;

public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory loggerFactory,
ILogger<ApiKeyAuthenticationHandler> logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
{
_logger = logger;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//ApiKey header get
if (!TryGetApiKeyHeader(out string providedApiKey, out AuthenticateResult authenticateResult))
{
return authenticateResult;
}

//TODO: you apikey validity check
if (await ApiKeyCheckAsync(providedApiKey))
{
var principal = new ClaimsPrincipal(); //TODO: Create your Identity retreiving claims
var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.Scheme);

return AuthenticateResult.Success(ticket);
}

_failureReason = AuthenticationFailureReason.API_KEY_INVALID;
return AuthenticateResult.NoResult();
}

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $@"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.ContentType = MediaTypeNames.Application.Json;

//TODO: setup a response to provide additional information if you want
var result = new
{
StatusCode = Response.StatusCode,
Message = _failureReason switch
{
AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED => "ApiKey not provided",
AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL => "ApiKey value is null",
AuthenticationFailureReason.NONE or AuthenticationFailureReason.API_KEY_INVALID or _ => "ApiKey is not valid"
}
};

using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}

protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $@"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = MediaTypeNames.Application.Json;

var result = new
{
StatusCode = Response.StatusCode,
Message = "Forbidden"
};

using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}

#region Privates
private bool TryGetApiKeyHeader(out string apiKeyHeaderValue, out AuthenticateResult result)
{
apiKeyHeaderValue = null;
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
{
_logger.LogError("ApiKey header not provided");

_failureReason = AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED;
result = AuthenticateResult.Fail("ApiKey header not provided");

return false;
}

apiKeyHeaderValue = apiKeyHeaderValues.FirstOrDefault();
if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKeyHeaderValue))
{
_logger.LogError("ApiKey header value null");

_failureReason = AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL;
result = AuthenticateResult.Fail("ApiKey header value null");

return false;
}

result = null;
return true;
}

private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...

return Task.FromResult<bool>(true);
}
#endregion
}

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";

public static string Scheme => DefaultScheme;
public static string AuthenticationType => DefaultScheme;
}

public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
=> authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}

Then register inside builder setup:

_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddApiKeySupport(options => { });

You have to also setup the standard JWT Bearer validation (I don't post it for the sake of brevity).

To protect your endpoint add the Authorize attribute like:

[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)]  //ApiKey
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...

return null;
}

//or..

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...

return null;
}

//or..
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationOptions.DefaultScheme}" )] //ApiKey and Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...

return null;
}

For me it is the best way so as to carry out the authorization check before the start of the application pipeline (fail fast) and to be able to create the user identity.

But if you don't need to put informations about the Api Key inside the ClaimsPrincipal and only check the validity of Api Key the simplest way to do that is:

  • Protect the "admin" actions with JWT auth (with Authorize attribute)
  • Setup and register a middleware to only check the Api Key in all actions
    Here is an example:
public class SimpleApiKeyMiddleware
{
private static readonly string API_KEY_HEADER = "X-Api-Key";

private readonly RequestDelegate _next;
private readonly ILogger<SimpleApiKeyMiddleware> _logger;

public SimpleApiKeyMiddleware(RequestDelegate next, ILogger<SimpleApiKeyMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task Invoke(HttpContext httpContext)
{
//Get apikey header
if (!httpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
{
_logger.LogError("ApiKey not found inside request headers");

//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not found inside request headers");
}
else if (!await ApiKeyCheckAsync(apiKey))
{
_logger.LogError("ApiKey is not valid: {ApiKey}", apiKey);

//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not valid");
}
else
{
_logger.LogInformation("ApiKey validated: {ApiKey}", apiKey);

//Proceed with pipeline
await _next(httpContext);
}
}

private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...

return Task.FromResult<bool>(true);
}

private async Task GenerateForbiddenResponse(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = MediaTypeNames.Application.Json;

using var responseStream = new MemoryStream();
await System.Text.Json.JsonSerializer.SerializeAsync(responseStream, new
{
Status = StatusCodes.Status403Forbidden,
Message = message
});

await context.Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
}

Registration:

_ = app.UseMiddleware<ApiKeyMiddleware>();  //Register as first middleware to avoid other middleware execution before api key check

Usage:

//Admin: Jwt and Api Key check
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt and Api Key
[HttpGet]
public async Task<IActionResult> MyAdminApi()
{
//...omissis...
}

//Non Admin: Api Key check only
[HttpGet]
public async Task<IActionResult> MyNonAdminApi()
{
//...omissis...
}

Note: the middleware code above forces exit from pipeline returning an http result so as to stop next middleware execution. Also note that the asp.net core 6 pipeline executes Authorization first and then all the registered middlewares.

How to generate Jwt token in Asp.net 6.0 web Api

Below is a demo , you can refer to it.

In Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<IUsersDb>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("IUsersDb")));
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("V1", new OpenApiInfo
{
Version = "V1",
Title = "Educal API",
Description = "Main API Documantation of Educal API"
});
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"Please provide authorization token to access restricted features.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
});

c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}


},
new List<string>()
}
});
});
// ADD JWT Authentication
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.Events = new JwtBearerEvents();
o.Events.OnTokenValidated = context =>
{
context.Response.StatusCode = 200;
return Task.CompletedTask;
};
o.Events.OnAuthenticationFailed = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
o.Events.OnChallenge = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
o.Events.OnMessageReceived = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
var key = Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]);
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ClockSkew = TimeSpan.Zero,
//ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
builder.Services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options => {
options.SwaggerEndpoint("/swagger/V1/swagger.json", "Main API Documantation of Educal API");
});
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

IAuthenticationService:

 public interface IAuthenticationService
{
Models.AuthenticationToken? Authenticate(User user);
}

AuthenticationService:

public class AuthenticationService : IAuthenticationService
{
private readonly IConfiguration _configuration;
private readonly IUsersDb _usersDb;

public AuthenticationService(IConfiguration configuration, IUsersDb usersDb)
{
_configuration = configuration;
_usersDb = usersDb;
}

public Models.AuthenticationToken? Authenticate(User user)
{
var foundUser = _usersDb.User
.FirstOrDefault(x => x.Name == user.Name && x.Password == user.Password);
if (foundUser == null)
{
return null;
}

//If user found then generate JWT
return CreateAuthenticationToken(foundUser);
}

private Models.AuthenticationToken CreateAuthenticationToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var tokenKey = Encoding.UTF8.GetBytes(_configuration["JWT:Key"]);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new(ClaimTypes.Name, user.Name),
}),

Expires = DateTime.UtcNow.AddMinutes(10),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(tokenKey),
SecurityAlgorithms.HmacSha256Signature)
};

var token = tokenHandler.CreateToken(tokenDescriptor);

return new Models.AuthenticationToken()
{
Token = tokenHandler.WriteToken(token),
};
}
}

AuthenticationToken:

public class AuthenticationToken
{
public string Token { get; set; }
}

UsersController:

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
private readonly IAuthenticationService _authenticationService;
private readonly IUsersDb _usersDb;

public UsersController(IAuthenticationService authenticationService, IUsersDb usersDb)
{
_authenticationService = authenticationService;
_usersDb = usersDb;
}

[AllowAnonymous]
[HttpPost]
[Route("authenticate")]
public IActionResult Authenticate(User user)
{
var token = _authenticationService.Authenticate(user);

if (token == null)
{
return Unauthorized();
}

return Ok(token);
}
}

User

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Password { get; set; }
}

IUsersDb:

public class IUsersDb : DbContext
{
public IUsersDb(DbContextOptions<IUsersDb> options)
: base(options)
{
}

public DbSet<User> User { get; set; }
}

appsettings.json:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"IUsersDb": "Server=(localdb)\\mssqllocaldb;Database=IUsersDb-7dc5b790-765f-4381-988c-5167405bb107;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"JWT": {
"Key": "fc746b61cde4f6665d3f9791446cd5395661860c0075a905ed9810b7391af467",
"Issuer": "Comply",
"Audience": "comply"
}
}

Result:

Sample Image

Best practices for managing Web API JWT token in another Web API

I'd handle this in some kind of BaseApiService

public class BaseApiService
{
private readonly IHttpClientFactory httpClientFactory;
private readonly ITokenHandler tokenHandler;

public BaseApiService(IHttpClientFactory httpClientFactory, ITokenHandler tokenHandler)
{
this.httpClientFactory = httpClientFactory;
this.tokenHandler = tokenHandler;
}

protected async Task<HttpResponseMessage> RequestAsync(HttpRequestMessage requestMessage)
{
var httpClient = httpClientFactory.CreateClient();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenHandler.Token);
var response = await httpClient.SendAsync(requestMessage);

if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
var token = await tokenHandler.UpdateTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await RequestAsync(requestMessage);
}
}

return response;
}
}

Which would be responsible for making request, response serialization (notice I've used string responses for simplicity sake) and handling token for each request. Also you might want to consider handling errors and also handle infinite loops because it's currently calling self (e.g. on second call if it's unauthorized again, exit with error).

Token handler would be defined as singleton in DI and this is implementation

public interface ITokenHandler
{
string Token { get; }
Task<string> UpdateTokenAsync();
}

public class TokenHandler : ITokenHandler
{
private readonly IHttpClientFactory httpClientFactory;
public string Token { get; private set; }

public TokenHandler(IHttpClientFactory httpClientFactory)
{
this.httpClientFactory = httpClientFactory;
}

public async Task<string> UpdateTokenAsync()
{
var httpClient = httpClientFactory.CreateClient();
var result = await httpClient.PostAsync("/external-api/token", new FormUrlEncodedContent(new []
{
new KeyValuePair<string, string>("username", "external-admin"),
new KeyValuePair<string, string>("password", "external-password"),
}));}}


Related Topics



Leave a reply



Submit