IdentityServer4 custom AuthenticationHandler can't find all claims for a user - c#

I am using the IdentityServer4 sample that uses Asp.Net Identity and EntityFramework.
I am trying to create group controls using custom policies based on claims/roles.
My problem is that when I try and get the users claims in the authorization handler the claims I am looking for are not returned.
Looking at the database in SSMS I find the claims/roles I created are in tables called "AspNetRoles", "AspNetRoleClaims", "AspNetUserClaims" along with the user I created being in "AspNetUsers" and keys for the user and role being in "AspNetUserRoles".
When I call to get the users claims for authorization the list of claims seems to come from the "IdentityClaims" table.
There doesn't seem to be a simple way to check for claims in "AspNetClaims" like there is for claims in "IdentityClaims" so I assume I've made an error somewhere.
I've looked around a fair bit for a solution and tried a fair few things but I can't find anything that works.
Below is the code i thought would me most relevant to the question along with some screenshots taken of the running code.
Any help would be much appreciated, thanks in advance.
Code
MvcClient.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthorization(options =>
{
options.AddPolicy("AdminRights", policy =>
{
policy.Requirements.Add(new AdminRequirement());
policy.RequireAuthenticatedUser();
policy.AddAuthenticationSchemes("Cookies");
});
});
services.AddSingleton<IAuthorizationHandler, AdminRequirementHandler>();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token token"; // NEW CHANGE (token)
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("api1");
options.Scope.Add("AdminPermission"); // NEW CHANGE
options.Scope.Add("offline_access");
});
}
MvcClient.Controllers.HomeController
[Authorize(Policy = "AdminRights")]
public IActionResult Administrator()
{
return View();
}
IdentityServerWithAspIdAndEF.Startup (Called last in Configure)
private async Task CreateSuperuser(IServiceProvider serviceProvider, ApplicationDbContext context)
{
//adding custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roleNames = { "Administrator", "Internal", "Customer" };
foreach (var roleName in roleNames)
{
//creating the roles and seeding them to the database
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (roleExist)
await RoleManager.DeleteAsync( await RoleManager.FindByNameAsync(roleName) );
var newRole = new IdentityRole(roleName);
await RoleManager.CreateAsync(newRole);
if(roleName == "Administrator")
await RoleManager.AddClaimAsync(newRole, new Claim("AdminPermission", "Read"));
}
//creating a super user who could maintain the web app
var poweruser = new ApplicationUser
{
UserName = Configuration.GetSection("UserSettings")["UserEmail"],
Email = Configuration.GetSection("UserSettings")["UserEmail"]
};
string UserPassword = Configuration.GetSection("UserSettings")["UserPassword"];
var _user = await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]);
if (_user != null)
await UserManager.DeleteAsync( await UserManager.FindByEmailAsync(Configuration.GetSection("UserSettings")["UserEmail"]) );
var createPowerUser = await UserManager.CreateAsync(poweruser, UserPassword);
if (createPowerUser.Succeeded)
{
//here we tie the new user to the "Admin" role
await UserManager.AddToRoleAsync(poweruser, "Administrator");
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Create"));
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Update"));
await UserManager.AddClaimAsync(poweruser, new Claim("AdminPermission", "Delete"));
}
}
IdentityServerWithAspIdAndEF.Config
public class Config
{
// scopes define the resources in your system
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource() // NEW CHANGE
{
Name = "AdminPermission",
DisplayName = "Admin Permission",
UserClaims =
{
"AdminPermission",
}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
{
Scopes = // NEW CHANGE
{
new Scope("AdminPermission", "Admin Permission")
{
UserClaims = { "AdminPermission" }
}
}
}
};
}
// clients want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
// client credentials client
return new List<Client>
{
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false, // NEW CHANGE (false)
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
"AdminPermission", // NEW CHANGE
},
AllowOfflineAccess = true,
},
// Other Clients omitted as not used.
};
}
}
IdentityServerWithAspIdAndEF.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
string connectionString = Configuration.GetConnectionString("DefaultConnection");
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
})
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "implicit";
options.SaveTokens = true;
// options.GetClaimsFromUserInfoEndpoint = true; // NEW CHANGE
// options.ResponseType = "code id_token token"; // NEW CHANGE
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
MvcClient.Authorization
public class AdminRequirementHandler : AuthorizationHandler<AdminRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
{
Console.WriteLine("User Identity: {0}", context.User.Identity);
Console.WriteLine("Role is 'Administrator'? : {0}", context.User.IsInRole("Administrator"));
Console.WriteLine("Identities of user:-");
foreach (var v in context.User.Identities)
{
Console.WriteLine("\tName: {0},\tActor: {1},\tAuthType: {2},\tIsAuth: {3}", v.Name, v.Actor, v.AuthenticationType, v.IsAuthenticated);
Console.WriteLine("\n\tClaims from Identity:-");
foreach (var c in v.Claims)
Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
}
Console.WriteLine("Claims from other source:-");
foreach(Claim c in context.User.Claims)
{
Console.WriteLine("\t\tType: {0},\tValue: {1},\tSubject: {2},\tIssuer: {3}", c.Type, c.Value, c.Subject, c.Issuer);
}
Console.WriteLine("\n *** Starting Authroization. ***\n");
Claim
role = context.User.FindFirst("role"),
accessLevel = context.User.FindFirst("AdminPermission");
if (role == null)
Console.WriteLine("\tUser as no 'role' : '{0}'", role == null ? "null" : role.Value);
else
Console.WriteLine("\tUser has 'role' : '{0}'", role.Value);
if (accessLevel == null)
Console.WriteLine("\tUser has no claim 'AdminPermission' : '{0}'", accessLevel == null ? "null" : accessLevel.Value);
else
Console.WriteLine("\tUser has 'AdminPermission' : '{0}'", accessLevel.Value);
if (role != null && accessLevel != null)
{
if (role.Value == "Administrator" && accessLevel.Value == "Read")
context.Succeed(requirement);
}
else
Console.WriteLine("\n *** Authorization Failue. ***\n");
return Task.CompletedTask;
}
}
Data
ApiClaims :
ApiResources : api1
ApiScopeClaims : AdminPermission, ApiScopeId = 2
ApiScopes : api1, ApiResourceId = 1
: AdminPermission, ApiResourceId = 1
ApiSecrets :
AspNetRoleClaims : AdminPermission, Read, RoleId = b2f03...
AspNetRoles : Administrator, b2f03...
: Customer, 779f7...
: Internal, 10d5d...
AspNetUserClaims : AdminPermission, Create, UserId = 8ee62...
: AdminPermission, Update, UserId = 8ee62...
: AdminPermission, Delete, UserId = 8ee62...
AspNetUserLogins :
AspNetUserRoles : UserId = 8ee62..., RoleId = b2f03...
AspNetUsers : superuser#mail.com, Id = 8ee62...
AspNetUserTokens :
ClientClaims :
ClientCorsOrigins :
ClientGrantTypes : hybrid, ClientId = 1
: client_credentials, ClientId = 1
: client_credentials, ClientId = 2
: password, ClientId = 3
ClientIdPRestrictions :
ClientPostLogoutRedirectUris : http://localhost:5002/signout-callback-oidc, ClientId = 1
ClientProperties :
ClientRedirectUris : http://localhost:5002/signin-oidc, ClientId = 1
Clients : mvc, AllowAccessTokenViaBrowser = 1, AllowOfflineAccess = 1, RequireConsent = 0
: client ...
: ro.client ...
ClientScopes : openid, ClientId = 1
: profile, ClientId = 1
: AdminPermission, ClientId = 1
ClientSecrets : Type = SharedSecret
IdentityClaims : Id IdentityResourceId Type
1 1 sub
2 2 name
3 2 family_name
4 2 given_name
5 2 middle_name
6 2 nickname
7 2 preferred_username
8 2 profile
9 2 picture
10 2 website
11 2 gender
12 2 birthdate
13 2 zoneinfo
14 2 locale
15 2 updated_at
16 3 AdminPermission
IdentityResources : openid
: profile
: AdminPermission
PersistedGrants : [8x] Type = refresh_token
Images
Autos at the beginning of the AuthorizationHandler
Claims shown on the identity server
Claims shows on the MVC client
Log from when an authenticate user attempts to access
JWT received by MVC client
Database tables

