User is not authenticated in Middleware, but authenticated in Controller - .net-core

I am using OpenIddict package to handle authentication in my .net core Api app. Inside my controllers, I can see User, its claims, roles, etc. However in Middleware, I don't have access to the user, as if the user is not authenticated.
I think I am missing something in ConfigureServices or Configure methods in Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(
config =>
{
//We are adding a default policy required for all requests
var policyBuilder =
new AuthorizationPolicyBuilder(OpenIddictValidationDefaults.AuthenticationScheme);
var policy = policyBuilder.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}
);
services.AddDbContext<TGDbContext>((ctx, options) =>
{
options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
options.UseOpenIddict();
}, ServiceLifetime.Scoped, ServiceLifetime.Scoped);
// Register the OpenIddict services.
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<TGDbContext>();
})
.AddServer(options =>
{
options.UseMvc();
options.EnableTokenEndpoint( "/connect/token");
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.AcceptAnonymousClients();
options.AllowCustomFlow("GuestLogin");
options.RegisterScopes("Booking");
if (this._env.IsDevelopment())
options.DisableHttpsRequirement(); // Note: Requires Https in production
options.RegisterScopes(
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Phone,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles);
})
.AddValidation();
// add identity
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<TGDbContext>()
.AddDefaultTokenProviders();
// Configure Identity options and password complexity here
services.Configure<IdentityOptions>(options =>
{
// User settings
options.User.RequireUniqueEmail = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 5;
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddAuthorization(options =>
{
options.AddPolicy(.....);
....
....
....
});
.....
.....
.....
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
....
....
....
app.UseAuthentication();
app.Use(async (context, next) =>
{
var isAuthenticated = context.User.Identity.IsAuthenticated;
// isAuthenticated is false here
// also context.User.Claims is empty
Console.WriteLine(isAuthenticated);
await next.Invoke();
});
....
....
....
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
I am targeting .Net Core 2.2 using OpenIddict 2.0.1 and OpenIddict.EntityFramework 2.0.1.

The behavior you're seeing is expected (and not specific to OpenIddict): by registering an AuthorizeFilter pointing to the OpenIddict validation handler, you're only configuring MVC to validate bearer tokens, but not the rest of your app, including middleware.
To configure OpenIddict as the default authentication scheme, call
services.AddAuthentication(options =>
{
// When targeting OpenIddict 3.0, OpenIddictValidationAspNetCoreDefaults
// must be used instead of OpenIddictValidationDefaults.
options.DefaultAuthenticateScheme = OpenIddictValidationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationDefaults.AuthenticationScheme;
});

Related

Authorization Policy Attribute returns 403 and User.IsInRole returns false but claims are present WEB API Azure AD (Single Tenant)

Setup
I have an ASP.NET Core MVC web app configured with Azure AD. I have an ASP.NET Core Web API configured Azure AD.
I obtain the token from Azure AD in the client web app and use it when requesting resources from the Web API, this works fine.
Issue
The '[Authorize(Policy = 'policyName')]' attribute returns 403 and User.IsInRole() returns false in the WEB API controller, however when I remove the authorize policy attribute and check the user claims object, I can see the role present. Please check the image below
Claims Image
RoleClaimType Image
In my startup file I have configured token validation parameters of roleClaimType to be roles
`services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>{
// Use the groups claim for populating roles
options.TokenValidationParameters.RoleClaimType = "roles";
});`
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.AddOptions();
services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// Use the groups claim for populating roles
options.TokenValidationParameters.RoleClaimType = "roles";
});
services.AddAuthorization(options =>
{
options.AddPolicy(Constants.AssignmentToAccountCreatorsRoleRequired, policy => policy.RequireRole(Constants.CAN_CREATE_ACCOUNT));
});
services.AddDbContext<DbContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));
//services.BuildServiceProvider().GetService<DbContext>().Database.Migrate();
services.AddAutoMapper(typeof(Startup));
services.AddControllers().AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
//TODO: Replace CORS configuration
services.AddCors(c => c.AddDefaultPolicy(policy => {
policy.WithOrigins("https://localhost:44385", "https://someweb.net/")
.AllowAnyHeader()
.AllowAnyMethod();
}));
services.AddControllers();
services.AddScoped<ConfigurationModel>();
//services.AddScoped<GraphProfileClient>();
//services.AddScoped<GraphEmailClient>();
services.AddMemoryCache();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "NAPI", Version = "v1" });
});
}
// 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.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I noticed the options for AddAuthorization() policy in the startup.cs contains a RequireClaim method which takes in the claimType and the allowedValue(s). RequireClaim(String, IEnumerable)
services.AddAuthorization(options =>
{
options.AddPolicy("AssignmentRoleRequired", policy => policy.RequireClaim("roles", "canDoSomething"));
}); //Startup.cs
[Authorize(Policy = "AssignmentToRoleRequired")] //For Controllers
Similarly the User Object also contains a method called HasClaim(), that takes in the claimType and the allowed value.
Within a controller HasClaim(String, String)
var result = User.HasClaim("roles", "canDoSomething"); //returns true

