Multiple authentication schemes in ASP.NET Core 5.0 WebAPI - asp.net-core

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

Related

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.

Blazor WASM Hosted - Authorize on API Always returns UnAuthorized

I have a blazor wasm hosted solution that is setup using Role authentication. However, whenever I add an [Authorize] attribute to any of my API Controllers I get a 401 Unauthorized. I know the user has the proper role as the UI is showing and hiding features for that role. Its like the roles are not being passed up to the API. What am I missing?
Server - Starup.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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
//Register the Datacontext and Connection String
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
//Sets up the default Asp.net core Identity Screens - Use Identity Scaffolding to override defaults
services.AddDefaultIdentity<ApplicationUser>( options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequiredUniqueChars = 0;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<DataContext>();
//Associates the User to Context with Identity
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, DataContext>( options =>
{
options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Role);
options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove(JwtClaimTypes.Role);
//Adds authentication handler
services.AddAuthentication().AddIdentityServerJwt();
//Register Repositories for Dependency Injection
services.AddScoped<ICountryRepository, CountryRepository>();
services.AddControllersWithViews();
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
//AutoMigrates data
dataContext.Database.Migrate();
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseSerilogIngestion();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
Client - Program.cs
public class Program
{
public static async Task Main(string[] args)
{
//Serilog
var levelSwitch = new LoggingLevelSwitch();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.Enrich.WithProperty("InstanceId", Guid.NewGuid().ToString("n"))
.WriteTo.BrowserHttp(controlLevelSwitch: levelSwitch)
.CreateLogger();
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("XXX.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("XXX.ServerAPI"));
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<RolesClaimsPrincipalFactory>();
var baseAddress = new Uri($"{builder.HostEnvironment.BaseAddress}api/");
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(client =>
{
client.BaseAddress = apiBaseUrl;
});
}
RegisterTypedClient<ICountryService, CountryService>(baseAddress);
await builder.Build().RunAsync();
}
}
RolesClaimPrincipalFactory.cs
public class RolesClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public RolesClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
ClaimsPrincipal user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
Claim[] roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
if (roleClaims != null && roleClaims.Any())
{
foreach (Claim existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
}
You are likely having this issue since you are using ICountryService that has it's own http client which is not configured to include auth tokens in the outgoing requests -- no tokens, no access.
We can attach tokens by adding an AuthorizationMessageHandler to the client, just like your named client (XXX.ServerAPI) is configured.
Try changing your typed client helper method to this:
/* Client Program.cs */
void RegisterTypedClient<TClient, TImplementation>(Uri apiBaseUrl)
where TClient : class where TImplementation : class, TClient
{
builder.Services.AddHttpClient<TClient, TImplementation>(
client => client.BaseAddress = apiBaseUrl)
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
}
You probably want to change the helper to also only include tokens to client's that actually require them (if you are using that helper for other clients as well)
See the docs for more info.

.net 4.6 web api2 401 Unauthorized with identity server 4

I have already a working identity server 4 in a .net core application.
namespace IdentityServer
{
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("myresourceapi", "My Resource API")
{
Scopes = {new Scope("apiscope")}
}
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
// for public api
new Client
{
ClientId = "secret_client_id",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "apiscope" }
}
};
}
}
}
namespace IdentityServer
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddOperationalStore(options =>
{
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30; // interval in seconds
})
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());
}
// 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.UseIdentityServer();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}
The problem is that now I need to make authenticated requests to a .net 4.6 web api2 (not core). And the IdentityServer4.AccessTokenValidation package doesn't work for that.
According to this question (https://stackoverflow.com/questions/41992272/is-it-possible-to-use-identity-server-4-running-on-net-core-with-a-webapi-app-r) all I have to do is to use the same package that was used for Identity server 3(IdentityServer3.AccessTokenValidation).
This is the code i have implemented in the webapi 2
using IdentityServer3.AccessTokenValidation;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Host.SystemWeb;
using IdentityModel.Extensions;
using System.Web.Http;
[assembly: OwinStartup(typeof(WebApplication10.Startup))]
namespace WebApplication10
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44357",
// For access to the introspection endpoint
ClientId = "secret_client_id",
ClientSecret = "secret".ToSha256(),
RequiredScopes = new[] { "apiscope" }
});
}
}
}
namespace WebApplication10.Controllers
{
public class ValuesController : ApiController
{
[Authorize]
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
}
The status i get all the time is 401 Unauthorized.
Am i doing something wrong?
Any help with this?
Thanks.
Without logs can't be sure what is issue in your case, but here is couple of fixes I made to make it work:
On Statup.cs class of IdentityServer project
Change AccessTokenJwtType to JWT, default value on IdentityServer4 is at+jwt but .Net Framework Api (OWIN/Katana) requires JWT.
Add /resources aud by setting EmitLegacyResourceAudienceClaim to true, this is removed on IdentityServer4.
You can verify the access_token on https://jwt.ms/ by checking "typ" and "aud" .
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
options.EmitLegacyResourceAudienceClaim = true;
});
On Statup.cs class of .Net Framework Api project, set ValidationMode to ValidationMode.Local, custom access token validation endpoint used by this method is removed on IdentityServer4.
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44357",
// For access to the introspection endpoint
ClientId = "secret_client_id",
ClientSecret = "secret".ToSha256(),
RequiredScopes = new[] { "apiscope" },
ValidationMode = ValidationMode.Local,
});
I have sample working implementation here
I strongly suggest you to gather logs on API, this helps to find the actual issue in your case and finding the fix. here is a sample to turn on OWIN log on Api.
You can follow the sample from CrossVersionIntegrationTests .
Identity server 4 doesn't have the connect/accesstokenvalidation endpoint . So in identity server4 app , you can modify your ApiResource to add the ApiSecret :
new ApiResource("api1", "My API"){ ApiSecrets = new List<Secret> {new Secret("scopeSecret".Sha256())}}
And in your web api , config the IdentityServerBearerTokenAuthenticationOptions like :
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
ValidationMode = ValidationMode.ValidationEndpoint,
ClientId = "api1",
ClientSecret = "scopeSecret",
RequiredScopes = new[] { "api1" }
});
ClientId & ClientSecret are both from your ApiResource .

