How to authenticate AAD user in Identity Server 4? - asp.net-core

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.

Related

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

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

Multiple authentication schemes in ASP.NET Core 5.0 WebAPI

I have a full set of (ASP.NET Core) web APIs developed in .NET 5.0 and implemented Cookies & OpenIdConnect authentication schemes.
After successful authentication (user id and password) with Azure AD, cookie is generated and stores user permissions etc.
Now, I would like to expose the same set of APIs to a third party consumer using API Key based authentication (via api-key in the request headers).
I have developed a custom authentication handler as below.
using Microsoft.AspNetCore.Authentication;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
}
namespace Management.Deployment.Securities.Authentication
{
public static class ApiKeyAuthenticationDefaults
{
public static readonly string AuthenticationScheme = "ApiKey";
public static readonly string DisplayName = "ApiKey Authentication Scheme";
}
}
ApiKeyAuthenticationHandler is defined as below, straight forward, if the request headers contain the valid api key then add permissions claim (assigned to the api key) and mark the authentication as success else fail.
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Management.Securities.Authorization;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Management.Deployment.Securities.Authentication
{
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
{
private const string APIKEY_NAME = "x-api-key";
private const string APIKEY_VALUE = "sdflasuowerposaddfsadf1121234kjdsflkj";
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
string extractedApiKey = Request.Headers[APIKEY_NAME];
if (!APIKEY_VALUE.Equals(extractedApiKey))
{
return Task.FromResult(AuthenticateResult.Fail("Unauthorized client."));
}
var claims = new[]
{
new Claim("Permissions", "23")
};
var claimsIdentity = new ClaimsIdentity(claims, nameof(ApiKeyAuthenticationHandler));
var authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
}
I have also defined ApiKeyAuthenticationExtensions as below.
using Microsoft.AspNetCore.Authentication;
using Management.Deployment.Securities.Authentication;
using System;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ApiKeyAuthenticationExtensions
{
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, x => { });
}
public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyAuthenticationSchemeOptions> configureOptions)
{
return builder.AddScheme<ApiKeyAuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, ApiKeyAuthenticationDefaults.DisplayName, configureOptions);
}
}
}
Skimmed version of ConfigureServices() in Startup.cs is here. Please note I have used ForwardDefaultSelector.
public void ConfigureServices(IServiceCollection services)
{
IAuthCookieValidate cookieEvent = new AuthCookieValidate();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = ".Mgnt.AspNetCore.Cookies";
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.Events = new CookieAuthenticationEvents
{
OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 403;
return Task.FromResult(0);
},
OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.FromResult(0);
},
OnValidatePrincipal = cookieEvent.ValidateAsync
};
options.ForwardDefaultSelector = context =>
{
return context.Request.Headers.ContainsKey(ApiConstants.APIKEY_NAME) ? ApiKeyAuthenticationDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme;
};
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => Configuration.Bind(OpenIdConnectDefaults.AuthenticationScheme, options))
.AddApiKey();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
}
The Configure method is as below.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHsts();
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
When I send the correct apikey in the request headers, the custom handler is returning success as authentication result and the request is processed further.
But if an incorrect api key is passed, it is not returning the authentication failure message - "Unauthorized client.". Rather, the request is processed further and sending the attached response.
What changes to be made to resolve this issue so the api returns the authentication failure message - "Unauthorized client." and stops further processing of the request?
if you plan to use apikeys, then you are on your own and there is (as far as I know) no built in direct support for API-keys. There is however built in support for JWT based access tokens and I would recommend that you use that as well for external third parties who wants to access your api. Perhaps using client credentials flow.
For some help, see http://codingsonata.com/secure-asp-net-core-web-api-using-api-key-authentication/
I also think you should configure and let the authorization handler be responsible for deciding who can access the services.
see Policy-based authorization in ASP.NET Core

Combine IdentityServer4 and ASP.NET Core Identity Framework