--EDIT--
I've Forked your code and solved the issue.
Here is a link to my repo.
https://github.com/derekrivers/IdentityServer4
In order to fix your solution, i changed the server authentication response type too :-
"code Id_token"
In the MVC Client setup in your config.cs I added the following properties :-
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true
I've also removed the adminpermission scope from the mvc client, as it isn't required.
I've also amended the AdminRequirementHandler.cs slightly, but i will let you explore that in my repo.
Basically, We have ensured that the user claims are in the Identity token, and by doing this they are then accessible within you AdminRequirementHandler
Hope this helps.

Related

ASP.NET Core 6: Users are logged out after 30 minutes, need 6 hours

I have an issue where no matter which settings I adjust, a user who has signed in is forcibly logged out after 30 minutes. I would like to extend this timeframe to 6 hours.
I am using a system where login credentials are provided externally. The system receives a JWT and uses the information contained there to authenticate the user. This is the root cause of the issue; users that log in via the .NET Framework's built-in authentication system don't have this problem. I don't know why or how.
Here are the highlights of what I have tried:
configuring application cookies in Program.cs:
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.ExpireTimeSpan = TimeSpan.FromHours(6);
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
});
Using persistence and authentication properties in the authentication process:
await _signInManager.SignInAsync(
user,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6)
});
I have ensured I have data protection enabled in Program.cs:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
I have published it to a dedicated IIS server where I set the Idle Timeout to 6 hours.
I have set SlidingExpiration = true in all the places that have the option to do so.
I have tried all of the above together, separate, and in combination. User sessions are still restricted to 30 minutes. When I set the Application Cookies to expire after 1 second, the session expires after 1 second. But when I set them to 6 hours, it still expires at 30 minutes. I have no idea why.
What am I missing here? I have been struggling with this for days and have still found no solution. I've found similar questions on Stack Overflow and their solutions all include what I have tried here, to no avail.
I have double-checked and noticed that the 30 minute time-out does not affect users that authenticate via the .NET Framework's built-in login system (where they enter their username/password). This only seems to affect users that authenticate using the credentials delivered via JWT.
Here is the full Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("ConnectionString");
builder.Services.AddDbContext<DBContext>(options => options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllersWithViews();
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(6);
});
//Add JWT bearer and denied paths
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Jwt:Key"))
};
});
//GDPR compliance
builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.ConfigureNonBreakingSameSiteCookies();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = ".AspNetCore.Identity.Application";
options.Cookie.MaxAge = TimeSpan.FromHours(6);
options.SlidingExpiration = true;
options.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = x =>
{
x.Response.Redirect("https://localhost:44329/Expired/Index/");
return Task.CompletedTask;
}
};
options.ExpireTimeSpan = TimeSpan.FromDays(14);
});
//define policy for different authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Administrator"));
options.AddPolicy("UsersOnly", policy => policy.RequireRole("User", "Editor", "Author"));
options.AddPolicy("RequireApprovedUser", policy => policy.Requirements.Add(new ApprovedUserRequirement(true)));
});
builder.Services.AddScoped<IAuthorizationHandler, ApprovedUserRequirementHandler>();
//Data Protection configuration
var keysFolder = Path.Combine(builder.Environment.ContentRootPath, "UserKeys");
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<DBContext>();
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(6);
options.Lockout.MaxFailedAccessAttempts = 5;
options.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultTokenProviders()
.AddDefaultUI()
.AddEntityFrameworkStores<DBContext>();
builder.Services.AddRazorPages();
//this will get the email settings in appsettings
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
//Use for sending email
builder.Services.AddTransient<IMailService, Project.Services.EmailSender>();
// Register the Google Analytics configuration
builder.Services.Configure<GoogleAnalyticsOptions>(options => builder.Configuration.GetSection("GoogleAnalytics").Bind(options));
// Register the TagHelperComponent for Google Analytics
builder.Services.AddTransient<ITagHelperComponent, GoogleAnalyticsTagHelper>();
builder.Services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, UserClaimsPrincipalFactory<IdentityUser, IdentityRole>>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode >= 400)
{
context.Request.Path = "/Error/Index/" + context.Response.StatusCode;
await next();
}
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors(builder => builder.AllowAnyOrigin()
.WithOrigins("https://localhost:44329", "https://localhost:44329")
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
Here is the code that authenticates incoming users:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}
Here are the settings I used in IIS. Note that I will have trouble adjusting settings unavailable on Plesk.
Welp, it took a lot of digging, but I found the solution.
I needed to ensure that users were saved to the IdentityUser tables using _userManager.CreateAsync(). The code for registering users I used would create a new IdentityUser every time without ensuring it went in the database. Without an entry in the IdentityUser tables, .NET Core can't use keys or sessions properly.
Here is the change to that code:
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> TokenAuthenticationSignInAsync([FromQuery] string id)
{
try
{
string qstring = HttpContext.Request.Query["jwt"];
//This parses the JWT into a UserPOCO
ParseJWT parseJWT = new ParseJWT(_config);
//UserPOCO has 3 properties: Name (string), Email (string), and ValidToken (bool)
UserPOCO user = parseJWT.ParseToUser(qstring);
if (user.ValidToken != true)
{
_logger.LogWarning($"Invalid Login");
return RedirectToAction("Index", "Forbidden");
}
else
{
IdentityUser? iUser;
//check to see if user is in IdentityUser
iUser = await _userManager.FindByEmailAsync(user.Email);
//if user is not in IdentityUser, create a new one:
if(iUser == null)
{
iUser = new()
{
UserName = user.Name,
Email = user.Email,
EmailConfirmed = true
};
//Very important! This allows the system to remember users
//Performing this operation also gives iUser the necessary keys
await _userManager.CreateAsync(iUser);
}
await _signInManager.SignInAsync(
iUser,
new AuthenticationProperties {
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddHours(6),
AllowRefresh = true
});
return RedirectToAction("Index", "Dashboard");
}
}
catch (System.Exception e)
{
_logger.LogError(e, $"Error in {nameof(TokenAuthenticationSignIn)}");
return NotFound();
}
}

