OWIN how to configure /signout-oidc to remove cookies - c#

I'm creating applications that are authorized by a personal run OIDC server.
The server is using Openiddict library, and the applications are using OWIN for the configuration. this is because the OIDC server is running on .Net core and the applications on .Net framework.
When trying to log out from these applications I redirect to the OIDC server /Account/Logout, this will in turn get all the logged in applications and open an iframe with the front channel logout url (/signout-oidc).
When logging out, it will give a 404 not found, meaning that the url "example.com/signout-oidc" has not been created.
The used libraries for the application are:
Microsoft.Owin.Security
Microsoft.Owin.Security.Cookies
Microsoft.Owin.Security.OpenIdConnect
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.Use(async (Context, next) =>
{
Debug.WriteLine("1 ==>request, before cookie auth");
await next.Invoke();
Debug.WriteLine("6 <==response, after cookie auth");
});
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.Use(async (Context, next) =>
{
Debug.WriteLine("2 ==>after cookie, before OIDC");
await next.Invoke();
Debug.WriteLine("5 <==after OIDC");
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions()
{
ClientId = "application-client-id",
ClientSecret = "application-client-secret",
Scope = "openid profile email",
Authority = "personal-oidc-link",
AuthenticationMode = AuthenticationMode.Active,
ResponseType = OpenIdConnectResponseType.IdToken,
RedirectUri = "https://example.com/signin-oidc",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = ClaimTypes.NameIdentifier
},
});
app.Use(async (Context, next) =>
{
Debug.WriteLine("3 ==>after OIDC, before leaving the pipeline");
await next.Invoke();
Debug.WriteLine("4 <==after entering the pipeline, before OIDC");
});
The server configuration:
services.AddOpenIddict()
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and entities.
options.UseEntityFrameworkCore()
.UseDbContext<DataContext>()
.ReplaceDefaultEntities<CompanyApplication, CompanyAuthorization, CompanyScope, CompanyToken, Guid>();
})
.AddServer(options =>
{
options.UseMvc();
options.UseJsonWebTokens();
options.AddEphemeralSigningKey("RS512");
// options.AddDevelopmentSigningCertificate();
if (this.environment.IsDevelopment())
{
options.DisableHttpsRequirement();
}
// Enable the authorization, logout, token and userinfo endpoints.
options
.EnableAuthorizationEndpoint("/oidc/authorize")
.EnableLogoutEndpoint("/Account/Logout")
.EnableTokenEndpoint("/oidc/token")
.EnableUserinfoEndpoint("/oidc/userinfo");
options
.AllowAuthorizationCodeFlow()
.AllowImplicitFlow()
.AllowRefreshTokenFlow();
options.RegisterClaims(
CompanyClaims.FriendlyName,
CompanyClaims.Email,
CompanyClaims.EmailVerified,
CompanyClaims.Sub,
CompanyClaims.Group,
CompanyClaims.GivenName,
CompanyClaims.MiddleName,
CompanyClaims.FamilyName);
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(
OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(
CookieAuthenticationDefaults.AuthenticationScheme,
o =>
{
o.AccessDeniedPath = "/Home/Denied";
o.LoginPath = "/Account/Login";
o.LogoutPath = "/Account/Logout";
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.Name = "session";
o.Cookie.Expiration = TimeSpan.FromHours(24);
o.ExpireTimeSpan = TimeSpan.FromHours(24);
});
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseResponseCaching();
app.UseMvcWithDefaultRoute();
}
I expect that OWIN generates the /signout-oidc route, and when called it deletes the authentication cookie.
Edit: Added some more configuration files.

You need to use
PostLogoutRedirectUri = ""

I've no experience with the openiddict library here. But I'm using the IdentityServer library.
I've learned that in the OpenId Connect flow to remove the cookies using the FrontChannel logout you need to:
o.Cookie.SameSite = SameSiteMode.None;
By doing this, the GET request to /signout-oidc, initiated by your OpenId server will contain the authentication cookie of the user currently logging out. So, the middleware handling the /signout-oidc request knows which user is doing the logout and is able to logout that user.
You can use the Developer Tools in your browser (don't forget to enable 'Preserve log' when using Chrome) to find the GET /signout-oidc request and see if it contains the authentication cookie.
A second option is to choose using a backchannel logout flow, don't know if the openiddict supports this.

Related

Implement multiple authorization in .net 6 web API

I have auth0 authentication implemented for my webAPIs. But now due to some requirement change few of APIs need to be authorized with another scheme. So I need below specified different authorization schemes to authorize my API
Auth0 scheme (already authorizing api's)
Azure AD B2C
I have implemented Azure AD B2C and which is working fine when used alone but when I am trying to add it enable it with a previous scheme it is causing issues.
public static IServiceCollection AddSecurityPolicy(this IServiceCollection services, ConfigurationManager config)
{
const string ClientPortalScheme = "ClientPortalBearerScheme";
//from https://auth0.com/blog/securing-aspnet-minimal-webapis-with-auth0/
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = config["AuthenticationSettings:Domain"];
options.Audience = config["AuthenticationSettings:Audience"];
}).AddJwtBearer(ClientPortalScheme, ClientPortalScheme, options =>
{
options.Authority = config["AzureADB2CSettings:Domain"];
options.Audience = config["AzureADB2CSettings:Tenant"];
});
//services.AddMicrosoftIdentityWebApiAuthentication(config, "AzureADB2CSettings");
//By default, require an authenticated user
//Only one JWT bearer authentication is registered with the default authentication scheme JwtBearerDefaults.AuthenticationScheme.
//Additional authentication has to be registered with a unique authentication scheme.
//see https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
ClientPortalScheme);
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
return services;
}
This is how my code looks like.
Issue is when I am calling my endpoint it says
Unable to obtain configuration from: 'https://mydomain.auth0.com/.well-known/openid-configuration'.
---> System.IO.IOException: IDX20804: Unable to retrieve document from: 'https://mydomain.auth0.com/.well-known/openid-configuration'.
Please let me know if any information is required int this regard
I tried to reproduce the issue in my environment.
It occurs when app config is not receiving OpenIDmeta data properly
The issue was due to TLS configuration in my case as TLS 1.1 or TLS 1.0 are depreciated.
Please make sure to set TLS to 1.2 or greater.
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
IdentityModelEventSource.ShowPII = true;
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
......
System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3;;
....
app.UseAuthentication();
app.UseAuthorization();
....
}
Please make sure your backend API refers to /.well-known/openid-configuration
Or check this way.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
MetadataAddress = "http[s]://{IdentityServer}/.well-known/openid-configuration"
...
});
In AzureAdB2c make sure to set the Authority or (Domain and instance ) property correctly .
Authority being the combination of Instance and domain
Appsettings.json
Authority : https://[yourb2ctenant}.b2clogin.com/{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:Policy"]}/v2.0
Also see if Instance can be https://<tenant>.b2clogin.com/tfp/
Domain : <b2ctenant>.onmicrosoft.com
Then the authentication can be carried on successfully:

