how to solve 401 unauthorized error in postman when calling a .Net API - authentication

I have a .net core webapi working fine and tested with swagger, also the method has set to allow anonymous access so no authentication should be required. But when testing the POST method with Postman, I always get the 401 error.. Appreciate any help!

Since I am not clear about your specific code implementation, I wrote a demo here, which is an example of generating token from user login to access permission API. You can refer to it, maybe it will help you a little:
First,open the appsettings.json file and change the section named Jwt:
"Jwt": {
"Issuer": "testUser",
"Audience": "user",
"Key": "this is my custom Secret key for authnetication"
}
Enable the JWT authentication scheme and swagger authorization configuration when the configuration starts, the entire code is as follows:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using WebApplication129.Controllers.conf;
namespace WebApplication129
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//services.AddSingleton<IConfiguration>(Configuration);
services.AddAuthentication(opt => {
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddControllers(options =>
{
options.Conventions.Add(new GroupingByNamespaceConvention());
});
services.AddSwaggerGen(config =>
{
config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description =
"JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat="JWT"
});
config.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
var titleBase = "Test API";
var description = "This is a Web API for Test operations";
var TermsOfService = new Uri("https://xxxxxx");
var License = new OpenApiLicense()
{
Name = "MIT"
};
var Contact = new OpenApiContact()
{
Name = "Test",
Email = "Test#hotmail.com",
Url = new Uri("https://xxxxxx")
};
config.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = titleBase + " v1",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
config.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = titleBase + " v2",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
config.IncludeXmlComments(xmlPath);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "Test v1");
//config.SwaggerEndpoint("/swagger/v2/swagger.json", "Test v2");
});
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Log in and generate the jwt part as follows. Since I did not use it with a database, I customized a user:
Model:
public class Usuario
{
public string NomeUsuario { get; set; }
public string Senha { get; set; }
}
Controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebApplication129.Model;
namespace WebApplication129.Controllers.V1
{
[Route("api/[controller]")]
[ApiController]
public class SegurancaController : Controller
{
private IConfiguration _config;
public SegurancaController(IConfiguration Configuration)
{
_config = Configuration;
}
[HttpPost]
[Route("login")]
public IActionResult Login([FromBody] Usuario loginDetalhes)
{
bool resultado = ValidarUsuario(loginDetalhes);
if (resultado)
{
var tokenString = GerarTokenJWT();
return Ok(new { token = tokenString });
}
else
{
return Unauthorized();
}
}
private string GerarTokenJWT()
{
var issuer = _config["Jwt:Issuer"];
var audience = _config["Jwt:Audience"];
var expiry = DateTime.Now.AddMinutes(120);
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer: issuer, audience: audience,
expires: expiry, signingCredentials: credentials);
var tokenHandler = new JwtSecurityTokenHandler();
var stringToken = tokenHandler.WriteToken(token);
return stringToken;
}
private bool ValidarUsuario(Usuario loginDetalhes)
{
if (loginDetalhes.NomeUsuario == "TestName" && loginDetalhes.Senha == "TestPwd")
{
return true;
}
else
{
return false;
}
}
}
}
Test and verify API:
[ApiController]
[Route("api/v1/[controller]")]
public class HomeController : ControllerBase
{
/// <summary>
/// Add the description information you need
/// </summary>
///
///
[Route("test")]
[HttpGet]
public string Test( int userID)
{
return "v1 test";
}
[Route("list_data")]
[HttpGet]
[Authorize]
public Object Data()
{
User user = new User();
user.id = 1;
user.userName = "Test";
user.email = "test#xxx.com";
user.address = "testAddress";
return user;
}
}
The above shows two APIs, one requires authorization and the other does not require authorization to access.
Resutl:

Still If anyone can't figure out the error after #Tupac answer, check that you have included proper
app.UseAuthentication();
app.UseAuthorization();
in Program.cs file

Related

JWT Authentication And Authorization In .NET 6.0 401Unauthorized Error

