Authentication problem with cookie in ASP .NET Core 6 - c#

I'm trying to use the authentication cookie for a successful login.
Here it is how I create a cookie authentication
private async Task<IList<string>> CreatingAuthCookie(ApplicationUser user, bool rememberMe)
{
var rolesUser = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, user.NameUser),
new Claim(ClaimTypes.Surname, user.LastNameUser),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
foreach (var role in rolesUser)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var identity = new ClaimsIdentity(claims, "NameCookieIdentity");
var claimsPrincipal = new ClaimsPrincipal(identity);
await HttpContext.SignInAsync("NameCookieIdentity", claimsPrincipal, new AuthenticationProperties()
{
IsPersistent = rememberMe,
});
return rolesUser;
}
Here is How it is handled
builder.Services.AddAntiforgery(options => {
options.Cookie.Name = "X-CSRF-TOKEN-NameCookieIdentity";
options.HeaderName = "X-CSRF-TOKEN-NameCookieIdentity";
options.FormFieldName = "X-CSRF-TOKEN-NameCookieIdentity";
});
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.AddAuthentication("NameCookieIdentity").AddCookie("NameCookieIdentity", option =>
{
option.Cookie.Name = "NameCookieIdentity";
option.Cookie.HttpOnly = true;
option.ExpireTimeSpan = System.TimeSpan.FromDays(2);
option.SlidingExpiration = true;
option.LoginPath = "/Identity/User/Login";
option.LogoutPath = "/Identity/User/Logout";
option.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
option.SlidingExpiration = true;
});
builder.Services.AddIdentity<ApplicationUser, IdentityRole >(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 8;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#+";
options.Lockout.DefaultLockoutTimeSpan = System.TimeSpan.FromHours(1);
options.Lockout.AllowedForNewUsers = true;
options.Lockout.MaxFailedAccessAttempts = 5;
options.SignIn.RequireConfirmedAccount = true;
options.SignIn.RequireConfirmedEmail = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
And here is how I managed the middleware
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions()
{
HttpsCompression = Microsoft.AspNetCore.Http.Features.HttpsCompressionMode.Compress,
OnPrepareResponse = (context) =>
{
var headers = context.Context.Response.GetTypedHeaders();
headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
MaxAge = TimeSpan.FromDays(7)
};
headers.Expires = DateTime.UtcNow.AddDays(7);
}
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
and so on..
The problem is: When I'm trying to reach an action inside a controller which is protected by the [Authorize] data annotation, this last one don't gives me the access to go inside.
I can access by only remove the data annotation Authorize on the top of the action.
Last thing.
This is How I access to the data stored inside the cookie.
var userId = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
Even with the a created cookie, by debugging this statement, I can't access to the data inside of the cookie and the var userId is null.
Please help me out and suggest me well staff to manage the authentication as well as possible.
I'm new in stackoverflow so be good with me :)

Related

How to access the database data in OnSignedIn .net Core - Entity framework

When a user signs in to a .net core MVC program, I need to view user-specific data from the database using logged in user id and save those data to current cookie. Following is the path I'm now taking. Show me how to do this if something is wrong.
is there any way access like
dbcontext.footable.where(x=> x.id = "somevalue")
Startup.cs
services.ConfigureIdentitySettings(apiSetting);
ConfigureIdentitySettings.cs
public static class IdentitySettingsExtensions
{
// Identity Setup
public static void ConfigureIdentitySettings(this IServiceCollection services, IConfigurationSection apiSetting)
{
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "foo";
options.ExpireTimeSpan = TimeSpan.FromMinutes(Configurations.ExipireTimeSpan);
options.ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
options.LoginPath = Configurations.LoginPath;
options.LogoutPath = Configurations.LogoutPath;
options.AccessDeniedPath = Configurations.AccessDeniedPath;
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents
{
OnSignedIn = context =>
{
// Here i got current logged in user id
var loggedInUserRole = context.Principal.GetLoggedInUserId();
//I need to access database data from here after that adding those data into current cookie
return Task.CompletedTask;
}
};
});
services.Configure<IdentityOptions>(options =>
{
// Providers
//options.Tokens.PasswordResetTokenProvider = PasswordResetTokenProviderName;
//options.Tokens.EmailConfirmationTokenProvider = ConfirmEmailTokenProviderName;
options.SignIn.RequireConfirmedEmail = true;
options.User.RequireUniqueEmail = true;
options.Password.RequireUppercase = Configurations.RequireUppercase;
options.Password.RequireLowercase = Configurations.RequireLowercase;
options.Password.RequireDigit = Configurations.RequireDigit;
options.Password.RequiredLength = Configurations.RequireLength;
options.Password.RequireNonAlphanumeric = Configurations.RequireNonAlphanumeric;
});
var firstLifeSpan = Convert.ToInt32(apiSetting["FirstEmailConfirmationLifeSpan"]);
var secondLifeSpan = Convert.ToInt32(apiSetting["SecondEmailConfirmationLifeSpan"]);
services.Configure<DataProtectionTokenProviderOptions>(o =>
o.TokenLifespan = TimeSpan.FromDays(firstLifeSpan > secondLifeSpan ? firstLifeSpan : secondLifeSpan));
}
}
You can access database data in OnSignedIn method like below:
services.ConfigureApplicationCookie(options =>
{
//...
options.Events = new CookieAuthenticationEvents
{
OnSignedIn = context =>
{
//Build an intermediate service provider
var sp = services.BuildServiceProvider();
//Resolve the services from the service provider
var myDbContext = sp.GetService<ApplicationDbContext>();
//access database data...
var data = myDbContext.footable.Where(x => x.Id== "xxx");
//...
return Task.CompletedTask;
}
};
});