Asp.net Blazor server app fails to redirect in kubernetes with OIDC

We have a .NET 5 (Blazor Server) app running in Azure Kubernetes that uses OpenID Connect to authenticate with a 3rd party. The app is running behind Ingress. Ingress uses https. The app is only http. After we authenticate with OIDC and get redirected back to /signin-oidc, we get a .NET error that we haven't been able to solve.
warn: Microsoft.AspNetCore.Http.ResponseCookies[1]
The cookie '.AspNetCore.OpenIdConnect.Nonce.CfDJ8EYehvsxFBVNtGDsitGDhE8K9FHQZVQwqqr1YO-zVntEtRgpfb_0cHpxfZp77AdGnS35iGRKYV54DTgx2O6ZO_3gq98pbP_XcbHnJmBDtZg2g5hhPakTrRirxDb-Qab0diaLMFKdmDrNTqGkVmqiGWpQkSxcnmxzVGGE0Cg_l930hk6TYgU0qmkzSO9WS16UBOYiub32GF4I9_qPwIiYlCq5dMTtUJaMxGlo8AdAqknxTzYz4UsrrPBi_RiWUKaF6heQitbOD4V-auHmdXQm4LE' has set 'SameSite=None' and must also set 'Secure'.
warn: Microsoft.AspNetCore.Http.ResponseCookies[1]
The cookie '.AspNetCore.Correlation.MMrYZ2WKyYiV4hMC6bhQbGZozpubcF2tYsKq748YH44' has set 'SameSite=None' and must also set 'Secure'.
warn: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[15]
'.AspNetCore.Correlation.MMrYZ2WKyYiV4hMC6bhQbGZozpubcF2tYsKq748YH44' cookie not found.
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]
An unhandled exception has occurred while executing the request.
System.Exception: An error was encountered while handling the remote login.
---> System.Exception: Correlation failed.
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
public class Startup
{
private static readonly object refreshLock = new object();
private IConfiguration Configuration;
private readonly IWebHostEnvironment Env;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Console.WriteLine($"LogQAApp Version: {Assembly.GetExecutingAssembly().GetName().Version}");
// We apparently need to set a CultureInfo or some of the Razor pages dealing with DateTimes, like LogErrorCountByTime fails with JavaScript errors.
// I wanted to set it to CultureInvariant, but that wouldn't take. Didn't complain, but wouldn't actually set it.
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");
Configuration = configuration;
Env = env;
}
// 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.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); // Needed for 1252 code page encoding.
Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("");
services.AddSignalR(e =>
{
e.MaximumReceiveMessageSize = 102400000;
});
services.AddBlazoredSessionStorage();
services.AddCors();
services.AddSyncfusionBlazor();
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
ServiceConfigurations.LoadFromConfiguration(Configuration);
#region Authentication
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(
options =>
{
options.Events = GetCookieAuthenticationEvents();
}
)
.AddOpenIdConnect("SlbOIDC", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration["SlbOIDC:Authority"];
if (Env.IsDevelopment())
{
options.ClientId = Configuration["SlbOIDC:ClientID"];
options.ClientSecret = Configuration["SlbOIDC:ClientSecret"];
}
else
{
options.ClientId = Configuration.GetValue<string>("slbclientid");
options.ClientSecret = Configuration.GetValue<string>("slbclientsecret");
}
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.SaveTokens = true;
options.ClaimsIssuer = "SlbOIDC";
// Azure is communicating to us over http, but we need to tell SLB to respond back to us on https.
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
Console.WriteLine($"Before: {context.ProtocolMessage.RedirectUri}");
context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http://", "https://");
Console.WriteLine($"After: {context.ProtocolMessage.RedirectUri}");
return Task.FromResult(0);
}
};
});
services.AddSession(options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
});
#endregion
services.AddScoped<BrowserService>();
services.AddSingleton<ConcurrentSessionStatesSingleton>();
services.AddSingleton<URLConfiguration>();
services.AddScoped<CircuitHandler>((sp) => new CircuitHandlerScoped(sp.GetRequiredService<ConcurrentSessionStatesSingleton>(), sp.GetRequiredService<BrowserService>(), sp.GetRequiredService<IJSRuntime>()));
services.AddScoped<SessionServiceScoped>();
services.AddScoped<LogEditorScoped>();
services.AddSingleton<ModalService>();
services.AddFlexor();
services.AddScoped<ResizeListener>();
services.AddScoped<ApplicationLogSingleton>();
services.AddScoped<LogChartsSingleton>();
services.AddScoped<CurveNameClassificationSingleton>();
services.AddScoped<HubClientSingleton>();
services.AddScoped((sp) => new LogAquisitionScopedService(
sp.GetRequiredService<URLConfiguration>(),
sp.GetRequiredService<HubClientSingleton>(),
sp.GetRequiredService<ApplicationLogSingleton>(),
sp.GetRequiredService<IConfiguration>(),
sp.GetRequiredService<SessionServiceScoped>(),
sp.GetRequiredService<AuthenticationStateProvider>(),
sp.GetRequiredService<IHttpContextAccessor>(),
sp.GetRequiredService<IJSRuntime>()
)
);
services.AddScoped<UnitSingleton>();
services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = true; });
services.AddScoped<TimeZoneService>();
services.AddHostedService<ExcelBackgroundService>();
services.AddHostedService<LogEditorBackgroundService>();
}
// 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();
}
else
{
app.UseExceptionHandler("/Error");
//app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseAuthentication();
if (!Env.IsDevelopment())
{
app.UseTrafficManager();
}
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
private CookieAuthenticationEvents GetCookieAuthenticationEvents()
{
return new CookieAuthenticationEvents()
{
OnValidatePrincipal = context =>
{
lock (refreshLock)
{
if (context.Properties.Items.ContainsKey(".Token.expires_at"))
{
DateTime expire = DateTime.Parse(context.Properties.Items[".Token.expires_at"]);
if (expire.AddMinutes(-20) < DateTime.Now)
{
try
{
CloudAuthentication cloudAuthentication = new CloudAuthentication();
TokenResponse tokenResponse = cloudAuthentication.GetRefreshToken(context.Properties.Items[".Token.refresh_token"]);
context.Properties.Items[".Token.access_token"] = tokenResponse.access_token;
context.Properties.Items[".Token.refresh_token"] = tokenResponse.refresh_token;
context.Properties.Items[".Token.expires_at"] = DateTime.Now.AddSeconds(tokenResponse.expires_in).ToString();
context.ShouldRenew = true;
}
catch (Exception ex)
{
context.RejectPrincipal();
}
}
}
return Task.FromResult(0);
}
}
};
}
}
It's a good question - there are a couple of interesting points here that I've expanded on since they are related to SameSite cookies.
REVERSE PROXY SETUP
By default the Microsoft stack requires you to run on HTTPS if using cookies that require an SSL connection. However, you are providing SSL via a Kubernetes ingress, which is a form of reverse proxy.
The Microsoft .Net Core Reverse Proxy Docs may provide a solution. The doc suggests that you can inform the runtime that there is an SSL context, even though you are listening on HTTP:
app.Use((context, next) =>
{
context.Request.Scheme = "https";
return next();
});
I would be surprised if Microsoft did not support your setup, since it is a pretty mainstream hosting option. If this does not work then you can try:
Further searching around Blazor and 'reverse proxy hosting'
Worst case you may have to use SSL inside the cluster for this particular component, as Johan indicates
WIDER INFO - API DRIVEN OAUTH
Many companies want to develop Single Page Apps, but use a website based back end in order to manage the OAuth security. Combining serving of web content with OAuth security adds complexity. It is often not understood that the OAuth SPA security works better if developed in an API driven manner.
The below resources show how the SPA code can be simplified and in this example the API will issue cookies however it is configured. This would enable it to listen over HTTP inside the cluster (if needed) but to also issue secure cookies:
API driven OpenID Connect code
Curity Blog Post
WIDER INFO: SAMESITE COOKIES
It is recommended to use SameSite=strict as the most secure option, rather than SameSite=none. There are sometimes usability problems with the strict option however, which can cause cookies to be dropped after redirects or navigation from email links.
This can result in companies downgrading their web security to a less secure SameSite option. These problems do not occur when an API driven solution is used, and you can then use the strongest SameSite=strict option.

