I'm new to writing Web APIs in .NET. I wrote this API which is working fine normally but then I added JWT authentication and now when I provide correct username and password I get an authentication bearer token that I add to swagger UI but now when I try to access any other end point I get this 401 Unauthorized status. I'm unable to understand why. I've also tried this with Postman but same response.
Here is my Program.cs
using System.Text;
using Comply_Api_DotNet.Database;
using Comply_Api_DotNet.Repository;
using Comply_Api_DotNet.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddScoped<IUsersDb, UsersDb>();
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.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = #"Please provide authorization token to access restricted features.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
// ADD JWT Authentication
builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
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"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Her is my Controller.
[Authorize]
[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);
}
// GET api/<UsersController>/5
[HttpGet]
public IEnumerable<User> Get(long id)
{
var usersFound = _usersDb.GetAllUsers(id);
return usersFound;
}
// POST api/<UsersController>
[HttpPost]
public User Post([FromBody] User user)
{
var userAdded = _usersDb.AddNewUser(user);
return userAdded;
}
// PUT api/<UsersController>/5
[HttpPut("{id:long}")]
public void Put(long id, [FromBody] User user)
{
throw new NotImplementedException();
}
[HttpDelete("{id:long}")]
public bool Delete(long id)
{
return _usersDb.DeleteUser(id);
}
} // end of class
appsettings.Json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"JWT": {
"Key": "fc746b61cde4f6665d3f9791446cd5395661860c0075a905ed9810b7391af467",
"Issuer": "Comply",
"Audience": "comply"
}
}
UPDATE: Authentication Service
public class AuthenticationService : IAuthenticationService
{
private readonly IConfiguration _configuration;
private readonly IUsersDb _usersDb;
public AuthenticationService(IConfiguration configuration, IUsersDb usersDb)
{
_configuration = configuration;
_usersDb = usersDb;
}
public AuthenticationToken? Authenticate(User user)
{
var foundUser = _usersDb.GetAllUsers(0)
.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 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),
Issuer = _configuration["JWT:Issuer"],
Audience = _configuration["JWT:Audience"],
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return new AuthenticationToken()
{
Token = tokenHandler.WriteToken(token),
};
}
} //end of class
The issue is here Type = SecuritySchemeType.ApiKey, you are specifying security scheme type as apiKey. you need to replace that with Type = SecuritySchemeType.Http,. So, your OpenApiSecurityScheme should now look like this.
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = #"Please provide authorization token to access restricted features.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
});
In swagger Ui, You should add Bearer before Token. Reference:Bearer Authentication
Authorization: Bearer <token>
As you can see in Postman Headers, It has a Bearer.
Related
I'm trying to implement JWT based authentication in my ASP.NET Core 5 Web API. However, I always end up with the response code 401 when using my APIs marked with the [Authorize] attribute.
Here's what I have so far. First, my AccountController issues a JWT if the user provides a valid username and password:
[Authorize]
[ApiController]
[Route("api/" + Constants.ApiVersion + "/Accounts")]
public class AccountController : ControllerBase
{
private readonly UserManager<AppUser> _userManager;
private readonly IPasswordHasher<AppUser> _passwordHasher;
public AccountController(UserManager<AppUser> userManager, IPasswordHasher<AppUser> passwordHasher)
{
_userManager = userManager;
_passwordHasher = passwordHasher;
}
[AllowAnonymous]
[HttpPost]
[Route("Token")]
public async Task<IActionResult> Login([FromForm]LoginBindingModel model)
{
if(model == null)
{
return BadRequest();
}
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
AppUser user = await _userManager.FindByNameAsync(model.UserName);
if(user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
{
return Unauthorized();
}
SymmetricSecurityKey encryptionKey = new(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"));
JwtSecurityTokenHandler jwtTokenHandler = new();
SecurityTokenDescriptor tokenDescriptor = new()
{
Subject = new ClaimsIdentity(new[] { new Claim("UserName", user.UserName) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(encryptionKey, SecurityAlgorithms.HmacSha256Signature)
};
SecurityToken jwtToken = jwtTokenHandler.CreateToken(tokenDescriptor);
string token = jwtTokenHandler.WriteToken(jwtToken);
return Ok(token);
}
[HttpPost]
[Route("ChangePassword")]
public async Task<ActionResult> ChangePassword([FromBody]ChangePasswordBindingModel model)
{
if(model == null)
{
return BadRequest();
}
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
AppUser user = await _userManager.GetUserAsync(User);
if(user == null)
{
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
IdentityResult result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
return GetHttpResponse(result);
}
...
}
This code seems to work as it should. It returns a token that is successfully parsed by jwt.io and contains the username I put into it.
Next, the Startup class looks as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration
{
get;
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApplicationSettings>(Configuration.GetSection(nameof(ApplicationSettings)));
services.AddIdentityCore<AppUser>(options =>
{
Configuration.GetSection(nameof(IdentityOptions)).Bind(options);
});
services.AddScoped<IPasswordHasher<AppUser>, Identity.PasswordHasher<AppUser>>();
services.AddTransient<IUserStore<AppUser>, UserStore>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "whatever",
ValidateAudience = true,
ValidAudience = "whatever",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"))
};
});
services.AddMvc();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if(env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I'm sending an HTTP POST request to the Token route which returns me the JWT. After that I'm sending an HTTP POST request with the necessary JSON data in the request body and Authorization: Bearer <the JWT> in the header to the ChangePassword route.
However, that always returns me response code 401 without any additional information or exception.
I'm unaware what the magic in Startup.ConfigureServices is actually supposed to do behind the scenes. Anyway, it obviously doesn't work. Does anyone know what is going on and what to do to make it work?
However, that always returns me response code 401 without any
additional information or exception.
That is because you set ValidateIssuer and ValidateAudience true but there is no Issuer and Audience in the generated token.
One way is that you can set Issuer and Audience in code:
SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor()
{
Issuer= "whatever",
Audience= "whatever",
Subject = new ClaimsIdentity(new[] { new Claim("UserName", user.Name) }),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(encryptionKey, SecurityAlgorithms.HmacSha256Signature)
};
Another way is that you can set ValidateIssuer and ValidateAudience false:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false, //change here..
ValidIssuer = "whatever",
ValidateAudience = false, //change here..
ValidAudience = "whatever",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("TODO_Find_better_key_and_store_as_secret"))
};
});
I have configured ocelot in Linux containers with multiple micro service. For restricting some of the micro services I'm using RouteClaimsRequirement. I have administrator role as claim, but when I send token with role administrator the Ocelot returns 403 Forbidden, which is the HttpCode for not meeting the criteria in RouteClaimsRequirement. If I delete the RouteClaimsRequirment from ocelot.json everything is working.
{
"DownstreamPathTemplate": "/api/v1/product/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "product",
"Port": 443
}
],
"UpstreamPathTemplate": "/product/{everything}",
"UpstreamHttpMethod": [ "Get", "Post", "Delete" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "Bearer",
"AllowedScopes": []
},
"RouteClaimsRequirement": { <---- Problem Part
"Role": "Administrator"
},
"DangerousAcceptAnyServerCertificateValidator": true,
"RateLimitOptions": {
"ClientWhitelist": [],
"EnableRateLimiting": true,
"Period": "5s",
"PeriodTimespan": 6,
"Limit": 8
}
}
Here how the ocelot project startup class looks like:
public void ConfigureServices(IServiceCollection services)
=> services
.AddCors()
.AddTokenAuthentication(Configuration)
.AddOcelot();
public static IServiceCollection AddTokenAuthentication(
this IServiceCollection services,
IConfiguration
configuration,
JwtBearerEvents events = null)
{
var secret = configuration
.GetSection(nameof(ApplicationSettings))
.GetValue<string>(nameof(ApplicationSettings.Secret));
var key = Encoding.ASCII.GetBytes(secret);
services
.AddAuthentication(authentication =>
{
authentication.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
authentication.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(bearer =>
{
bearer.RequireHttpsMetadata = false;
bearer.SaveToken = true;
bearer.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
if (events != null)
{
bearer.Events = events;
}
});
services.AddHttpContextAccessor();
services.AddScoped<ICurrentUserService, CurrentUserService>();
return services;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app
.UseCors(options => options
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod())
.UseAuthentication()
.UseAuthorization()
.UseOcelot().Wait();
}
Token generation looks like this:
public string GenerateToken(User user, IEnumerable<string> roles = null)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(this.applicationSettings.Secret);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.Email)
};
if (roles != null)
{
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var encryptedToken = tokenHandler.WriteToken(token);
return encryptedToken;
}
Decrypted token:
{
"nameid": "e18d5f1f-a315-435c-9e38-df9f2c77ad20",
"unique_name": "test#aa.bg",
"role": "Administrator",
"nbf": 1595460189,
"exp": 1596064989,
"iat": 1595460189
}
After a lot of research in the code and debugging, I manage to find the problem. It came from the claim name, it is not role, it is http://schemas.microsoft.com/ws/2008/06/identity/claims/role, but when you write it down in the ocelot.json it is recognized wrongly because of the semicolon.
Midway researching it, I find an answer in github of a person which tackle the same problem and I copied his solution. Here link to the solution.
How the problem is handled: We are writing the url with special symbol instead of : and after that we rewrite it to the proper one, so the ocelot.json configuration does not throw the problem.
First you need to create IClaimsAuthoriser
public class ClaimAuthorizerDecorator : IClaimsAuthoriser
{
private readonly ClaimsAuthoriser _authoriser;
public ClaimAuthorizerDecorator(ClaimsAuthoriser authoriser)
{
_authoriser = authoriser;
}
public Response<bool> Authorise(ClaimsPrincipal claimsPrincipal, Dictionary<string, string> routeClaimsRequirement, List<PlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
{
var newRouteClaimsRequirement = new Dictionary<string, string>();
foreach (var kvp in routeClaimsRequirement)
{
if (kvp.Key.StartsWith("http$//"))
{
var key = kvp.Key.Replace("http$//", "http://");
newRouteClaimsRequirement.Add(key, kvp.Value);
}
else
{
newRouteClaimsRequirement.Add(kvp.Key, kvp.Value);
}
}
return _authoriser.Authorise(claimsPrincipal, newRouteClaimsRequirement, urlPathPlaceholderNameAndValues);
}
}
After that service collection extension is needed:
public static class ServiceCollectionExtensions
{
public static IServiceCollection DecorateClaimAuthoriser(this IServiceCollection services)
{
var serviceDescriptor = services.First(x => x.ServiceType == typeof(IClaimsAuthoriser));
services.Remove(serviceDescriptor);
var newServiceDescriptor = new ServiceDescriptor(serviceDescriptor.ImplementationType, serviceDescriptor.ImplementationType, serviceDescriptor.Lifetime);
services.Add(newServiceDescriptor);
services.AddTransient<IClaimsAuthoriser, ClaimAuthorizerDecorator>();
return services;
}
}
In start up you define the extension after adding the ocelot
public void ConfigureServices(IServiceCollection services)
{
services
.AddCors()
.AddTokenAuthentication(Configuration)
.AddOcelot().AddSingletonDefinedAggregator<DashboardAggregator>();
services.DecorateClaimAuthoriser();
}
In the end you need to change the configurational json to this:
"RouteClaimsRequirement": {
"http$//schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator"
}
I created a web api that uses JWT tokens for authorization with a role based policy (based on
this article).
The user logs in generates a token that is used for authorization. I successfully generate the token but when I start to use it to access restricted API actions with it it doesn't work and keeps giving me the 401 HTTP error (I cant even debug considering the action call doesn't trigger). What am I doing wrong?.
Classes:
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();
services.AddScoped<ICountriesService, CountriesService>();
services.AddScoped<ICompanyService, CompanyService>();
services.AddScoped<IPlaneServices, PlaneService>();
services.AddScoped<IPlaneTypeService, PlaneTypeService>();
services.AddScoped<ICitiesService, CitiesService>();
services.AddScoped<IAirfieldService, AirfieldService>();
services.AddScoped<ITicketTypeService, TicketTypeService>();
services.AddScoped<IFlightService, FlightService>();
services.AddScoped<ILuxuryService, LuxuryService>();
services.AddScoped<IUserService, UserService>();
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" });
c.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"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
// var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
//var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
//c.IncludeXmlComments(xmlPath);
});
services.AddAutoMapper(cfg => cfg.AddProfile<Mapper.Mapper>(),
AppDomain.CurrentDomain.GetAssemblies());
services.AddDbContext<FlightMasterContext>();
services.AddCors();
var secret = Configuration.GetValue<string>(
"AppSettings:Secret");
var key = Encoding.ASCII.GetBytes(secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = string.Empty;
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
The controller:
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class AaTestController : ControllerBase
{
private FlightMasterContext db { get; set; }
private IUserService _userService;
public AaTestController(FlightMasterContext db, IUserService userService)
{
this.db = db;
_userService = userService;
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
return Ok(user);
}
//DOESNT TRIGGER
[Authorize(Roles = Role.Admin)]
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
return Ok(users);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
// only allow admins to access other user records
var currentUserId = int.Parse(User.Identity.Name);
if (id != currentUserId && !User.IsInRole(Role.Admin))
return Forbid();
var user = _userService.GetById(id);
if (user == null)
return NotFound();
return Ok(user);
}
}
Service used for authentication and authorization:
public interface IUserService
{
User Authenticate(string username, string password);
IEnumerable<User> GetAll();
User GetById(int id);
}
public class UserService : IUserService
{
// users hardcoded for simplicity, store in a db with hashed passwords in production applications
private List<User> _users = new List<User>
{
new User { Id = 1, FirstName = "Admin", LastName = "User", Username = "admin", Password = "admin", Role = Role.Admin },
new User { Id = 2, FirstName = "Normal", LastName = "User", Username = "user", Password = "user", Role = Role.User }
};
public User Authenticate(string username, string password)
{
var user = _users.SingleOrDefault(x => x.Username == username && x.Password == password);
var secret = "THIS IS Ughjgjhgjhghgighiizgzigiz";
// return null if user not found
if (user == null)
return null;
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role)
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
user.Token = tokenHandler.WriteToken(token);
return user.WithoutPassword();
}
public IEnumerable<User> GetAll()
{
return _users.WithoutPasswords();
}
public User GetById(int id)
{
var user = _users.FirstOrDefault(x => x.Id == id);
return user.WithoutPassword();
}
}
Those methods should be called in reversed order:
app.UseAuthentication();
app.UseAuthorization();
First middleware should authenticate user, and only then next one - authorize. Unfortunately not all MS docs pay attention on this detail.
First middleware should authenticate user, and only then next one - authorize. Unfortunately not all MS docs pay attention on this detail.
I have documented my api using Swashbuckle.AspNetCore.Swagger and I want to test some resources that have Authorize attribute on them using swagger ui.
api
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
namespace Api.Controllers
{
[Route("[controller]")]
[Authorize]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
}
Response code is Unauthorized 401, so how can I authorize this using swagger?
I have an Authorization server setup using IdentityServer4.
authorization server - startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<ApplicationUser>();
authorization server - config.cs
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
...
...
}
api - startup.cs
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
ECommerceDbContext context)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000/",
RequireHttpsMetadata = false,
AutomaticAuthenticate = true,
ApiName = "api1"
});
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
DbInitialiser.Init(context);
app.UseMvc();
}
I want an authorize button which redirects to a login screen and then grants access to api resources which the user has permissions for.
Is it possible to use asp.net core 1.1 Swagger middleware to do this? Or do I need to write some javascript that gets a token from IdentityServer4 authorization server?
Please help as I am new to authentication and authorization
I solved this by adding a new client to the IdentityServer4 Authorization Server project.
config.cs
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
new Client
{
ClientId="swaggerui",
ClientName = "Swagger UI",
AllowedGrantTypes=GrantTypes.Implicit,
AllowAccessTokensViaBrowser=true,
RedirectUris = { "http://localhost:49831/swagger/o2c.html" },
PostLogoutRedirectUris={ "http://localhost:49831/swagger/" },
AllowedScopes = {"api1"}
},
...
...
...
}
}
I created a swagger OperationFilter in tha API so that a red exclamation mark icon appears next to the method that requires authorization
internal class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.ApiDescription.ControllerAttributes().OfType<AuthorizeAttribute>().Any() ||
context.ApiDescription.ActionAttributes().OfType<AuthorizeAttribute>().Any();
if (hasAuthorize)
{
operation.Responses.Add("401", new Response { Description = "Unauthorized" });
operation.Responses.Add("403", new Response { Description = "Forbidden" });
operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
operation.Security.Add(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new [] { "api1" } }
});
}
}
}
To finish I configured authorization in swagger by adding an oauth2 security definition and operationfilter
startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "ECommerce API",
Description = "",
TermsOfService = "None",
Contact = new Contact { Name = "", Email = "", Url = "" },
License = new License { Name = "", Url = "" }
});
//Set the comments path for the swagger json and ui.
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var xmlPath = Path.Combine(basePath, "WebApi.xml");
c.IncludeXmlComments(xmlPath);
c.OperationFilter<AuthorizeCheckOperationFilter>();
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = "http://localhost:5000/connect/authorize",
TokenUrl = "http://localhost:5000/connect/token",
Scopes = new Dictionary<string, string>()
{
{ "api1", "My API" }
}
});
});
As mentioned by James in the comment to the accepted answer, the way to check the Authorize attribute is slightly different now, the AuthorizeCheckOperationFilterin the answer needs slightly tweaking, this may not 100% be the best way to do this, however I've not had any problems with the code below.
internal class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out var methodInfo);
if (methodInfo == null)
return;
var hasAuthorizeAttribute = false;
if (methodInfo.MemberType == MemberTypes.Method)
{
// NOTE: Check the controller itself has Authorize attribute
hasAuthorizeAttribute = methodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
// NOTE: Controller has Authorize attribute, so check the endpoint itself.
// Take into account the allow anonymous attribute
if (hasAuthorizeAttribute)
hasAuthorizeAttribute = !methodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any();
else
hasAuthorizeAttribute = methodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
}
if (!hasAuthorizeAttribute)
return;
operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new Response { Description = "Unauthorized" });
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new Response { Description = "Forbidden" });
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the string[]
// which will indicate which permission you require.
operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
operation.Security.Add(new Dictionary<string, IEnumerable<string>>
{
{ "Bearer", new string[] { } }
});
}
}
For .Net Core and Swashbuckle.AspNetCore 5.4.1, the following is a working update of #Dan's response:
Create an IOperationalFilter class:
internal class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out var methodInfo);
if (methodInfo == null)
return;
var hasAuthorizeAttribute = false;
if (methodInfo.MemberType == MemberTypes.Method)
{
// NOTE: Check the controller itself has Authorize attribute
hasAuthorizeAttribute = methodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
// NOTE: Controller has Authorize attribute, so check the endpoint itself.
// Take into account the allow anonymous attribute
if (hasAuthorizeAttribute)
hasAuthorizeAttribute = !methodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any();
else
hasAuthorizeAttribute = methodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any();
}
if (!hasAuthorizeAttribute)
return;
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status401Unauthorized.ToString()))
operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse { Description = "Unauthorized" });
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status403Forbidden.ToString()))
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse { Description = "Forbidden" });
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the string[]
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header
},
new List<string>()
}
}
};
}
}
Wire up the filter and security definition in Startup:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSwaggerGen(c =>
{
c.OperationFilter<AuthorizeCheckOperationFilter>();
c.EnableAnnotations();
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Scheme = "Bearer",
In = ParameterLocation.Header,
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
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'"
});
});
...
}
I've been on quite an adventure to get JWT working on DotNet core 2.0 (now reaching final release today). There is a ton of documentation, but all the sample code seems to be using deprecated APIs and coming in fresh to Core, It's positively dizzying to figure out how exactly it's supposed to be implemented. I tried using Jose, but app. UseJwtBearerAuthentication has been deprecated, and there is no documentation on what to do next.
Does anyone have an open source project that uses dotnet core 2.0 that can simply parse a JWT from the authorization header and allow me to authorize requests for a HS256 encoded JWT token?
The class below doesn't throw any exceptions, but no requests are authorized, and I get no indication why they are unauthorized. The responses are empty 401's, so to me that indicates there was no exception, but that the secret isn't matching.
One odd thing is that my tokens are encrypted with the HS256 algorithm, but I see no indicator to tell it to force it to use that algorithm anywhere.
Here is the class I have so far:
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace Site.Authorization
{
public static class SiteAuthorizationExtensions
{
public static IServiceCollection AddSiteAuthorization(this IServiceCollection services)
{
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SECRET_KEY"));
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
ValidateAudience = false,
ValidateIssuer = false,
IssuerSigningKeys = new List<SecurityKey>{ signingKey },
// Validate the token expiry
ValidateLifetime = true,
};
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.IncludeErrorDetails = true;
o.TokenValidationParameters = tokenValidationParameters;
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 401;
c.Response.ContentType = "text/plain";
return c.Response.WriteAsync(c.Exception.ToString());
}
};
});
return services;
}
}
}
Here is a full working minimal sample with a controller. I hope you can check it using Postman or JavaScript call.
appsettings.json, appsettings.Development.json. Add a section. Note, Key should be rather long and Issuer is an address of the service:
...
,"Tokens": {
"Key": "Rather_very_long_key",
"Issuer": "http://localhost:56268/"
}
...
!!! In real project, don't keep Key in appsettings.json file. It should be kept in Environment variable and take it like this:
Environment.GetEnvironmentVariable("JWT_KEY");
UPDATE: Seeing how .net core settings work, you don't need to take it exactly from Environment. You may use setting. However,instead we may write this variable to environment variables in production, then our code will prefer environment variables instead of configuration.
AuthRequest.cs : Dto keeping values for passing login and password:
public class AuthRequest
{
public string UserName { get; set; }
public string Password { get; set; }
}
Startup.cs in Configure() method BEFORE app.UseMvc() :
app.UseAuthentication();
Startup.cs in ConfigureServices() :
services.AddAuthentication()
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
};
});
Add a controller:
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly IConfiguration _config;
private readonly IUserManager _userManager;
public TokenController(IConfiguration configuration, IUserManager userManager)
{
_config = configuration;
_userManager = userManager;
}
[HttpPost("")]
[AllowAnonymous]
public IActionResult Login([FromBody] AuthRequest authUserRequest)
{
var user = _userManager.FindByEmail(model.UserName);
if (user != null)
{
var checkPwd = _signInManager.CheckPasswordSignIn(user, model.authUserRequest);
if (checkPwd)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_config["Tokens:Issuer"],
_config["Tokens:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
}
return BadRequest("Could not create token");
}}
That's all folks! Cheers!
UPDATE: People ask how get Current User. Todo:
In Startup.cs in ConfigureServices() add
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
In a controller add to constructor:
private readonly int _currentUser;
public MyController(IHttpContextAccessor httpContextAccessor)
{
_currentUser = httpContextAccessor.CurrentUser();
}
Add somewhere an extension and use it in your Controller (using ....)
public static class IHttpContextAccessorExtension
{
public static int CurrentUser(this IHttpContextAccessor httpContextAccessor)
{
var stringId = httpContextAccessor?.HttpContext?.User?.FindFirst(JwtRegisteredClaimNames.Jti)?.Value;
int.TryParse(stringId ?? "0", out int userId);
return userId;
}
}
My tokenValidationParameters works when they look like this:
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = GetSignInKey(),
ValidateIssuer = true,
ValidIssuer = GetIssuer(),
ValidateAudience = true,
ValidAudience = GetAudience(),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
and
static private SymmetricSecurityKey GetSignInKey()
{
const string secretKey = "very_long_very_secret_secret";
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
return signingKey;
}
static private string GetIssuer()
{
return "issuer";
}
static private string GetAudience()
{
return "audience";
}
Moreover, add options.RequireHttpsMetadata = false like this:
.AddJwtBearer(options =>
{
options.TokenValidationParameters =tokenValidationParameters
options.RequireHttpsMetadata = false;
});
EDIT:
Dont forget to call
app.UseAuthentication();
in Startup.cs -> Configure method before app.UseMvc();
Asp.net Core 2.0 JWT Bearer Token Authentication Implementation with Web Api Demo
Add Package "Microsoft.AspNetCore.Authentication.JwtBearer"
Startup.cs ConfigureServices()
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "me",
ValidAudience = "you",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
};
});
Startup.cs Configure()
// ===== Use Authentication ======
app.UseAuthentication();
User.cs // It is a model class just for example. It can be anything.
public class User
{
public Int32 Id { get; set; }
public string Username { get; set; }
public string Country { get; set; }
public string Password { get; set; }
}
UserContext.cs // It is just context class. It can be anything.
public class UserContext : DbContext
{
public UserContext(DbContextOptions<UserContext> options) : base(options)
{
this.Database.EnsureCreated();
}
public DbSet<User> Users { get; set; }
}
AccountController.cs
[Route("[controller]")]
public class AccountController : Controller
{
private readonly UserContext _context;
public AccountController(UserContext context)
{
_context = context;
}
[AllowAnonymous]
[Route("api/token")]
[HttpPost]
public async Task<IActionResult> Token([FromBody]User user)
{
if (!ModelState.IsValid) return BadRequest("Token failed to generate");
var userIdentified = _context.Users.FirstOrDefault(u => u.Username == user.Username);
if (userIdentified == null)
{
return Unauthorized();
}
user = userIdentified;
//Add Claims
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
new Claim(JwtRegisteredClaimNames.Sub, "data"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken("me",
"you",
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return Ok(new
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
expires_in = DateTime.Now.AddMinutes(30),
token_type = "bearer"
});
}
}
UserController.cs
[Authorize]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
private readonly UserContext _context;
public UserController(UserContext context)
{
_context = context;
if(_context.Users.Count() == 0 )
{
_context.Users.Add(new User { Id = 0, Username = "Abdul Hameed Abdul Sattar", Country = "Indian", Password = "123456" });
_context.SaveChanges();
}
}
[HttpGet("[action]")]
public IEnumerable<User> GetList()
{
return _context.Users.ToList();
}
[HttpGet("[action]/{id}", Name = "GetUser")]
public IActionResult GetById(long id)
{
var user = _context.Users.FirstOrDefault(u => u.Id == id);
if(user == null)
{
return NotFound();
}
return new ObjectResult(user);
}
[HttpPost("[action]")]
public IActionResult Create([FromBody] User user)
{
if(user == null)
{
return BadRequest();
}
_context.Users.Add(user);
_context.SaveChanges();
return CreatedAtRoute("GetUser", new { id = user.Id }, user);
}
[HttpPut("[action]/{id}")]
public IActionResult Update(long id, [FromBody] User user)
{
if (user == null)
{
return BadRequest();
}
var userIdentified = _context.Users.FirstOrDefault(u => u.Id == id);
if (userIdentified == null)
{
return NotFound();
}
userIdentified.Country = user.Country;
userIdentified.Username = user.Username;
_context.Users.Update(userIdentified);
_context.SaveChanges();
return new NoContentResult();
}
[HttpDelete("[action]/{id}")]
public IActionResult Delete(long id)
{
var user = _context.Users.FirstOrDefault(u => u.Id == id);
if (user == null)
{
return NotFound();
}
_context.Users.Remove(user);
_context.SaveChanges();
return new NoContentResult();
}
}
Test on PostMan:
Pass TokenType and AccessToken in Header in other webservices.
Best of Luck! I am just Beginner. I only spent one week to start learning asp.net core.
Here is a solution for you.
In your startup.cs, firstly, config it as services:
services.AddAuthentication().AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = "somethong",
ValidAudience = "something",
:
};
});
second, call this services in config
app.UseAuthentication();
now you can use it in your controller by add attribute
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet]
public IActionResult GetUserInfo()
{
For full details source code that use angular as Frond-end see here
Here is my implementation for a .Net Core 2.0 API:
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Add framework services
services.AddMvc(
config =>
{
// This enables the AuthorizeFilter on all endpoints
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}
).AddJsonOptions(opt =>
{
opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
services.AddLogging();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = Configuration["AzureAD:Audience"];
options.Authority = Configuration["AzureAD:AADInstance"] + Configuration["AzureAD:TenantId"];
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAuthentication(); // THIS METHOD MUST COME BEFORE UseMvc...() !!
app.UseMvcWithDefaultRoute();
}
appsettings.json:
{
"AzureAD": {
"AADInstance": "https://login.microsoftonline.com/",
"Audience": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"Domain": "mydomain.com",
"TenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
...
}
The above code enables auth on all controllers. To allow anonymous access you can decorate an entire controller:
[Route("api/[controller]")]
[AllowAnonymous]
public class AnonymousController : Controller
{
...
}
or just decorate a method to allow a single endpoint:
[AllowAnonymous]
[HttpPost("anonymousmethod")]
public async Task<IActionResult> MyAnonymousMethod()
{
...
}
Notes:
This is my first attempt at AD auth - if anything is wrong, please let me know!
Audience must match the Resource ID requested by the client. In our case our client (an Angular web app) was registered separately in Azure AD, and it used its Client Id, which we registered as the Audience in the API
ClientId is called Application ID in the Azure Portal (why??), the Application ID of the app registration for the API.
TenantId is called Directory ID in the Azure Portal (why??), found under Azure Active Directory > Properties
If deploying the API as an Azure hosted Web App, ensure you set the Application Settings:
eg. AzureAD:Audience / xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Just to update on the excellent answer by #alerya I had to modify the helper class to look like this;
public static class IHttpContextAccessorExtension
{
public static string CurrentUser(this IHttpContextAccessor httpContextAccessor)
{
var userId = httpContextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return userId;
}
}
Then I could obtain the userId in my service layer. I know it's easy in the controller, but a challenge further down.