Asp.Net Core 2 oidc middleware does not challenge after proxy request returns 401

I'm trying to build a centralised proxy that will intercept all requests and handle authentication with openidconnect.
Currently the proxied request simply returns 401, so the middleware suppose to challenge and redirect me to the login page. The issue is using .Net Core 1.1's implemtation it work, but it doesn't seem to work in .Net Core 2.
I've simplified the code but this works, I get redirected to google's signin page.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication();
services.AddProxy();
}
public void Configure(IApplicationBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
});
app.UseGoogleAuthentication(new GoogleOptions
{
AutomaticChallenge = true,
SignInScheme = "oidc",
ClientId = "clientId",
ClientSecret = "clientSecret",
});
app.MapWhen(
context => context.RequestStartsWith("http://www.web1.com"),
builder => builder.RunProxy(baseUri: new Uri("http://www.proxy1.com"))
);
}
}
And this doesn't work with .Net Core 2.0's implementation, I'm getting a 401 exception page
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(options =>
{
options.ClientId = "clientId";
options.ClientSecret = "clientSecret";
});
services.AddProxy();
}
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.MapWhen(
context => context.RequestStartsWith("http://www.web1.com"),
builder => builder.RunProxy(baseUri: new Uri("http://www.proxy1.com"))
);
}
}
Any ideas?
After looking through the source code, it turns out that Authentication middleware in Asp.Net Core 2 does not challenge after response returns 401 status code, so simply return HttpUnauthorizedResult does not work anymore. The reason Authorize attribute works is it returns a ChallengeResult, which it will eventually will call ChallengeAsync.
The work around is, I've created my own middleware which handles 401 Status Code
public class ChallengeMiddleware
{
private readonly RequestDelegate _next;
private readonly IAuthenticationSchemeProvider _schemes;
public ChallengeMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next;
_schemes = schemes;
}
public async Task Invoke(HttpContext context)
{
context.Response.OnStarting(async () =>
{
if (context.Response.StatusCode == 401)
{
var defaultChallenge = await _schemes.GetDefaultChallengeSchemeAsync();
if (defaultChallenge != null)
{
await context.ChallengeAsync(defaultChallenge.Name);
}
}
await Task.CompletedTask;
});
await _next(context);
}
}

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.