i am creating a token and it does not accept the token I created, the error code 401 returns
appsettings.json
{
"TokenOptions": {
"Audience": "https://localhost:7098",
"Issuer": "https://localhost:7098",
"AccessTokenExpiration": 500,
"SecurityKey": "mysecretkeymysecretkey"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
TokenOptions.cs
namespace Shared.Utilities.Security.Jwt;
public class TokenOptions
{
public string Audience { get; set; }
public string Issuer { get; set; }
public int AccessTokenExpiration { get; set; }
public string SecurityKey { get; set; }
}
JwtHelper.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using Shared.Entities.Concrete;
using Shared.Extensions;
using Shared.Utilities.Security.Encyption;
namespace Shared.Utilities.Security.Jwt;
public class JwtHelper : ITokenHelper
{
public IConfiguration Configuration { get; }
private TokenOptions _tokenOptions;
private DateTime _accessTokenExpiration;
public JwtHelper(IConfiguration configuration)
{
Configuration = configuration;
_tokenOptions = Configuration.GetSection(key: "TokenOptions").Get<TokenOptions>();
}
public AccessToken CreateToken(User user, List<OperationClaim> operationClaims)
{
_accessTokenExpiration = DateTime.Now.AddMinutes(_tokenOptions.AccessTokenExpiration);
var securityKey = SecurityKeyHelper.CreateSecurityKey(_tokenOptions.SecurityKey);
var signingCredentials = SigningCredentialsHelper.CreateSigningCredentials(securityKey);
var jwt = CreateJwtSecurityToken(_tokenOptions, user, signingCredentials, operationClaims);
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var token = jwtSecurityTokenHandler.WriteToken(jwt);
return new AccessToken
{
Token = token,
Expiration = _accessTokenExpiration
};
}
public JwtSecurityToken CreateJwtSecurityToken(TokenOptions tokenOptions, User user, SigningCredentials signingCredentials, List<OperationClaim> operationClaims)
{
var Jwt = new JwtSecurityToken(
issuer: tokenOptions.Issuer,
audience: tokenOptions.Audience,
expires: _accessTokenExpiration,
notBefore: DateTime.Now,
claims: SetClains(user, operationClaims),
signingCredentials: signingCredentials
);
return Jwt;
}
private IEnumerable<Claim> SetClains(User user, List<OperationClaim> operationClaims)
{
var claims = new List<Claim>();
claims.AddNameIdentifier(user.Id.ToString());
claims.AddEmail(user.Email);
claims.AddName($"{user.FirstName} {user.LastName}");
claims.AddRole(operationClaims.Select(c => c.Name).ToArray());
return claims;
}
}
ITokenHelper.cs
using Shared.Entities.Concrete;
namespace Shared.Utilities.Security.Jwt;
public interface ITokenHelper
{
AccessToken CreateToken(User user, List<OperationClaim> operationClaims);
}
AccessToken.cs
namespace Shared.Utilities.Security.Jwt
{
public class AccessToken
{
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
}
HashingHelper.cs
using System.Text;
namespace Shared.Utilities.Security.Hashing;
public class HashingHelper
{
public static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
}
}
public static bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
{
using (var hmac = new System.Security.Cryptography.HMACSHA512(passwordSalt))
{
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != passwordHash[i])
{
return false;
}
}
}
return true;
}
}
SigningCredentialsHelper.cs
using Microsoft.IdentityModel.Tokens;
namespace Shared.Utilities.Security.Encyption;
public class SigningCredentialsHelper
{
public static SigningCredentials CreateSigningCredentials(SecurityKey securityKey)
{
return new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
}
}
SecurityKeyHelper.cs
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Shared.Utilities.Security.Encyption;
public class SecurityKeyHelper
{
public static SecurityKey CreateSecurityKey(string securityKey)
{
return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
}
}
ClaimExtensions.cs
using System.Security.Claims;
using Microsoft.IdentityModel.JsonWebTokens;
namespace Shared.Extensions;
public static class ClaimExtensions
{
public static void AddEmail(this ICollection<Claim> claims, string email)
{
claims.Add(new Claim(type: JwtRegisteredClaimNames.Email, value: email));
}
public static void AddName(this ICollection<Claim> claims, string name)
{
claims.Add(new Claim(type: ClaimTypes.Name, value: name));
}
public static void AddNameIdentifier(this ICollection<Claim> claims, string nameIdentifier)
{
claims.Add(new Claim(type: ClaimTypes.NameIdentifier, value: nameIdentifier));
}
public static void AddRole(this ICollection<Claim> claims, string[] roles)
{
roles.ToList().ForEach(role => claims.Add(new Claim(type: ClaimTypes.Role, value: role)));
}
}
AutofacBusinessModule.cs
using Autofac;
using Data.Abstract;
using Data.Concrete.EntityFramework;
using Services.Abstract;
using Services.Concrete;
using Shared.Utilities.Security.Jwt;
namespace Services.DependencyResolvers.Autofac
{
public class AutofacBusinessModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<ProductManager>().As<IProductService>();
builder.RegisterType<EfProductRepository>().As<IProductRepository>();
builder.RegisterType<CategoryManager>().As<ICategoryService>();
builder.RegisterType<EfCategoryRepository>().As<ICategoryRepository>();
builder.RegisterType<UserManager>().As<IUserService>();
builder.RegisterType<EfUserRepository>().As<IUserRepository>();
builder.RegisterType<AuthManager>().As<IAuthService>();
builder.RegisterType<JwtHelper>().As<ITokenHelper>();
}
}
}
AuthController.cs
using Entities.Dtos;
using Microsoft.AspNetCore.Mvc;
using Services.Abstract;
namespace WebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserForLoginDto userForLoginDto)
{
var userToLogin = await _authService.Login(userForLoginDto);
if (!userToLogin.IsSuccess)
return BadRequest(userToLogin.Message);
var result = await _authService.CreateAccessToken(userToLogin.Data);
if (result.IsSuccess)
return Ok(result.Data);
return BadRequest(result.Message);
}
[HttpPost("register")]
public async Task<IActionResult> Register(UserForRegisterDto userForRegisterDto)
{
var userExists = await _authService.UserExists(userForRegisterDto.Email);
if (!userExists.IsSuccess)
return BadRequest(userExists.Message);
var registerResult = await _authService.Register(userForRegisterDto);
var result = await _authService.CreateAccessToken(registerResult.Data);
if (result.IsSuccess)
return Ok(result.Data);
return BadRequest(result.Message);
}
}
}
Program.cs
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Services.DependencyResolvers.Autofac;
using Shared.Utilities.Security.Encyption;
using Shared.Utilities.Security.Jwt;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()).ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule(new AutofacBusinessModule());
});
var tokenOptions = builder.Configuration.GetSection(key: "TokenOptions").Get<TokenOptions>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
IssuerSigningKey = SecurityKeyHelper.CreateSecurityKey(tokenOptions.SecurityKey),
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
CreateTokenPostman.cs
enter image description here
401ErrorPostman.cs
enter image description here
Thanks. I found the error related to the latest version of JWT. The error was fixed when I downloaded the old version of JWT.
The version I used is 6.0.7:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.7" />
I have compared your code with this repo. And I find something, it maybe useful to you.
You should change your code like below.
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
IssuerSigningKey = SecurityKeyHelper.CreateSecurityKey(tokenOptions.SecurityKey),
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
//ClockSkew = TimeSpan.Zero
};
Or
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
IssuerSigningKey = SecurityKeyHelper.CreateSecurityKey(tokenOptions.SecurityKey),
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
// add this line
RequireExpirationTime = true
};