Hi everyone I have a problem with a combination of IdentityServer4 and Identity Framework.
I have 3 projects in my solution.
First, it is an OAuth project with IdentityServer4. This project has the next configuration:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Platform.OAuth.Data;
using Platform.OAuth.Data.Models;
namespace Platform.OAuth
{
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.AddDbContext<ApplicationContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
x => x.MigrationsAssembly(typeof(ApplicationContext).Assembly.FullName));
});
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationContext>();
services.AddControllers();
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryClients(new List<Client>
{
new Client
{
ClientId = "api-client",
ClientName = "API Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
AllowOfflineAccess = true
}
})
.AddInMemoryApiScopes(new List<ApiScope>
{
new ApiScope("api1", "My API")
})
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication();
}
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Second, it is API project that contains some API which I want to protect. This project has next configuration:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace Api
{
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.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:44392"; /// <-- OAuth project url
options.TokenValidationParameters = new TokenValidationParameters
{
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();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Thrid, It is client project that makes calls for both API.
static async Task Main(string[] args)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:44392");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "api-client",
ClientSecret = "secret",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
client.SetBearerToken(tokenResponse.AccessToken);
var testResponse = await client.GetAsync("https://localhost:44392/WeatherForecast");
if(testResponse.IsSuccessStatusCode)
{
var content = await testResponse.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
Console.WriteLine("==============================================================");
var response = await apiClient.GetAsync("https://localhost:44369/WeatherForecast");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}
Both web API projects have default WeatherForecastController which I've protected by AuthorizeAttribute And when the client makes a request with a token to API, the action returns data, but when a request for OAuth, action returns 404 error. I think it is good for the OAuth project but not for the API because API and 'OAuth' projects don't have authorized users. But why the API return data?
Receiving 404 should not be related with any auth issue, since it is the HTTP "not found" code, retrieved precisely when the resource you tried to access is not found with the URL that was used.
Concerning the data received from the API endpoint, it is a successful response, right? I will assume it is.
Following your client code snippet, it seems to me that you get a token from the Identity Server and perform a GET request with the token set. Since the token is valid, the client is authenticated and the API recognizes it.
If you set the [Authorize] attribute without any specified policy, the default applies. The default is: if you are authenticated, you are also authorized; this means that providing a valid token is enough to be authorized - which you do.
I would say that this is why you get a successful response. More information here.

Authorize doesn't work in Signalr of ASP.NET Core 2.1

I've upgraded my project from ASP.Net Core 2.0 to ASP.NET Core 2.1 by following this tutorial.
Everything was fine until I applied Signar Core 2.1 to my project.
This is my Startup.cs
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<IAuthorizationHandler, SolidAccountRequirementHandler>();
services.AddCors(
options => options.AddPolicy("AllowCors",
builder =>
{
builder
.AllowAnyOrigin()
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
})
);
services.AddAuthorization(x =>
{
x.AddPolicy("MainPolicy", builder =>
{
builder.Requirements.Add(new SolidAccountRequirement());
});
});
services.AddSignalR();
#region Mvc builder
var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
authenticationBuilder.AddJwtBearer(o =>
{
// You also need to update /wwwroot/app/scripts/app.js
o.SecurityTokenValidators.Clear();
// Initialize token validation parameters.
var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.ValidAudience = "audience";
tokenValidationParameters.ValidIssuer = "issuer";
tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SigningKey"));
tokenValidationParameters.ValidateLifetime = false;
o.TokenValidationParameters = tokenValidationParameters;
});
// Construct mvc options.
services.AddMvc(mvcOptions =>
{
////only allow authenticated users
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.AddRequirements(new SolidAccountRequirement())
.Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
})
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1); ;
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseCors("AllowCors");
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chathub");
});
app.UseMvc();
}
}
This is my SolidRequirementHandler
public class SolidAccountRequirementHandler : AuthorizationHandler<SolidAccountRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SolidAccountRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
This is my ChatHub.cs:
public class ChatHub : Hub
{
[Authorize(Policy = "MainPolicy")]
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
What I expected was MainPolicy would be called when I used my AngularJS app to connect to ChatHub. However, OnConnectedAsync() function was called without checking request identity.
The policy of MVC Controller was applied successfully, but Signalr's doesn't.
Can anyone help me please ?
Thank you,
I posted this question onto Signalr github issue page.
Here is the answer they gave me .
I tried and it worked successfully:
The solution is to put [Authorize] attribute onto ChatHub
[Authorize(Policy = "MainPolicy")]
public class ChatHub : Hub
{
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
}
Just share to who doesn't know :)
I have the same problem, there are four key things:
1- In your Startup.cs be aware of this Order inside Configure(IApplicationBuilder app)
app.UseRouting();
app.UseAuthorization( );
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<myChat>("/chat");
});
the app.UseAuthorization( ); should always be between app.UseRouting(); and app.UseEndpoints().
2- SignalR doesn't send Tokens in Header but it sends them in Query. In your startup.cs inside ConfigureServices(IServiceCollection services) You have to configure your app in a way to read Tokens from the query and put them in the header. You can Configure your JWT in this way:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidIssuer = [Issuer Site],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes([YOUR SECRET KEY STRING]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var path = context.Request.Path;
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/chat"))
{
context.Request.Headers.Add("Authorization", new[] { $"Bearer {accessToken}" });
}
return Task.CompletedTask;
}
};
});
3- Your Client should send Token when it wants to establish a connection. You can add token to Query when building the connection.
var connection = new signalR.HubConnectionBuilder().withUrl(
"http://localhost:5000/chat", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
accessTokenFactory: () => "My Token Is Here"}).build();
4- I didn't add a default Athuentication scheme inside services.AddAuthentication()
So every time I have to specify my authorization scheme like this. [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
And Finally, You can Protect your Chat Class Like this
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class myChat : Hub
{
///Some functions
}
It seems that Using statements is important, So make sure using the right ones.
SignalR hub Authorize attribute doesn't work
Note: I have a problem with Authorizing only a single method in the myChat class. I don't know why.

Populate custom claim from SQL with Windows Authenticated app in .Net Core

Scenario - .Net Core Intranet Application within Active Directory using SQL Server to manage application specific permissions and extended user identity.
Success to date - User is authenticated and windows claims are available (Name and Groups). Identity.Name can be used to return a domain user model from the database with the extended properties.
Issue and Question - I am trying to then populate one custom claim property "Id" and have that globally available via the ClaimsPrincipal. I have looked into ClaimsTransformation without much success to date. In other articles I have read that you MUST add claims prior to Sign In but can that really be true? That would mean total reliance on AD to fulfil all claims, is that really the case?
Below is my simple code at this point in the HomeController. I am hitting the database and then trying to populate the ClaimsPrincipal but then return the domain user model. I think this could be where my problem lies but I am new to Authorisation in .net and struggling to get my head around claims.
Many thanks for all help received
Current Code:
public IActionResult Index()
{
var user = GetExtendedUserDetails();
User.Claims.ToList();
return View(user);
}
private Models.User GetExtendedUserDetails()
{
var user = _context.User.SingleOrDefault(m => m.Username == User.Identity.Name.Remove(0, 6));
var claims = new List<Claim>();
claims.Add(new Claim("Id", Convert.ToString(user.Id), ClaimValueTypes.String));
var userIdentity = new ClaimsIdentity("Intranet");
userIdentity.AddClaims(claims);
var userPrincipal = new ClaimsPrincipal(userIdentity);
return user;
}
UPDATE:
I have registered ClaimsTransformation
app.UseClaimsTransformation(o => new ClaimsTransformer().TransformAsync(o));
and built ClaimsTransformer as below in line with this github query
https://github.com/aspnet/Security/issues/863
public class ClaimsTransformer : IClaimsTransformer
{
private readonly TimesheetContext _context;
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
System.Security.Principal.WindowsIdentity windowsIdentity = null;
foreach (var i in context.Principal.Identities)
{
//windows token
if (i.GetType() == typeof(System.Security.Principal.WindowsIdentity))
{
windowsIdentity = (System.Security.Principal.WindowsIdentity)i;
}
}
if (windowsIdentity != null)
{
//find user in database
var username = windowsIdentity.Name.Remove(0, 6);
var appUser = _context.User.FirstOrDefaultAsync(m => m.Username == username);
if (appUser != null)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim("Id", Convert.ToString(appUser.Id)));
/*//add all claims from security profile
foreach (var p in appUser.Id)
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim(p.Permission, "true"));
}*/
}
}
return await System.Threading.Tasks.Task.FromResult(context.Principal);
}
}
But am getting NullReferenceException: Object reference not set to an instance of an object error despite having returned the domain model previously.
WITH STARTUP.CS
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Birch.Intranet.Models;
using Microsoft.EntityFrameworkCore;
namespace Birch.Intranet
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization();
// Add framework services.
services.AddMvc();
// Add database
var connection = #"Data Source=****;Initial Catalog=Timesheet;Integrated Security=True;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";
services.AddDbContext<TimesheetContext>(options => options.UseSqlServer(connection));
// Add session
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(60);
options.CookieName = ".Intranet";
});
}
// 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)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseClaimsTransformation(o => new ClaimsTransformer().TransformAsync(o));
app.UseSession();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
You need to use IClaimsTransformer with dependency injection.
app.UseClaimsTransformation(async (context) =>
{
IClaimsTransformer transformer = context.Context.RequestServices.GetRequiredService<IClaimsTransformer>();
return await transformer.TransformAsync(context);
});
// Register
services.AddScoped<IClaimsTransformer, ClaimsTransformer>();
And need to inject DbContext in ClaimsTransformer
public class ClaimsTransformer : IClaimsTransformer
{
private readonly TimesheetContext _context;
public ClaimsTransformer(TimesheetContext dbContext)
{
_context = dbContext;
}
// ....
}