Identity Server 4 - Role claims missing in OpenIdConnect handler in MVC client

I know this issue has been reported alot and there are a lot of articles on how to fix this. I've gone through a lot of them and still can't solve this.
I am setting custom claims on the Principal.Identity from within the OnSecurityTokenValidated callback in Identity Server like so:
public async Task SecurityTokenValidated(SecurityTokenValidatedContext context) {
var identity = context.Principal.Identity as ClaimsIdentity;
foreach(var claim in context.Principal.Claims.Where(x = >x.Type == "adGroupClaimType").ToList()) {
var groupName = Configuration.GetSection("ClaimMappings").GetValue < string > ($ "Prefix_{claim.Value}");
if (!string.IsNullOrWhiteSpace(groupName)) {
identity.AddClaim(new Claim(ClaimTypes.Role, groupName));
}
identity.RemoveClaim(claim);
}
}
The following is the configuration I use in Identity Server:
public static IEnumerable < IdentityResource > IdentityResources = >new List < IdentityResource > {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource("roles", new[] {
ClaimTypes.Role
})
};
public static IEnumerable < ApiScope > ApiScopes = >new List < ApiScope > {
new ApiScope("api", "API")
};
public static IEnumerable < Client > Clients = >new List < Client > {
new Client {
ClientId = "mvc-openid",
ClientSecrets = {
new Secret("secret".Sha256())
},
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RedirectUris = {
"https://localhost:6001/signin-oidc"
},
AllowedScopes = new List < string > {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api",
"roles"
},
RequirePkce = false,
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true
}
};
There's an MVC client which connects to the Identity Server using OpenIdConnect. The code is given below:
public void ConfigureServices(IServiceCollection services) {
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options = >{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie(options = >{
options.ExpireTimeSpan = 15;
options.SlidingExpiration = true;
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.None;
}).AddOpenIdConnect("oidc", options = >{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc-openid";
options.ClientSecret = "secret";
options.ResponseType = "code id_token token";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Add("api");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.Events.OnTokenValidated = OnTokenValidated;
options.TokenValidationParameters = new TokenValidationParameters {
NameClaimType = ClaimTypes.Name,
RoleClaimType = ClaimTypes.Role
};
options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Role, ClaimTypes.Role);
});
}
private Task OnTokenValidated(TokenValidatedContext context) {
var t = context.Principal.Claims;
return Task.CompletedTask;
}
When I inspect the claims inside OnTokenValidated, I could see that all the role claims that I set from Identity Server are missing. Can someone tell me where I am going wrong? I have tried almost everything found on the Stack Overflow threads about this issue. Clueless right now as to what to do next.
I have also only found this solution so far.
options.Events.OnUserInformationReceived = context =>
{
var roleElement = context.User.RootElement.GetProperty("role");
var claims = new List<Claim>();
if (roleElement.ValueKind == System.Text.Json.JsonValueKind.Array)
{
foreach (var r in roleElement.EnumerateArray())
claims.Add(new Claim(JwtClaimTypes.Role, r.GetString()));
}
else
{
claims.Add(new Claim(JwtClaimTypes.Role, roleElement.GetString()));
}
var id = context.Principal.Identity as ClaimsIdentity;
id.AddClaims(claims);
return Task.CompletedTask;
};
https://github.com/skoruba/IdentityServer4.Admin/issues/109

Where and when does CookieApplicationOptions LoginPath check to redirect?