Swashbuckle extension version switch

I am new to asp.net core and I am using swagger. I downloaded it by following the steps Install-Package Swashbuckle.AspNetCore -Version 5.6.3.Then add middleware
services.AddSwaggerGen();Then add app.UseSwagger(c =>
{
c.SerializeAsV2 = true;
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
}); to the ge
nerated JSON document and Swagger UI.
Finally add the header information as per the documentation.
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "ToDo API",
Description = "A simple example ASP.NET Core Web API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "Shayne Boyer",
Email = string.Empty,
Url = new Uri("https://twitter.com/spboyer"),
},
License = new OpenApiLicense
{
Name = "Use under LICX",
Url = new Uri("https://example.com/license"),
}
});
});
These are all requirements to follow the documentation. But I need to extend the requirements now, I need to add the version information of the API, similar to the API V1 API V2 version switch. I have referenced some sources but don't have the complete code, can you guys help me? Any help is excellent! !
Are you trying to switch versions like this?
First I created 2 versions of folders and controllers.Therefore, the namespace of each controller corresponds to its folder, like this:
V1 version:
namespace WebApplication129.Controllers.V1
{
[ApiController]
[Route("api/v1/[controller]")]
public class HomeController : ControllerBase
{
[Route("test")]
[HttpGet]
public string Test()
{
return "v1 test";
}
}
}
V2 version:
namespace WebApplication129.Controllers.V2
{
[ApiController]
[Route("api/v2/[controller]")]
public class HomeController : ControllerBase
{
[Route("test")]
[HttpGet]
public string Test()
{
return "v2 test";
}
}
}
Then create an agreement to inform Swagger, in this way, we can control how Swagger generates Swagger documents, thereby controlling the UI.
Create the following class:
public class GroupingByNamespaceConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
var controllerNamespace = controller.ControllerType.Namespace;
var apiVersion = controllerNamespace.Split(".").Last().ToLower();
if (!apiVersion.StartsWith("v")) { apiVersion = "v1"; }
controller.ApiExplorer.GroupName = apiVersion;
}
}
Now we must apply the convention. For that we go to AddControllers in ConfigureServices and add the convention:
services.AddControllers(options =>
{
options.Conventions.Add(new GroupingByNamespaceConvention());
});
The final complete startup.cs configuration is as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using System;
using WebApplication129.Controllers.conf;
namespace WebApplication129
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.Conventions.Add(new GroupingByNamespaceConvention());
});
services.AddSwaggerGen(config =>
{
var titleBase = "Test API";
var description = "This is a Web API for Test operations";
var TermsOfService = new Uri("https://xxxxxx");
var License = new OpenApiLicense()
{
Name = "Test"
};
var Contact = new OpenApiContact()
{
Name = "Test",
Email = "Test#hotmail.com",
Url = new Uri("https://xxxxxx")
};
config.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = titleBase + " v1",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
config.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = titleBase + " v2",
Description = description,
TermsOfService = TermsOfService,
License = License,
Contact = Contact
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(config =>
{
config.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
config.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

How to authenticate AAD user in Identity Server 4?

I have integrated the Azure Active Directory in Identity Server 4 as an external provider.
I want to authenticate Azure Active Directory users from Identity Server 4 by using the APIs.
Here is the code:
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1",
UserName = "ad user name",
Password = "add user password"
});
When I execute this piece of code, I got an invalid username or password error.
Note: the provided credentials are valid.
Here is my startup.cs file
using IdentityServer4;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using MorpheusIdentityServer.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4.Validation;
namespace MorpheusIdentityServer
{
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
string connectionString = Configuration.GetSection("ConnectionString").Value;
var builder = services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddProfileService<ProfileService>();
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = Configuration.GetSection("ActiveDirectoryAuthority").Value;
options.ClientId = Configuration.GetSection("ActiveDirectoryClientId").Value;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.CallbackPath = "/signin-aad";
options.SignedOutCallbackPath = "/signout-callback-aad";
options.RemoteSignOutPath = "/signout-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
ValidateIssuer = false
};
});
services.AddSingleton<IResourceOwnerPasswordValidator, ValidateExternalUser>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
InitializeDatabase(app);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
private void InitializeDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
context.Database.Migrate();
if (!context.Clients.Any())
{
foreach (var client in Config.Clients)
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
foreach (var resource in Config.IdentityResources)
{
context.IdentityResources.Add(resource.ToEntity());
}
context.SaveChanges();
}
if (!context.ApiScopes.Any())
{
foreach (var resource in Config.ApiScopes)
{
context.ApiScopes.Add(resource.ToEntity());
}
context.SaveChanges();
}
}
}
}
}
By default Resource Owner Password workflow uses the connect/token endpoint which validates the users who are present in the AspNetUser table only (it will not validate external users), so that's the reason you are getting the Invalid User name or password error message because your user is present in Microsoft AD and not in ID4 database.
Log in using the Resource Owner Password password isn't the recommended approach but if you still need it anyway, you can override the endpoint implementation and write your own custom logic to authenticate the user with a Microsoft AD data source.
I have searched and found some possible ways to customize usernames and passwords from external providers. Here are the steps
1- Implement a class like below
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var username= context.UserName;
var password= context.Password;
// write the code which will validate the username and password from external provider.
}
}
2- Then register this interface in a startup.cs file like below:
services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
So, when you try to call the endpoint /connect/token this class will be triggered.