403 Error on JWT Role Based Authorization using .NET Core Web API 3.1

I am getting 403 Forbidden while trying to implement Role based JWT Authorization using .NET Core Web API 3.1 version. Below is how my code looks like:
// API
[HttpGet, Route("GetAll")]
[Authorize(Roles = "Admin")]
public IEnumerable<Users> GetAllUsers(string environment) {}
// JWT Token Generation
public UserDetail GenerateToken(string userName, string password)
{
string key = Configuration["Jwt:Key"];
var issuer = Configuration["Jwt:Issuer"];
var audience = Configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
IdentityModelEventSource.ShowPII = true;
//Create a List of Claims, Keep claims name short
var roleClaims = GetRoleClaimsFor(user);
//Create Security Token object by giving required parameters
var token = new JwtSecurityToken(issuer,
audience: audience,
claims: roleClaims,
expires: DateTime.Now.AddMinutes(15),
signingCredentials: credentials);
//Generate JWT Token
var userToken = new JwtSecurityTokenHandler().WriteToken(token);
}
// StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = Configuration["Jwt:Policy"];
}).AddJwtBearer(Configuration["Jwt:Policy"], options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
// JWT Authorization configuration
services.AddAuthorization(auth =>
{
auth.AddPolicy(Configuration["Jwt:Policy"], new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireClaim(ClaimTypes.Name, Configuration["Jwt:RequiredClaim"]).Build());
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
}
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(role.UserGroup.GroupName, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}
I am able to run and see all the roles as claims from Principal.Identity after validating the token. But the Roles are not working as expected.
Creating the claims as ClaimsType.Role instead of custom string solved the issue. Below is the refactored GetClaimsFor method
private IList<Claim> GetRoleClaimsFor(UserPrincipal user)
{
var roleClaims = new List<Claim>();
UserRoles = new List<string>();
// Pull only groups where user belongs to
PrincipalSearchResult<Principal> groups = user.GetGroups();
// Filter user groups related to GBITs application alone
var gbitsGroups = (from grp in groups
select grp);
// Loop through gbitsGroups and assign the related role
foreach (GroupPrincipal gbitsGrp in gbitsGroups)
{
try
{
// Pull UserGroup related Permissions
var roles = _userGroupPermissionRepository.GetUserGroupPermissionFor(gbitsGrp.Name);
// Loop through UserGroupPermissions to create Claims
foreach (var role in roles)
{
var claim = new Claim(ClaimsType.Role, role.Permission.PermissionName);
roleClaims.Add(claim);
}
// Load Roles collection
IList<string> usrRoles = (from role in roles
select role.Permission.PermissionName).ToList();
UserRoles.AddRange(usrRoles);
}
catch (Exception ex)
{
throw ex;
}
}
return roleClaims;
}

Check claims (email) before creating a new cookie, without asp. core identity store using asp net core social login

I'm working on my hobby project where I'have implemented social login via Google.
now I want to prevent this so that only certain user can sign in into the app, As I found that there is no way to restrict this on google OAuth side, So I have added a table to store the email and role.
if the email address is not found in that table I want to prevent a user from signing.
services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(googleOption =>
{
googleOption.ClientId = Configuration["Authentication:Google:ClientID"]; ;
googleOption.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
googleOption.Events.OnRemoteFailure = (context) =>
{
context.HandleResponse();
return context.Response.WriteAsync("<script>window.close();</script>");
};
googleOption.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents
{
OnTicketReceived = async ctx =>
{
string emailAddress = ctx.Principal.
FindFirstValue("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
var db = ctx.HttpContext.RequestServices.GetRequiredService<DbContext>();
var roles = await db.EmailRoles.Where(c => c.Email == emailAddress).ToListAsync();
if (roles.Count > 1)
{
var claims = new List<Claim>();
foreach (var item in roles)
{
claims.Add(new Claim(ClaimTypes.Role, item.Role));
}
var appIdentity = new ClaimsIdentity(claims);
ctx.Principal.AddIdentity(appIdentity);
}
}
};
});
I think you are looking for OnCreatingTicket. this will allow you to test the users as their logging in. In this example only gmail.com emails would be allowed to login anyone else would be kicked out
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle("Google", options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
options.Events = new OAuthEvents
{
OnCreatingTicket = context =>
{
string domain = context.User.Value<string>("domain");
if (domain != "gmail.com")
throw new GoogleAuthenticationException("You must sign in with a gmail.com email address");
return Task.CompletedTask;
}
};
});

Manually generate IdentityServer4 reference token and save to PersistedGrants table

I have learnt IdentityServer4 for a week and successfully implemented a simple authentication flow with ResourceOwnedPassword flow.
Now, I'm implementing Google authentication with IdentityServer4 by following this tutorial
This is what I am doing:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
const string connectionString = #"Data Source=.\SQLEXPRESS;database=IdentityServer4.Quickstart.EntityFramework-2.0.0;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddProfileService<IdentityServerProfileService>()
.AddResourceOwnerValidator<IdentityResourceOwnerPasswordValidator>()
// this adds the config data from DB (clients, resources)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
{
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
};
})
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
});
// Add jwt validation.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddIdentityServerAuthentication(options =>
{
// base-address of your identityserver
options.Authority = "https://localhost:44386";
options.ClaimsIssuer = "https://localhost:44386";
// name of the API resource
options.ApiName = "api1";
options.ApiSecret = "secret";
options.RequireHttpsMetadata = false;
options.SupportedTokens = SupportedTokens.Reference;
});
//...
}
** Google controller (Which is for handling returned token from Google **
public class GLoginController : Controller
{
#region Properties
private readonly IPersistedGrantStore _persistedGrantStore;
private readonly IUserFactory _userFactory;
private readonly IBaseTimeService _baseTimeService;
private readonly ITokenCreationService _tokenCreationService;
private readonly IReferenceTokenStore _referenceTokenStore;
private readonly IBaseEncryptionService _baseEncryptionService;
#endregion
#region Constructor
public GLoginController(IPersistedGrantStore persistedGrantStore,
IBaseTimeService basetimeService,
ITokenCreationService tokenCreationService,
IReferenceTokenStore referenceTokenStore,
IBaseEncryptionService baseEncryptionService,
IUserFactory userFactory)
{
_persistedGrantStore = persistedGrantStore;
_baseTimeService = basetimeService;
_userFactory = userFactory;
_tokenCreationService = tokenCreationService;
_referenceTokenStore = referenceTokenStore;
_baseEncryptionService = baseEncryptionService;
}
#endregion
#region Methods
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login()
{
var authenticationProperties = new AuthenticationProperties
{
RedirectUri = "/api/google/handle-external-login"
};
return Challenge(authenticationProperties, "Google");
}
[HttpGet("handle-external-login")]
//[Authorize("ExternalCookie")]
[AllowAnonymous]
public async Task<IActionResult> HandleExternalLogin()
{
//Here we can retrieve the claims
var authenticationResult = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var principal = authenticationResult.Principal;
var emailAddress = principal.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(emailAddress))
return NotFound(new ApiMessageViewModel("Email is not found"));
// Find user by using username.
var loadUserConditions = new LoadUserModel();
loadUserConditions.Usernames = new HashSet<string> { emailAddress };
loadUserConditions.Pagination = new PaginationValueObject(1, 1);
// Find users asynchronously.
var loadUsersResult = await _userFactory.FindUsersAsync(loadUserConditions);
var user = loadUsersResult.FirstOrDefault();
// User is not defined.
if (user == null)
{
user = new User(Guid.NewGuid(), emailAddress);
user.Email = emailAddress;
user.HashedPassword = _baseEncryptionService.Md5Hash("abcde12345-");
user.JoinedTime = _baseTimeService.DateTimeUtcToUnix(DateTime.UtcNow);
user.Kind = UserKinds.Google;
user.Status = UserStatuses.Active;
//await _userFactory.AddUserAsync(user);
}
else
{
// User is not google account.
if (user.Kind != UserKinds.Google)
return Forbid("User is not allowed to access system.");
}
var token = new Token(IdentityServerConstants.TokenTypes.IdentityToken);
var userCredential = new UserCredential(user);
token.Claims = userCredential.GetClaims();
token.AccessTokenType = AccessTokenType.Reference;
token.ClientId = "ro.client";
token.CreationTime = DateTime.UtcNow;
token.Audiences = new[] {"api1"};
token.Lifetime = 3600;
return Ok();
}
#endregion
}
Everything is fine, I can get claims returned from Google OAuth2, find users in database using Google email address and register them if they dont have any acounts.
My question is: How can I use Google OAuth2 claims that I receive in HandleExternalLogin method to generate a Reference Token, save it to PersistedGrants table and return to client.
This means when user access https://localhost:44386/api/google/login, after being redirected to Google consent screen, they can receive access_token, refresh_token that has been generated by IdentityServer4.
Thank you,
In IdentityServer the kind (jwt of reference) of the token is
configurable for each client (application), requested the token.
AccessTokenType.Reference is valid for TokenTypes.AccessToken not
TokenTypes.IdentityToken as in your snippet.
In general it would be simpler to follow the original quickstart and then extend the generic code up to your needs. What I can see now in the snippet above is just your specific stuff and not the default part, responsible for creating the IdSrv session and redirecting back to the client.
If you still like to create a token manually:
inject ITokenService into your controller.
fix the error I mentioned above: TokenTypes.AccessToken instead of TokenTypes.IdentityToken
call var tokenHandle = await TokenService.CreateAccessTokenAsync(token);
tokenHandle is a key in PersistedGrantStore