Getting lost configuring IdentityServer4 for SPA

I have been through MS docs and IdentityServer docs. I am still stuck.
I have tried every config option and I've reached breaking point.
I started with the MS SPA template, added AspNet Core Identity, and fiddled with these setting. I'm able to call the Api's using Postman, and get a token, it has authority, and scope, and name set, but the API's show null in User.Identity when I debug.
Currently my error is this:
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
Here is my configureServices:
public void ConfigureServices(IServiceCollection services)
{
// Add ASP.Net Core Identity
// this handles user authentication as well as user registration and management
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = false;
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<AccountingDbContext>();
services.AddRazorPages();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Handle authentication and authorization using Open ID Connect.
// The components for authentication and authorization are:
// - user
// - client (the Angular app)
// - identity provider (ASP.Net Core Identity with IdentityServer4), and
// - resource server (the api)
// identity provider and resource server are hosted together.
services.AddIdentityServer()
.AddApiAuthorization<User, AccountingDbContext>(options =>
{
options.ApiResources
.First() // represents the configured default Api
.UserClaims
.Add(JwtClaimTypes.Name);
})
.AddDeveloperSigningCredential();
// Add Controllers with API
services.AddControllers();
.AddMvcOptions(options =>
{
// Adding an empty Authorise filter means that any Authenticated user is valid.
options.Filters.Add(new AuthorizeFilter());
});
// For Azure App Service deployments on Linux
if (!Environment.IsDevelopment())
{
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
});
}
// Authenticate requests to the Api
// accepts any access token issued by identity server
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
})
.AddIdentityServerJwt();
// https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html
// Adds Authorization policy to make sure the token has scope 'Accounting.ApplicationAPI openid profile'
services.AddAuthorization(options =>
{
// Registers the Policy with Authorization middleware
options.AddPolicy("Accounting.ApplicationAPI", policy =>
{
// Policy specifies that any Authenticated user with Accounting.ApplicationAPI scope is valid.
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "Accounting.ApplicationAPI openid profile");
});
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "./Accounting.Ng/dist";
});
}
and configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("Accounting.ApplicationAPI");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "../Accounting.Ng";
if (env.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer(Configuration["AngularServer"]);
}
});
}

Middleware Ordering