Customizable Authorize attribute in asp.net core

I am using Jwt Token authentication in Asp.Net Core, in order to authenticate I am using built in attribute [Authorize], what I want to do is that make this attribute customizable, means I want to make this attribute configurable, to either enable it or disable authentication and I can place the setting in appsetting.json.
According to your description, I suggest you could try to use Policy-based authorization to achieve your requirement.
I suggest you could write a custom Handler to compare the appsetting.json value and the route value.
If the appsetting.json value contains the request the route value, then you could return Succeed to avoid auth.
More details, you could refer to below codes:
Appseting.json(DisableAuthController value)
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"DisableAuthController": "home,default,account"
}
}
Create a new class named: UserResourceHandler
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace FormAuthCore
{
public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
private readonly IConfiguration Configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
public UserResourceHandler(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
Configuration = configuration;
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
{
var re = Configuration.GetSection("DisableAuthController").Value;
var context = _httpContextAccessor.HttpContext.GetRouteData();
var area = (context.Values["area"] as string)?.ToLower();
var controller = (context.Values["controller"] as string)?.ToLower();
var action = (context.Values["action"] as string)?.ToLower();
if (re.Contains(controller))
{
authHandlerContext.Succeed(requirement);
}
}
}
}
Create a UserResourceRequirement class
using Microsoft.AspNetCore.Authorization;
namespace FormAuthCore
{
public class UserResourceRequirement : IAuthorizationRequirement { }
}
Add below codes into Startup.cs ConfigureServices method:
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add(new UserResourceRequirement()));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
Enable Policy-based authorization for the controller:
[Authorize(Policy = "UserResource")]
public class HomeController : Controller
Update:
I added token1 jwt token auth in the startup.cs
services.AddAuthentication("Token1")
.AddJwtBearer("Token1", options =>
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuer = "abc",
ValidateAudience = true,
ValidAudience = "abc",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")),
};
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
var Token = context.Request.Headers["UserCred1"].ToString();
context.Token = Token;
return Task.CompletedTask;
},
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("UserResource", policy => policy.Requirements.Add(new UserResourceRequirement()));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
Then I could use _httpContextAccessor.HttpContext.AuthenticateAsync("Token1").Result to check if the token is valid or not.
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
{
var re = Configuration.GetSection("DisableAuthController").Value;
var context = _httpContextAccessor.HttpContext.GetRouteData();
var re2 = _httpContextAccessor.HttpContext.AuthenticateAsync("Token1").Result;
var area = (context.Values["area"] as string)?.ToLower();
var controller = (context.Values["controller"] as string)?.ToLower();
var action = (context.Values["action"] as string)?.ToLower();
if (re.Contains(controller) || re2.Succeeded)
{
authHandlerContext.Succeed(requirement);
}
}