Blazor WebAssembly 401 Unauthorized even when I am authorized

I am using Blazor WebAssembly Asp.Net Core hosted PWAand integrated the AspNetCore.Identity into it. I created the AuthenticationStateProvider in the Client-Side and now I want to allow the user access to a controller where he needs to be authorized.
I have tested via postman, the users were been created and stored in DB as aspnetusers with the right credentials. The Login/Account Controller work as I wanted it.
When the user is authorized it tells this exception in the browser when accessing the authorized controller request:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Response status code does not indicate success: 401 (Unauthorized).
System.Net.Http.HttpRequestException: Response status code does not
indicate success: 401 (Unauthorized).
Startup.cs (ConfigureServices-Method):
...
serviceCollection.AddDbContext<SQLiteTestDbContext>(options =>
{
options.UseSqlite(config["ConnectionStrings:SQLiteTestConnection"]);
});
serviceCollection.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<SQLiteTestDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtAudience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
};
});
services.AddHttpContextAccessor();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseAuthentication();
app.UseAuthorization();
...
}
Program.cs Client-Side
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Logging.SetMinimumLevel(LogLevel.Warning);
//Registering Shared-Library models
builder.Services.AddScoped<ObjectModel>();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, ApiAuthenticationStateProvider>();
builder.Services.AddScoped<IAuthService, AuthService>();
//Registered BlazorContextMenu Service
builder.Services.AddBlazorContextMenu();
//Registering FileReader service, for image upload -> Azure
builder.Services.AddFileReaderService(options => options.UseWasmSharedBuffer = true);
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
My Controller with authorize attribute:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")]
[ApiController]
public class ObjectController : ControllerBase
{
....
Note:
When your user tries to access a protected (annotated with the Authorize attribute) page on the client he should login first or register.
In order to register, he should be redirected to an Account Controller where you should create a new user, and add it to the database (You said you " integrated the AspNetCore.Identity into it"), which is fine...and should be used to authenticate and verify the user's identity. You account controller should also produce a Jwt Token that should be passed to the client app, and stored in the local storage.
Now, whenever your user tries to access protected resources on your Web Api endpoints, you should retrieve the Jwt Token from the local storage, and add it to the request header. If you do so, the Unauthorized response would be something of the past.
Custom AuthenticationStateProvider can be a good place from which you can manage storing the Jwt Token in the local storage and retrieving it for outbound HTTP request calls.
Here's some sample code to clarify what you should do:
#code {
WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
var token = await TokenProvider.GetTokenAsync();
forecasts = await Http.GetJsonAsync<WeatherForecast[]>(
"api/WeatherForecast",
new AuthenticationHeaderValue("Bearer", token));
}
}
Note: TokenProvider is a custom AuthenticationStateProvider that defines a method called GetTokenAsync that provides (reading the Jwt Token from the local storage and passing it to the calling code) the Jwt Token
Hope this helps...
In case of Linux App Service in combination with ID Server the Authority needs to be set according to Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#azure-app-service-on-linux-1
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = "{AUTHORITY}";
});
Example: options.Authority = "https://contoso-service.azurewebsites.net";