I have a new .NET Core 3.1 app and am struggling with the concept of Middleware. From reading around, it seems the order of including different middlewares is important. I currently have several problems which I can't seem to solve:
I never see the developer error page, and have to check the event log to see what's happened if there's an error. I just get the blank "error 500" etc pages from Chrome. The custom error pages also never display when there's a 500/400.
The app always tries to redirect me to /Account/Login despite changing this in the cookie settings.
User.IsAuthenticated returns false when the CheckPermissionsAction call is made in Elmah, so I can't access Elmah. The User.IsInRole call works for from controllers though.
This is how I'm bootstrapping the app. It feels like something is overriding the settings:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<DataProtectionTokenProviderOptions>(options =>
options.TokenLifespan = TimeSpan.FromDays(2));
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
services.AddControllersWithViews();
services.AddTransient<IUserStore<User>, UserStore>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore>();
services.AddRazorPages();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/login");
options.AccessDeniedPath = new PathString("/error/denied");
options.LogoutPath = new PathString("/log-off");
options.ExpireTimeSpan = TimeSpan.FromDays(60);
options.SlidingExpiration = true;
options.Cookie.HttpOnly = true;
options.Cookie.Name = "MyCookie";
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.Cookie.SameSite = SameSiteMode.Lax;
});
services.AddIdentity<User, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
})
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.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.AddElmah<SqlErrorLog>(options =>
{
options.ConnectionString = Configuration.GetConnectionString("MyApp");
options.CheckPermissionAction = (context)=>{
return context.User.Identity.IsAuthenticated && context.User.IsInRole(RoleHelper.SuperAdmin);
};
options.Path = "/elmah";
});
services.AddSingleton<IAppConfiguration, AppConfiguration>(e => Configuration.GetSection("AppConfig")
.Get<AppConfiguration>());
OptionsConfigurationServiceCollectionExtensions.Configure<DbHelper>(services, Configuration.GetSection("ConnectionStrings"));
services.AddHttpContextAccessor();
}
public void ConfigureContainer(ContainerBuilder builder)
{
// wire up using autofac specific APIs here
builder.Register(context => new MapperConfiguration(cfg =>
{
cfg.CreateMap<User, MyDetailsViewModel>();
})).AsSelf().SingleInstance();
builder.RegisterModule(new RegistrationModule()); // separate assembly, wires up autofac registrations
builder.Register(c =>
{
//This resolves a new context that can be used later.
var context = c.Resolve<IComponentContext>();
var config = context.Resolve<MapperConfiguration>();
return config.CreateMapper(context.Resolve);
})
.As<IMapper>()
.InstancePerLifetimeScope();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// debugger shows this section is called, but I never see the error page.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseRouteDebugger();
}
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();
}
app.UseSession();
app.UseElmah();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
var cookiePolicyOptions = new CookiePolicyOptions
{
Secure = CookieSecurePolicy.SameAsRequest,
MinimumSameSitePolicy = SameSiteMode.None
};
app.UseCookiePolicy(cookiePolicyOptions);
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Guest}/{action=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapControllers();
});
app.UseStatusCodePages(async ctx =>
{
//Re-execute the request so the user gets the error page
string originalPath = ctx.HttpContext.Request.Path.Value;
switch (ctx.HttpContext.Response.StatusCode)
{
case 401:
//Re-execute the request so the user gets the error page
ctx.HttpContext.Items["originalPath"] = originalPath;
ctx.HttpContext.Request.Path = "/error/denied";
break;
case 412:
ctx.HttpContext.Items["originalPath"] = originalPath;
ctx.HttpContext.Request.Path = "/error/expired-account";
break;
case 404:
ctx.HttpContext.Items["originalPath"] = originalPath;
ctx.HttpContext.Request.Path = "/error/not-found";
break;
case 500:
ctx.HttpContext.Items["originalPath"] = originalPath;
ctx.HttpContext.Request.Path = "/error/not-found";
break;
}
});
DapperExtensions.DapperExtensions.SetMappingAssemblies(new[]
{
Assembly.GetAssembly(typeof(MyApp.Domain.Model.Note)),
Assembly.GetExecutingAssembly()
});
}
In regards to the order of your middleware, there is a problem with it.
There is a section in the Microsoft docs dedicated to the order of middleware, I suggest reading it.
As for your middleware, the correct order would be:
app.UseHttpsRedirection();
app.UseStatusCodePages(async ctx =>
{
// Omitted for brevity.
});
app.UseStaticFiles();
var cookiePolicyOptions = new CookiePolicyOptions
{
// Omitted for brevity.
};
app.UseCookiePolicy(cookiePolicyOptions);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
// If the app uses session state, call Session Middleware after Cookie
// Policy Middleware and before MVC Middleware.
app.UseSession();
app.UseElmah(); // Not sure about this one. I don't know what it's supposed to do?
app.UseEndpoints(endpoints =>
{
// Omitted for brevity.
});
DapperExtensions.DapperExtensions.SetMappingAssemblies(new[]
{
// Omitted for brevity.
});