Web Api Bearer JWT Token Authentication with Api Key fails on successive calls after successful token authentication

I have created an MVC Core API that authenticates users with an api key. On successful authentication it sends back a JWT token which they use for any subsequent requests.
I can successfully get authenticated with a valid api key and get a token as a response back. Then i can use this token to make a request but the next request fails.
In my real application the consumer is an MVC Core site and until now i hadn't noticed this issue because in every mvc controller action i was calling one api action but now that i have the need to call two api actions one after the other on the same mvc action the second one fails and i cannot understand why.
I have reproduced my issue in a sample web api and console application.
This is the code for the MVC Core API endpoint validating the api key and generating the jwt token:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using PureHub.Services.Models.Valid8.Authentication;
namespace BugApiJwt.Controllers
{
[Authorize]
[Route("v1/[controller]")]
public class AuthenticationController : ControllerBase
{
[AllowAnonymous]
[HttpPost("[action]")]
public virtual async Task<IActionResult> Token([FromBody] ApiLoginRequest model)
{
if (model != null)
{
if (model.ApiKey == "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R")
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new Claim(JwtRegisteredClaimNames.Iat,
new DateTimeOffset(DateTime.UtcNow).ToUniversalTime().ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ=="));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "http://localhost:58393/",
audience: "http://localhost:58393/",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: creds);
return Ok(new ApiLoginResponse
{
Token = new JwtSecurityTokenHandler().WriteToken(token),
Expiration = token.ValidTo
});
}
}
return BadRequest();
}
}
}
This is the protected resource:
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BugApiJwt.Controllers
{
[Authorize]
[Route("v1/values")]
public class ValuesController : Controller
{
[HttpGet]
public IEnumerable<string> Get()
{
return new[] { "value1", "value2" };
}
[HttpGet("{id}")]
public string Get(int id)
{
return $"You said: {id}";
}
}
}
And this is my startup:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace BugApiJwt
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "http://localhost:58393/",
ValidAudience = "http://localhost:58393/",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("FTTaIMmkh3awD/4JF0iHgAfNiB6/C/gFeDdrKU/4YG1ZK36o16Ja4wLO+1Qft6yd+heHPRB2uQqXd76p5bXXPQ==")),
};
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
}
And this is the console application i'm testing it with:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace BugApiJwt.Console
{
public class Program
{
private const string ApiKey = "VdPfwrL+mpRHKgzAIm9js7e/J9AbJshoPgv1nIZiat22R";
private const string BaseAddress = "http://localhost:58393/";
private static HttpClient _client = new HttpClient();
private static string _realToken = string.Empty;
private static void Main()
{
_client = new HttpClient
{
BaseAddress = new Uri(BaseAddress)
};
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// Works
System.Console.WriteLine("Call GetOne");
var getOne = Get().GetAwaiter().GetResult();
System.Console.WriteLine(getOne);
// Fails
System.Console.WriteLine("Call GetAll");
var getTwo = GetAll().GetAwaiter().GetResult();
System.Console.WriteLine(getTwo);
System.Console.WriteLine("All Finished. Press Enter to exit");
System.Console.ReadLine();
}
private static async Task<string> GetAuthenticationToken()
{
const string resource = "v1/authentication/token";
if (!string.IsNullOrEmpty(_realToken)){return _realToken;}
var loginRequest = new ApiLoginRequest{ApiKey = ApiKey};
var httpResponseMessage = await _client.PostAsync(resource, ObjectToJsonContent(loginRequest)).ConfigureAwait(false);
if (httpResponseMessage.IsSuccessStatusCode)
{
var content = await httpResponseMessage.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<ApiLoginResponse>(content);
_realToken = obj.Token;
return obj.Token;
}
throw new Exception("Token is null");
}
public static async Task<string> Get()
{
var resource = "v1/values/1";
var token = await GetAuthenticationToken();
_client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");
var httpResponseMessage = await _client.GetAsync(resource);
System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content;
}
public static async Task<string> GetAll()
{
var resource = "v1/values";
var token = await GetAuthenticationToken();
_client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bearer {token}");
var httpResponseMessage = await _client.GetAsync(resource);
System.Console.WriteLine(httpResponseMessage.RequestMessage.Headers.Authorization);
System.Console.WriteLine(httpResponseMessage.Headers.WwwAuthenticate);
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content;
}
private static StringContent ObjectToJsonContent<T>(T objectToPost) where T : class, new()
{
var tJson = JsonConvert.SerializeObject(objectToPost,
Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
return new StringContent(tJson, Encoding.UTF8, "application/json");
}
}
public class ApiLoginRequest
{
public string ApiKey { get; set; }
}
public class ApiLoginResponse
{
public string Token { get; set; }
public DateTime Expiration { get; set; }
}
}
Any help on why the second call fails?
The error message shown in the web api output window is:
Bearer was not authenticated. Failure message: No SecurityTokenValidator available for token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIyNmFiNjQzYjFjOTM0MzYwYjI4NDAxMzZjNDIxOTBlZSIsImlhdCI6MTUxMDA2NDg0MywiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiR2xvYmFsSWQiOiI2NjVjYWEzYjYxYmY0MWRmOGIzMTVhODY5YzQzMmJkYyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluaXN0cmF0b3IiLCJuYmYiOjE1MTAwNjQ4NDMsImV4cCI6MTUxMDA2NjY0MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo0NDM2MCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQzNjAifQ.wJ86Ut2dmbDRDCNXU2kWXeQ1pQGkiVtUx7oSyJIZMzc
It doesn't work because this piece of code TryAddWithoutValidation("Authorization", $"Bearer {token}"); adds the token on top of what is already in the authorization header without clearing it first. As a result successive calls add the bearer string with the token in the header which already contains the bearer token.