ASP.NET Core JWT mapping role claims to ClaimsIdentity

I want to protect ASP.NET Core Web API using JWT. Additionally, I would like to have an option of using roles from tokens payload directly in controller actions attributes.
Now, while I did find it out how to use it with Policies:
Authorize(Policy="CheckIfUserIsOfRoleX")
ControllerAction()...
I would like better to have an option to use something usual like:
Authorize(Role="RoleX")
where Role would be automatically mapped from JWT payload.
{
name: "somename",
roles: ["RoleX", "RoleY", "RoleZ"]
}
So, what is the easiest way to accomplish this in ASP.NET Core? Is there a way to get this working automatically through some settings/mappings (if so, where to set it?) or should I, after token is validated, intercept generation of ClaimsIdentity and add roles claims manually (if so, where/how to do that?)?
You need get valid claims when generating JWT. Here is example code:
Login logic:
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] ApplicationUser applicationUser) {
var result = await _signInManager.PasswordSignInAsync(applicationUser.UserName, applicationUser.Password, true, false);
if(result.Succeeded) {
var user = await _userManager.FindByNameAsync(applicationUser.UserName);
// Get valid claims and pass them into JWT
var claims = await GetValidClaims(user);
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
//...
} else {
throw new ApiException('Wrong username or password', 403);
}
}
Get user claims based UserRoles, RoleClaims and UserClaims tables (ASP.NET Identity):
private async Task<List<Claim>> GetValidClaims(ApplicationUser user)
{
IdentityOptions _options = new IdentityOptions();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName)
};
var userClaims = await _userManager.GetClaimsAsync(user);
var userRoles = await _userManager.GetRolesAsync(user);
claims.AddRange(userClaims);
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
var role = await _roleManager.FindByNameAsync(userRole);
if(role != null)
{
var roleClaims = await _roleManager.GetClaimsAsync(role);
foreach(Claim roleClaim in roleClaims)
{
claims.Add(roleClaim);
}
}
}
return claims;
}
In Startup.cs please add needed policies into authorization:
void ConfigureServices(IServiceCollection service) {
services.AddAuthorization(options =>
{
// Here I stored necessary permissions/roles in a constant
foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
options.AddPolicy(prop.GetValue(null).ToString(), policy => policy.RequireClaim(ClaimType.Permission, prop.GetValue(null).ToString()));
}
});
}
ClaimPermission:
public static class ClaimPermission
{
public const string
CanAddNewService = "Tự thêm dịch vụ",
CanCancelCustomerServices = "Hủy dịch vụ khách gọi",
CanPrintReceiptAgain = "In lại hóa đơn",
CanImportGoods = "Quản lý tồn kho",
CanManageComputers = "Quản lý máy tính",
CanManageCoffees = "Quản lý bàn cà phê",
CanManageBillards = "Quản lý bàn billard";
}
Use the similar snippet to get all pre-defined permissions and insert it to asp.net permission claims table:
var staffRole = await roleManager.CreateRoleIfNotExists(UserType.Staff);
foreach (var prop in typeof(ClaimPermission).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
await roleManager.AddClaimIfNotExists(staffRole, prop.GetValue(null).ToString());
}
I am a beginner in ASP.NET, so please let me know if you have better solutions.
And, I don't know how worst when I put all claims/permissions into JWT. Too long? Performance ? Should I store generated JWT in database and check it later for getting valid user's roles/claims?
This is my working code! ASP.NET Core 2.0 + JWT. Adding roles to JWT token.
appsettings.json
"JwtIssuerOptions": {
"JwtKey": "4gSd0AsIoPvyD3PsXYNrP2XnVpIYCLLL",
"JwtIssuer": "http://yourdomain.com",
"JwtExpireDays": 30
}
Startup.cs
// ===== Add Jwt Authentication ========
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
// jwt
// get options
var jwtAppSettingOptions = Configuration.GetSection("JwtIssuerOptions");
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtAppSettingOptions["JwtIssuer"],
ValidAudience = jwtAppSettingOptions["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
AccountController.cs
[HttpPost]
[AllowAnonymous]
[Produces("application/json")]
public async Task<object> GetToken([FromBody] LoginViewModel model)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);
if (result.Succeeded)
{
var appUser = _userManager.Users.SingleOrDefault(r => r.Email == model.Email);
return await GenerateJwtTokenAsync(model.Email, appUser);
}
throw new ApplicationException("INVALID_LOGIN_ATTEMPT");
}
// create token
private async Task<object> GenerateJwtTokenAsync(string email, ApplicationUser user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(role => new Claim(ClaimsIdentity.DefaultRoleClaimType, role)));
// get options
var jwtAppSettingOptions = _configuration.GetSection("JwtIssuerOptions");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtAppSettingOptions["JwtKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(jwtAppSettingOptions["JwtExpireDays"]));
var token = new JwtSecurityToken(
jwtAppSettingOptions["JwtIssuer"],
jwtAppSettingOptions["JwtIssuer"],
claims,
expires: expires,
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Fiddler test GetToken method. Request:
POST https://localhost:44355/Account/GetToken HTTP/1.1
content-type: application/json
Host: localhost:44355
Content-Length: 81
{
"Email":"admin#admin.site.com",
"Password":"ukj90ee",
"RememberMe":"false"
}
Debug response token https://jwt.io/#debugger-io
Payload data:
{
"sub": "admin#admin.site.com",
"jti": "520bc1de-5265-4114-aec2-b85d8c152c51",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "8df2c15f-7142-4011-9504-e73b4681fb6a",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
"exp": 1529823778,
"iss": "http://yourdomain.com",
"aud": "http://yourdomain.com"
}
Role Admin is worked!
For generating JWT Tokens we'll need AuthJwtTokenOptions helper class
public static class AuthJwtTokenOptions
{
public const string Issuer = "SomeIssuesName";
public const string Audience = "https://awesome-website.com/";
private const string Key = "supersecret_secretkey!12345";
public static SecurityKey GetSecurityKey() =>
new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Key));
}
Account controller code:
[HttpPost]
public async Task<IActionResult> GetToken([FromBody]Credentials credentials)
{
// TODO: Add here some input values validations
User user = await _userRepository.GetUser(credentials.Email, credentials.Password);
if (user == null)
return BadRequest();
ClaimsIdentity identity = GetClaimsIdentity(user);
return Ok(new AuthenticatedUserInfoJsonModel
{
UserId = user.Id,
Email = user.Email,
FullName = user.FullName,
Token = GetJwtToken(identity)
});
}
private ClaimsIdentity GetClaimsIdentity(User user)
{
// Here we can save some values to token.
// For example we are storing here user id and email
Claim[] claims = new[]
{
new Claim(ClaimTypes.Name, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email)
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token");
// Adding roles code
// Roles property is string collection but you can modify Select code if it it's not
claimsIdentity.AddClaims(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
return claimsIdentity;
}
private string GetJwtToken(ClaimsIdentity identity)
{
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(
issuer: AuthJwtTokenOptions.Issuer,
audience: AuthJwtTokenOptions.Audience,
notBefore: DateTime.UtcNow,
claims: identity.Claims,
// our token will live 1 hour, but you can change you token lifetime here
expires: DateTime.UtcNow.Add(TimeSpan.FromHours(1)),
signingCredentials: new SigningCredentials(AuthJwtTokenOptions.GetSecurityKey(), SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
In Startup.cs add following code to ConfigureServices(IServiceCollection services) method before services.AddMvc call:
public void ConfigureServices(IServiceCollection services)
{
// Other code here…
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthJwtTokenOptions.Issuer,
ValidateAudience = true,
ValidAudience = AuthJwtTokenOptions.Audience,
ValidateLifetime = true,
IssuerSigningKey = AuthJwtTokenOptions.GetSecurityKey(),
ValidateIssuerSigningKey = true
};
});
// Other code here…
services.AddMvc();
}
Also add app.UseAuthentication() call to ConfigureMethod of Startup.cs before app.UseMvc call.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Other code here…
app.UseAuthentication();
app.UseMvc();
}
Now you can use [Authorize(Roles = "Some_role")] attributes.
To get user id and email in any controller you should do it like this
int userId = int.Parse(HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
string email = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
Also userId can be retrived this way (this is due to claim type name ClaimTypes.Name)
int userId = int.Parse(HttpContext.User.Identity.Name);
It's better to move such code to some controller extension helpers:
public static class ControllerExtensions
{
public static int GetUserId(this Controller controller) =>
int.Parse(controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Name).Value);
public static string GetCurrentUserEmail(this Controller controller) =>
controller.HttpContext.User.Claims.First(c => c.Type == ClaimTypes.Email).Value;
}
The same is true for any other Claim you've added. You should just specify valid key.

Categories

Resources