I have an ASP.Net Core 2 API using IdentityServer4. I would like challenge ALL requests to the server and invoke the login redirect if the user is not authenticated, calling back to a specific URL after authentication.
The default is to invoke the login redirect only when an unauthenticated user requests a resource protected by the [Authorize] attribute. This will not work in my use case.
Basically, I want the functional equivalent of an [Authorize] attribute for the whole application not just specific controllers.
What is the easiest way to do this? Is there a setting I can use when configuring the services in Startup.cs (services.AddAuthentication)? Or through custom middleware right after app.UseAuthentication()?
I tried the following custom middleware but it says a handler is not configured.
ConfigureServices
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:4000";
options.ApiName = "myapi";
});
Configure
app.UseAuthentication();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(IdentityServerAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties {RedirectUri= "https://localhost:5000/" });
}
else { await next.Invoke(); }
});
app.UseMvc();
For configuring [Authorize] for the whole controllers, you could try AuthorizeFilter like below
services.AddMvc(config => {
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
For redirecting, you could try configure UserInteraction.LoginUrl
services.AddIdentityServer(opt => {
opt.UserInteraction.LoginUrl = "/Identity/Account/LogIn";
})
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<IdentityUser>();
Related
I'm trying to connect to a SignalR service from my blazor webassembly client but this fails I think on CORS. This is the code in my razor file.
m_connection = new HubConnectionBuilder()
.WithUrl(myMircoServiceUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(userService.Token);
})
.WithAutomaticReconnect()
.Build();
await m_connection.StartAsync();
Then in the webassembly logging I see the following error:
Access to fetch at 'xxxx/negotiate?negotiateVersion=1' from origin 'http://localhost:5010' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I added the following CORS policy in my Blazor server configuration and something similar in the microservice config:
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBlazorDebugging();
}
else
{
app.UseExceptionHandler(#"/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors(policy => policy
.WithOrigins("http://localhost:5010")
.AllowAnyHeader()
.AllowAnyMethod());
app.UseClientSideBlazorFiles<Client.Program>();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<Client.Program>(#"index.html");
});
Anybody got any idea what might be wrong?
Update 1
I now see the following error in the Chrome console:
dotnet.js:1 WebSocket connection to 'ws://localhost:5000/hubs/posts?id=9Jxs0DhP924zgw_eIeE9Lg' failed: HTTP Authentication failed; no valid credentials available
Update 2
I removed the [Authorize] attribute from the SignalR hub and now it connects. And I can send messages to the hub. Problem is there is a reason for this attribute, because I don't want that people can subscribe to messages that are not for them
Update 3
Still no progress. Looking at pulling out the authentication to a seperate microservice using IdentityServer4. Last status is I have the following startup routines:
Microservice: gist.github.com/njannink/15595b77ffe1c0593be1a555fa37f83f
Blazor server: gist.github.com/njannink/7302a888110e24d199ea45b66da4f26b
Blazor client: gist.github.com/njannink/add2568cbf48c8b3c070ccd4f28fd127
I've got the same errors with CORS and afterwards Websocket.
In my case the fallback longPolling was used as why the connection worked but the console logged the error HTTP Authentication failed; no valid credentials available.
If you use Identity Server JWT the following code solved the error for my case.
(The Code is from the Microsoft SignalR Documentation - Authentication and authorization in ASP.NET Core SignalR - Identity Server JWT authentication)
services.AddAuthentication()
.AddIdentityServerJwt();
// insert:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
ConfigureJwtBearerOptions>());
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
}
};
}
}
Important: Your Route has to start with hubs for the Options to trigger!
(see Line path.StartsWithSegments("/hubs")))
app.UseEndpoints(e =>
{
...
e.MapHub<ChatHub>("hubs/chat");
});
In my case, ASP.NET Core 2.2 I have an API from which I want to be able to use SignalR from the API to connect to my client application.
I have Projects for
Web API
IdentityServer4
MVC Client
With ASP.NET Core Identity as the for user management
In order for your user to be authenticated you need to implement a IUserIdProvider like this
public class IdBasedUserIdProvider : IUserIdProvider
{
public string GetUserId(HubConnectionContext connection)
{
//TODO: Implement USERID Mapper Here
//throw new NotImplementedException();
//return whatever you want to map/identify the user by here. Either ID/Email
return connection.User.FindFirst("sub").Value;
}
}
With this I make sure I am pushing along the ID/Email to a method I am calling either from the Server or Client. Although I can always use the .User on the HubContext and it works fine.
In my Web API Startup.cs file I came up with
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy =>
{
policy.WithOrigins(Configuration.GetSection("AuthServer:DomainBaseUrl").Get<string[]>())
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed((_) => true)
.SetIsOriginAllowedToAllowWildcardSubdomains();
});
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, UserManager<AppUser> userManager,
RoleManager<IdentityRole> roleManager){
app.UseCors();
}
NOTE
Configuration.GetSection("AuthServer:DomainBaseUrl").Get() retrieves the list of domains to allow CORS for from a config file.
And I did this configuration in My Client App COnfigureService Method
services.AddCors(cfg =>
{
cfg.AddDefaultPolicy(policy => {
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.SetIsOriginAllowed((host) => true);
policy.AllowAnyOrigin();
});
});
I hope this helps your situation.
The best solution is indeed as Ismail Umer described using a seperate authentication service using something like IdentityServer4. And use this service in all other services. This is something I will do in a next iteration.
As short term solution I temporary moved the blazor server part into my api service and use a dual authentication method (JWT header or cookie).
var key = Encoding.UTF8.GetBytes(m_configuration[#"SecurityKey"]);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = #"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true
};
})
.AddCookie();
// TODO: For time being support dual authorization. At later stage split in various micro-services and use IdentityServer4 for Auth
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
This is problem with Microsoft.AspNetCore.SignalR.Client 3.1.3.
You can read about it here in comments.
You can wait for update or temporarly fix this issue:
Disable negotiation
Set WebSocket transport explicitly
Modify query url
Add OnMessageReceived handler
Client side:
var token = await GetAccessToken();
var hubConnection = new HubConnectionBuilder()
.WithUrl($"/notification?access_token={token}", options =>
{
options.SkipNegotiation = true;
options.Transports = HttpTransportType.WebSockets;
options.AccessTokenProvider = GetAccessToken;
})
.Build();
Server side:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
// ...
})
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/notification", System.StringComparison.InvariantCulture)))
{
context.Token = accessToken;
}
return Task.CompletedTask;
},
};
});
}
I have a React Front end using the msal lib to authenticate the user client side with our Azure AD. This works great and authentication has no issues. I also have an ASP.Net Core WebApi to provide data to the client. I am using the JwtTokens to pass the Bearer token in the request. The WebApi is able to validate the token and all is well... I thought, however, when the WebApi method is invoked the only way I can get the User's email or name is to query the User.Claims with Linq.
this.User.Claims.Where(c=> c.Type == "preferred_username").FirstOrDefault().Value
I was about to go down the road of mapping these linq queries to an object which could be injected into the WebApi's controller, but that seems wrong.
I am obviously missing something in my Startup.cs for the WebApi, Any help or suggestions would be great!:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//add authentication JwtBearer Scheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = Configuration["JwtSettings:Audience"];
options.Authority = Configuration["JwtSettings:Authority"];
options.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
//log
return Task.CompletedTask;
},
OnAuthenticationFailed = ctx =>
{
//log
return Task.CompletedTask;
}
};
options.SaveToken = true;
});
services.AddAuthorization();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
I have a very simple web api application that is configured like that:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
Services has MVC registration like that:
_services
.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
There is no default route registered for root, so when I do a request to root of the application, I get 404. What I was expecting to get is 401 though as I thought that absolutely all requests should require authorization.
I have not figured a way to achieve that, and not even sure this is correct semantically, but there is no open endpoints in my API and I would like to just return 401 if there is no authentication for any route.
ASP.NET uses 401 internally, you will always get 404, you can handle it by using
AddAuthentification() Extension in your Service collector
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
}).AddCookie("Cookies",
(options) =>
{
options.AccessDeniedPath = "..your access denied page route..";
options.LoginPath ="..your login route.."
})
I am using AzureAD in asp.net core 2 app. I want to use cookie and bearer authentication both. I have following code in startup file:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
//options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddAzureADBearer(options => Configuration.Bind("AzureAdClient", options));
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), sqlServerOptions => sqlServerOptions.CommandTimeout(120)));
//services.AddMvc();
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
//options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
I have added authorized attribute as:
[Authorize(AuthenticationSchemes = "AzureADBearer")]
Now when hitting from postman, i can get the bearer token, but when i am using that token to access this API, i am getting signature invalid error:
WWW-Authenticate →Bearer error="invalid_token", error_description="The signature is invalid"
Any Ideas?
Try something like below , It should work.
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultChallengeScheme = AzureADDefaults.AuthenticationScheme;
sharedOptions.DefaultAuthenticateScheme = AzureADDefaults.AuthenticationScheme;
})
.AddAzureAD(options => Configuration.Bind("AzureAd", options))
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
and in controller application, you can set the schema like this:
[HttpGet]
[Authorize(AuthenticationSchemes = "AzureADBearer")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Hope it helps.
Well, I solved this. Now in asp.net core web + API project, I am using only the API specific AzureAD setting. For Postman and mobile application, I have created a new app registration and added scope for earlier app registration (API app registration), which have user impersonation and access as user permissions.
I have an Asp.NET Core application that uses IdentityServer for authentication. This works fine.
Now I want to use ASP.NET Core Identity in my application for managing roles, claims, etc.
The documentation saysI should add service.AddIdentity ... for that.
However, when I add that in my client to the Startup.cs, the login with IdentityServer no longer works.
I will be redirected to IdentityServer, login and redirected back to the client (this works fine)
However, then my client throws an error about authorization and redirects to the IdentityServer again. This causes an endless loop
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:44331/signin-oidc application/x-www-form-urlencoded 5297
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: AuthenticationScheme: Cookies signed in.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 184.9938ms 302
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:44331/
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler:Information: Identity.Application was not authenticated. Failure message: Unprotect ticket failed
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Route matched with {action = "Index", controller = "Home", page = "", area = ""}. Executing action TestApplication.Controllers.HomeController.Index (TestApplication)
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes ().
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler:Information: AuthenticationScheme: oidc was challenged.
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Information: Executed action TestApplication.Controllers.HomeController.Index (TestApplication) in 15.4912ms
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 29.286ms 302
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 POST http://localhost:44331/signin-oidc application/x-www-form-urlencoded 5297
-- and it starts all over again
Does anyone have a clue what I'm doing wrong?
Here's my Startup.cs
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment environment)
{
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
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<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<ApiService>();
services.AddSingleton(Configuration);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie()
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = 'Cookies';
options.UseTokenLifetime = true;
options.Authority = 'https://localhost:44350;
options.RequireHttpsMetadata = true;
options.ClientId = Configuration.GetValue<string>("IdentityServer:ClientId");
options.ClientSecret = Configuration.GetValue<string>("IdentityServer:ClientSecret");
options.ResponseType = "code id_token token"
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
.AddJsonOptions(x => x.SerializerSettings.NullValueHandling = NullValueHandling.Ignore);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller}/{action}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
On Identity Server side , you can create Profile Service to make IDS4 include role claim when issuing tokens . Then on client side you can map the role claim from your JWT Token to your claim principle . You can refer to code sample from here .
To manage users or roles , you can provide API endpoint in your identity server4 application , or create a new application as another resource to manage your database , you client application will acquire access token for accessing you new application form Identity Server 4 , send request by appending token to HTTP Authorization header for management API calls .
If anyone else comes across this and still wants to authenticate via IdSrv but manage users outside of IdSrv, replace this:
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
in your client app with this
services.AddIdentityCore<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
//.AddDefaultUI() // use standard identity UI forms for register, forgot pass etc.
.AddDefaultTokenProviders();
Uncomment the line above to use the standard UI forms from MS identity but beware that you'll have to override the default sign in logic afterwards. This is done by adding the identity scaffolding to your project.