Specifically, my question is about the CookieApplicationOptions and the LoginPath. My project successfully uses Aspnetcore.identity to login and create a session cookie.
My assumption was that I would be redirected to my LoginPath once I log in and create the cookie, and before I ever log in and create the cookie, I would be directed to my AccessDeniedPath. Neither of these happens, so I'm wondering when these are call to be redirected.
Currently in my Startup.cs I have
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
//Connect DB
services.AddDbContext<DollaWebContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DollaWebContext")));
//Create Table
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<DollaWebContext>()
.AddDefaultTokenProviders();
//Configure options for user
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
//options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = false;
// User settings
options.User.RequireUniqueEmail = false;
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedPhoneNumber = false;
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = new PathString("/register");
options.LogoutPath = new PathString("/login");
options.AccessDeniedPath = new PathString("/login");
options.SlidingExpiration = true;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "$DollaApi", Version = "v1" });
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
}
From research, it seems to have something to do with the [Authorize] tag in the controller but most examples don't give a concrete reason why.
For configuring services.ConfigureApplicationCookie, it will be used in CookieAuthenticationHandler.
For authentication process, it is achieved by app.UseAuthorization(); which will call AuthorizationMiddleware.
if (authorizeResult.Challenged)
{
if (policy.AuthenticationSchemes.Any())
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ChallengeAsync(scheme);
}
}
else
{
await context.ChallengeAsync();
}
return;
}
else if (authorizeResult.Forbidden)
{
if (policy.AuthenticationSchemes.Any())
{
foreach (var scheme in policy.AuthenticationSchemes)
{
await context.ForbidAsync(scheme);
}
}
else
{
await context.ForbidAsync();
}
return;
}
For context.ChallengeAsync(scheme);, it will call AuthenticationService.
public virtual async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
{
if (scheme == null)
{
var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync();
scheme = defaultChallengeScheme?.Name;
if (scheme == null)
{
throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions).");
}
}
var handler = await Handlers.GetHandlerAsync(context, scheme);
if (handler == null)
{
throw await CreateMissingHandlerException(scheme);
}
await handler.ChallengeAsync(properties);
}
And above code will call CookieAuthenticationHandler.

Creating user with same UserName and Email always return invalid user name error

Tried to create a user in identity tables using .net core. But it always return an error invalid UserName.
var user = new ApplicationUser();
user.UserName = "SuperAdmin1#gmail.com";
user.Email = "SuperAdmin1#gmail.com";
string userPWD = "Admin#123";
var chkUser = await UserManager.CreateAsync(user, userPWD);
ConfigureServices is follows.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#/";
})
.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
already added AllowedUserNameCharacters as "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._#/"
But still return error
User name '' is invalid, can only contain letters or digits.
You can turn this restriction off on the user validator:
UserManager.UserValidator = new UserValidator<TUser>(UserManager) { AllowOnlyAlphanumericUserNames = false }

Provider claims in IProfileService

When I auth using oidc I get back a bunch of claims. If I do not add my custom IProfileService all of these claims are passed through in the id_token that identity server issues. If I provide my own ProfileService, the list of claims on the Subject is a subset of what comes back from the idp. Is there any way to get the full list in the profile service?
Here is the relevant info from Startup.cs:
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
}).AddProfileService<ProfileService>();
services.AddAuthentication()
.AddOpenIdConnect("Name", "Name", o =>
{
o.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
o.SignOutScheme = IdentityServerConstants.SignoutScheme;
o.Authority = "https://sub.domain.com/adfs/";
o.ClientId = "00000000-0000-0000-0000-000000000000";
o.ClientSecret = "secret";
o.ResponseType = "id_token";
o.SaveTokens = true;
o.CallbackPath = "/signin-adfs";
o.SignedOutCallbackPath = "/signout-callback-adfs";
o.RemoteSignOutPath = "/signout-adfs";
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
and my ProfileService:
public class ProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var objectGuidClaim = context.Subject.Claims.FirstOrDefault(x => x.Type == "ObjectGUID");
if (objectGuidClaim != null)
{
var userId = new Guid(Convert.FromBase64String(objectGuidClaim.Value));
context.IssuedClaims.Add(new Claim("UserId", userId.ToString()));
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
So in my case, without the ProfileService then ObjectGUID is passed through, but using the ProfileService, it's not available in context.Subject.Claims list.
My goal is to take the "ObjectGUID" claim from the idp which is a base64 encoded guid and convert it to a hex string and pass that along as the "UserId" claim from identity server.
I'm not even sure this is the best way. I've also tried converting it through ClaimActions but my action never executes (I tested with a random guid to make sure it wasn't something with the conversion):
o.ClaimActions.MapCustomJson("UserId", obj => {
return Guid.NewGuid().ToString();
});
Is this a better way? Why is it not executing?
Try to:
ensure your Subject does not contain
http://schemas.company.com/identity/claims/objectguid instead of just ObjectGUID
extend your
OpenIdConnect configuration with: o.GetClaimsFromUserInfoEndpoint =
true; together with o.ClaimActions.MapUniqueJsonKey("ObjectGUID", "ObjectGUID"); or o.ClaimActions.MapUniqueJsonKey("http://schemas.company.com/identity/claims/objectguid", "ObjectGUID");
if nothing before helped, try:
o.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
var identity = context.Principal.Identity as ClaimsIdentity;
StringBuilder builder = new StringBuilder();
var claims = identity?.Claims.Select(x => $"{x.Type}:{x.Value};");
if (claims != null)
builder.AppendJoin(", ", claims);
Logger.LogInformation($"Ticket received: [Claims:{builder}]");
identity?.AddClaim(new Claim("userId", Guid.NewGuid().ToString()));
//you can embed your transformer here if you like
return Task.CompletedTask;
}};
(you can examine the exact incoming ticket here and leave the logging anyway for future purposes)

Categories