.NET Core Authorization - Constant 403 on JWT Bearer

I'm attempting to authorize requests to my API which bear a JWT token attached to it, however, none of the tutorials, blog posts, and documentation have helped avoiding a constant 403 - Unauthorized error.
This is the -skimmed- current configuration:
Class which generates the token: TokenManagement.cs:
// Add the claims to the token
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, credentials.Username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim("claimName", "claimValue")
};
Configuring the services: Startup.cs - ConfigureServices():
services.Configure<GzipCompressionProviderOptions>(options => options.Level = System.IO.Compression.CompressionLevel.Optimal);
services.AddResponseCompression();
services.AddAuthentication()
.AddJwtBearer(config => {
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = "Issuer",
ValidAudience = "Audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(symmetricKey))
};
});
services.AddAuthorization(options => {
options.AddPolicy("myCustomPolicy", policy => {
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("claimName", "claimValue");
});
});
services.AddMvc();
General Configuration: Startup.cs - Configure():
app.UseAuthentication();
app.Use(async (context, next) => {
await next();
if (context.Response.StatusCode == 404 &&
!Path.HasExtension(context.Request.Path.Value)) {
context.Request.Path = "/index.html";
await next();
}
});
app.UseMvc();
app.UseResponseCompression();
app.UseDefaultFiles();
app.UseStaticFiles();
Controller which should be authorized: ActionsController.cs:
[Authorize(Policy = "myCustomPolicy")]
[Route("api/[controller]")]
public class ActionsController : Controller
Any request I send to the server (which carries a JWT token with the proper claim), returns as a 403.
Any methods which have the [AllowAnonymous] attribute, work just fine.
Is there a way to -at least- debug and see what's going on?
I found out that some claim types changed to different values from my identity server config.
for example , In my Identity Server i am using role claim type:
UserClaims = new []
{
JwtClaimTypes.Role , user.role // "JwtClaimTypes.Role" yield "role"
};
But when i debuged my web api , the role claim type has changed to (see my snapshot below, under watch section):
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
Solution:
To "workaround" (is this desired behavior?) the issue, you need to check your claim type
value are planning use in web api, and use the correct claim type value in your policy.
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy =>
{
//policy.RequireClaim(IdentityModel.JwtClaimTypes.Role, "Admin"); // this doesn't work
policy.RequireClaim(ClaimTypes.Role, "Admin"); // this work
});
});
my web api debug snapshot:
Try to enable CORS in Startup.cs File
public void ConfigureAuth(IAppBuilder app) {
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
// Rest of code
}