.NET Core Angular client SPA has issue redirecting on logout to IdentityServer4 IDP

I've an Angular Client which needs to be authentcated. This client is hosted in .NetCore SPA. Now using IdentityServer4, I've setup the IDP to authenticate the Client.
Everything works fine for login. Here the client automatically redirects to IDP project to login. Once credentials are entered it will redirect back to the client app.
However, for logout it's not working. On the Angular client which is hosted as a SPA in .netcore, I've a logout button. This logout button event will call the API controller in the same client. In this controller there is code to Signout from the httpContext.
My expectation was, when the logout action is called in the controller it will logout and redirect me back to the IDP. On a positive note, I've implemented the same in MVC client(no SPA and angular) and it works for the logout by redirecting me back to the IDP.
Also another positive, on the angular client on logout it actually tries to logout, but the CORS policy is blocking it. The error message in the browser console has the redirect url as part of the error message. This url works when i click on it by redirecting me to the IDP and logging me out.
I'm not able to figure out what CORS policy I have to add to make this redirect work. I've tried adding CORS to the startup class, but did not work. Help please
Browser error message:
Access to XMLHttpRequest at 'https://localhost:44336/connect/endsession?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44374%2Fsignout-callback-oidc&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IlMwbFpqUi1QazItS0dLc2xxaFlQQ2ciLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzkzNDc3NDcsImV4cCI6MTU3OTM0ODA0NywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJhdWQiOiJBY3Rpdml0eVRyYWNrZXJOZ0NsaWVudF9DbGllbnRJZCIsIm5vbmNlIjoiNjM3MTQ5NDQ1NDMyMDYwODM5LllUVmpaV1UyTURjdE5ERmhOeTAwTW1SbExXRm1NVGd0TWpKaVl6VTFOMlJoWkRkaE9EYzNPV1EwTm1VdFpEUmxaUzAwT1RNMkxUZ3hOV0l0T0RJeU5HWTVaR1l4T0RsaSIsImlhdCI6MTU3OTM0Nzc0NywiYXRfaGFzaCI6ImJwc3hLLThSU2Nwb2hKMnJPSlViQlEiLCJzX2hhc2giOiJ0M2R1RmljZDR1VTRTQlQ3S253cG1nIiwic2lkIjoiYTFIWnE5YXFMdnR1WHB6S1FUZUVvUSIsInN1YiI6ImQ4NjBlZmNhLTIyZDktNDdmZC04MjQ5LTc5MWJhNjFiMDdjNyIsImF1dGhfdGltZSI6MTU3OTM0Nzc0NywiaWRwIjoibG9jYWwiLCJhbXIiOlsicHdkIl19.bd35dk-lcolUxgoNAzzc4kKIORQIsmeSu5JaARpyqj1I6cv5P6LSHrcdw3YmZ80q_tF8WLi7ywIml-enEP4JAe-nbYw7gSlFt9qHtw5eSF37dMdBZq7UUXt6EoK29xs9lp6TyIB11pzgRZ8tPVAPw0Y8rNpGSGYtjfWjp7t4FdKthvUchAo_SNh6l40S5oV0Yo_YIWfHtjxM-nLZXia0YCvjNEQChmTmkzSMCIdGnVqawhIzQ_O7jv0c1T7kCwaF5YGyer3ZUyj1UM53JTBbbGpKDrDh2DV-kd4tvhoaLnWQAoUqCQ1Ofl_kHc8vffqE7RRPGmQLQYOM48186hIe0g&state=CfDJ8DRPXADjz9hKioMAFvg6DCP1P37ODZ4R81EV3uFXBpxiOLWoJY6GDEcbYNZzB--zZjv-Z94PSfMJkcoJhQcmHAvmM_9yKL9hPaGqmucpJrO_wv74Fj8bmdm8C7l_MJZ3VaNahF5Bqvi9tWFUikbr-HJ_uI0GiGX6qsj5mkrp8K4x&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:44374/api/Authorization/Logout') from origin 'https://localhost:44374' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
And the API controller logout code:
[HttpGet]
public async Task Logout()
{
var httpclient = new HttpClient();
var disco = await httpclient.GetDiscoveryDocumentAsync("https://localhost:44336/");
// get the access token to revoke
var accessToken = await HttpContext
.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenResponse =
await httpclient.RevokeTokenAsync(new TokenRevocationRequest
{
Address = disco.RevocationEndpoint,
ClientId = "App_ClientId",
ClientSecret = "someSecret",
Token = accessToken
});
if (revokeAccessTokenResponse.IsError)
{
throw new Exception("Problem encountered while revoking the access token."
, revokeAccessTokenResponse.Exception);
}
}
// Clears the local cookie ("Cookies" must match name from scheme)
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc");
}
Startup.cs with entire client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ActivityTrackerAPIHttpClient>();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration["IdentityServer:Authority"];
options.ClientId = Configuration["IdentityServer:ClientId"];
options.ClientSecret = Configuration["IdentityServer:ClientSecret"];
options.ResponseType = "code id_token";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("roles");
options.Scope.Add(Configuration["IdentityServer:ApiName"]);
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Remove("amr");
options.ClaimActions.DeleteClaim("sid");
options.ClaimActions.DeleteClaim("idp");
options.ClaimActions.MapJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = JwtClaimTypes.Role,
};
});
services.ConfigureLoggerService();
}
// 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");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync("oidc");
}
else
{
await next();
}
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
I came up with a temporary solution at that point. However I realized, it is ideal to do the client security in the angular side.
My temp solution below.
Startup.cs of client
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var protocolMessage = context.ProtocolMessage;
var param = "";
foreach (var parameter in protocolMessage.Parameters)
{
param += $"{parameter.Key}={parameter.Value}$";
}
var url = $"{protocolMessage.IssuerAddress}?{param}x-client-SKU={protocolMessage.SkuTelemetryValue}&x-client-ver=5.5.0.0";
context.HttpContext.Session.SetString("LogoutUrl", url);
return Task.FromResult(0);
}
};
Also enable session in the client startup.cs by adding services.AddSession(); in ConfigureServices method and then add app.UseSession(); in the Configure method.
In the controller
[HttpGet]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
var url = HttpContext.Session.GetString("LogoutUrl");
return Ok(url);
}

ASP.NET Core 2.2 MVC problem redirecting after signing in

I have recently been experiencing an issue when attempting to login on the web app that I am working on, I did not modify the Startup.cs file and it used to work last time when I used it, but now when I am trying to log in it redirects me back to the Login page, although signing in succeeds
var result = await _signInManager.PasswordSignInAsync(user.UserName,
model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
// return LocalRedirect(returnUrl);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
it hits the RetirectToAction, but I am sent back to Login Page, also I see two statuses in the network console, 200 and 302 which should be ok
Just a little update, after trying different things it looks like the signInManager does not sign me in at all
Startup.cs
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.AddDbContext<AORContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<AORContext>();
services.Configure<IdentityOptions>(options =>
{
// Default User settings.
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.User.RequireUniqueEmail = true;
});
services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 0;
});
services.ConfigureApplicationCookie(options =>
{
options.AccessDeniedPath = "/Account/AccessDenied";
//options.Cookie.Name = "YourAppCookieName";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
// ReturnUrlParameter requires
//using Microsoft.AspNetCore.Authentication.Cookies;
//options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
//options.SlidingExpiration = true;
});
services.AddMvc(config =>
{
// using Microsoft.AspNetCore.Mvc.Authorization;
// using Microsoft.AspNetCore.Authorization;
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// 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.UseExceptionHandler("/Home/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();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
I had the same problem, and this feels more like a hack than anything but I got it work by redirecting the action back to itself with a flag. And if the flag is set, redirect to the actual target page:
// GET: LoginWithToken
[AllowAnonymous]
[HttpGet("LoginWithToken")]
public async Task<ActionResult> LoginWithToken(string token = null, bool tokenLoginSuccess = false)
{
// redirect
if (tokenLoginSuccess)
return RedirectToAction(nameof(Index));
// set cookie
await schoolLoginService.SignInWithToken(this.HttpContext, token);
return RedirectToAction(nameof(LoginWithToken), new { tokenLoginSuccess = true });
}

Resources