Accessing dotnetcore middleware AFTER a JWT Token is validated

I am using JWT bearer authentication, configured as follows.
My problem is that the middleware is executing before the token is validated.
How do I configure the middleware to run afterwards?
services.AddAuthentication()
.AddCookie(_ => _.SlidingExpiration = true)
.AddJwtBearer(
_ =>
{
_.Events = new JwtBearerEvents
{
// THIS CODE EXECUTES AFTER THE MIDDLEWARE????
OnTokenValidated = context =>
{
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(context.Principal.Claims, "local"));
return Task.CompletedTask;
}
};
_.RequireHttpsMetadata = false;
_.SaveToken = false;
_.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = this.Configuration["Tokens:Issuer"],
ValidAudience = this.Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["Tokens:Key"])),
};
});
I am attempting to add middleware into the pipeline that accesses the current user. This code unfortunately executes BEFORE the token is validated. How do I make it execute afterwards?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentityServer();
app.UseAuthentication();
app.Use(async (httpContext, next) =>
{
// THIS CODE EXECUTES BEFORE THE TOKEN IS VALIDATED IN OnTokenValidated.
var userName = httpContext.User.Identity.IsAuthenticated
? httpContext.User.GetClaim("email")
: "(unknown)";
LogContext.PushProperty("ActiveUser", !string.IsNullOrWhiteSpace(userName) ? userName : "(unknown)");
await next.Invoke();
});
It looks like you've found a good solution to your problem but I thought I'd add an answer to explain the behavior you're seeing.
Since you have multiple authentication schemes registered and none is the default, authentication does not happen automatically as the request goes through the pipeline. That's why the HttpContext.User was empty/unauthenticated when it went through your custom middleware. In this "passive" mode, the authentication scheme won't be invoked until it is requested. In your example, this happens when the request passes through your AuthorizeFilter. This triggers the JWT authentication handler, which validates the token, authenticates and sets the Identity, etc. That's why (as in your other question) the User is populated correctly by the time it gets to your controller action.
It probably doesn't make sense for your scenario (since you're using both cookies and jwt)... however, if you did want the Jwt authentication to happen automatically, setting HttpContext.User for other middleware in the pipeline, you just need to register it as the default scheme when configuring authentication:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
based on #leppie's comment, here is a solution that works.
public class ActiveUserFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var userName = context.HttpContext.User.Identity.IsAuthenticated
? context.HttpContext.User.GetClaim("email")
: "(unknown)";
using (LogContext.PushProperty("ActiveUser", !string.IsNullOrWhiteSpace(userName) ? userName : "(unknown)"))
await next();
}
}
Inserted as follows...
services.AddMvc(
_ =>
{
_.Filters.Add(
new AuthorizeFilter(
new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
IdentityConstants.ApplicationScheme)
.RequireAuthenticatedUser()
.Build()));
_.Filters.Add(new ActiveUserFilter());
...

Categories

Resources