From d1cadd9df80ebdf62fdc018b487709d1bbb6ba19 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 9 Feb 2025 16:57:10 +0000 Subject: [PATCH] get more claims --- .../accounting/AccountController.cs | 26 +++-- src/Yavsc/Config.cs | 3 +- src/Yavsc/Extensions/HostingExtensions.cs | 44 +++------ src/Yavsc/Services/ProfileService.cs | 50 +++++++--- .../Controllers/HomeController.cs | 42 ++++---- src/sampleWebAsWebApiClient/Startup.cs | 97 ++++++++++--------- .../Views/Home/Index.cshtml | 42 -------- 7 files changed, 135 insertions(+), 169 deletions(-) diff --git a/src/Yavsc/ApiControllers/accounting/AccountController.cs b/src/Yavsc/ApiControllers/accounting/AccountController.cs index 35f83402..c174c179 100644 --- a/src/Yavsc/ApiControllers/accounting/AccountController.cs +++ b/src/Yavsc/ApiControllers/accounting/AccountController.cs @@ -13,7 +13,8 @@ using System.Diagnostics; namespace Yavsc.WebApi.Controllers { - [Authorize("ApiScope"),Route("~/api/account")] + [Route("~/api/account")] + [Authorize("ApiScope")] public class ApiAccountController : Controller { private UserManager _userManager; @@ -22,9 +23,12 @@ namespace Yavsc.WebApi.Controllers private readonly ILogger _logger; public ApiAccountController(UserManager userManager, - SignInManager signInManager, ILoggerFactory loggerFactory, ApplicationDbContext dbContext) + SignInManager signInManager, + RoleManager roleManager, + ILoggerFactory loggerFactory, ApplicationDbContext dbContext) { UserManager = userManager; + this.roleManager = roleManager; _signInManager = signInManager; _logger = loggerFactory.CreateLogger("ApiAuth"); _dbContext = dbContext; @@ -42,9 +46,11 @@ namespace Yavsc.WebApi.Controllers } } + private readonly RoleManager roleManager; + // POST api/Account/ChangePassword - - public async Task ChangePassword(ChangePasswordBindingModel model) + + public async Task ChangePassword(ChangePasswordBindingModel model) { if (!ModelState.IsValid) { @@ -122,14 +128,14 @@ namespace Yavsc.WebApi.Controllers base.Dispose(disposing); } - [HttpGet("~/api/otherme")] - public async Task Me () + [HttpGet("me")] + public async Task Me() { if (User==null) return new BadRequestObjectResult( new { error = "user not found" }); - var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); - var uuid = User.GetUserId(); + var uid = User.GetUserId(); + var userData = await _dbContext.Users .Include(u=>u.PostalAddress) .Include(u=>u.AccountBalance) @@ -139,9 +145,9 @@ namespace Yavsc.WebApi.Controllers userData.Avatar , userData.PostalAddress, userData.DedicatedGoogleCalendar ); - var userRoles = _dbContext.UserRoles.Where(u=>u.UserId == uid).ToArray(); + var userRoles = _dbContext.UserRoles.Where(u=>u.UserId == uid).Select(r => r.RoleId).ToArray(); - IdentityRole [] roles = _dbContext.Roles.Where(r=>userRoles.Any(ur=>ur.RoleId==r.Id)).ToArray(); + IdentityRole [] roles = _dbContext.Roles.Where(r=>userRoles.Contains(r.Id)).ToArray(); user.Roles = roles.Select(r=>r.Name).ToArray(); diff --git a/src/Yavsc/Config.cs b/src/Yavsc/Config.cs index d1fb8d26..05be8631 100644 --- a/src/Yavsc/Config.cs +++ b/src/Yavsc/Config.cs @@ -80,7 +80,8 @@ public static class Config AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, - IdentityServerConstants.StandardScopes.Email } + IdentityServerConstants.StandardScopes.Email, + "scope2" } }, }; diff --git a/src/Yavsc/Extensions/HostingExtensions.cs b/src/Yavsc/Extensions/HostingExtensions.cs index b66dba5f..9520711e 100644 --- a/src/Yavsc/Extensions/HostingExtensions.cs +++ b/src/Yavsc/Extensions/HostingExtensions.cs @@ -178,34 +178,11 @@ internal static class HostingExtensions services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); -services - .AddAuthorization(options => - { - options.AddPolicy("ApiScope", policy => - { - policy - .RequireAuthenticatedUser() - .RequireClaim("scope", "api1"); - }); - }); - services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - - var identityServerBuilder = services.AddIdentityServer(options => - { - options.Events.RaiseErrorEvents = true; - options.Events.RaiseInformationEvents = true; - options.Events.RaiseFailureEvents = true; - options.Events.RaiseSuccessEvents = true; - - // see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/ - options.EmitStaticAudienceClaim = true; - options.EmitScopesAsSpaceDelimitedStringInJwt = true; - options.Endpoints.EnableUserInfoEndpoint = true; - }) + var identityServerBuilder = services.AddIdentityServer() .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryClients(Config.Clients) .AddInMemoryApiScopes(Config.ApiScopes) @@ -230,13 +207,7 @@ services // TODO .AddServerSideSessionStore() - var authenticationBuilder = services.AddAuthentication("Bearer") - .AddJwtBearer("Bearer", options => - { - options.Authority = "https://localhost:5001"; - options.TokenValidationParameters = - new() { ValidateAudience = false }; - }); + var authenticationBuilder = services.AddAuthentication(); authenticationBuilder.AddGoogle(options => { @@ -326,7 +297,7 @@ services _ = services.AddTransient(); _ = services.AddTransient((sp) => new FileDataStore("googledatastore", false)); _ = services.AddTransient(); - services.AddTransient(); + //services.AddTransient(); // TODO for SMS: services.AddTransient(); @@ -343,8 +314,15 @@ services { options.AddPolicy("ApiScope", policy => { - policy.RequireAuthenticatedUser(); + policy.RequireAuthenticatedUser() + .RequireClaim("scope", "scope2"); }); + options.AddPolicy("Performer", policy => + { + policy + .RequireAuthenticatedUser() + .RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "Performer"); + }); options.AddPolicy("AdministratorOnly", policy => { _ = policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName); diff --git a/src/Yavsc/Services/ProfileService.cs b/src/Yavsc/Services/ProfileService.cs index bb6f2d2b..d58e982d 100644 --- a/src/Yavsc/Services/ProfileService.cs +++ b/src/Yavsc/Services/ProfileService.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using IdentityModel; using IdentityServer8.Models; using IdentityServer8.Services; +using IdentityServer8.Stores; using Microsoft.AspNetCore.Identity; using Yavsc.Models; @@ -10,38 +11,65 @@ namespace Yavsc.Services public class ProfileService : IProfileService { private readonly UserManager _userManager; - private readonly RoleManager _roleManager; public ProfileService( - UserManager userManager, - RoleManager roleManager) + UserManager userManager) { _userManager = userManager; - _roleManager = roleManager; } - public async Task> GetClaimsFromUserAsync(ApplicationUser user) + public async Task> GetClaimsFromUserAsync( + ProfileDataRequestContext context, + ApplicationUser user) { + + var allowedScopes = context.Client.AllowedScopes + .Where(s => s != JwtClaimTypes.Subject) + .ToList(); + if (allowedScopes.Contains("profile")) + { + allowedScopes.Remove("profile"); + allowedScopes.Add(JwtClaimTypes.Name); + allowedScopes.Add(JwtClaimTypes.FamilyName); + allowedScopes.Add(JwtClaimTypes.Email); + allowedScopes.Add(JwtClaimTypes.PreferredUserName); + allowedScopes.Add("http://schemas.microsoft.com/ws/2008/06/identity/claims/role"); + } + var claims = new List { new Claim(JwtClaimTypes.Subject,user.Id.ToString()), - new Claim(JwtClaimTypes.PreferredUserName,user.UserName) }; - var role = await _userManager.GetRolesAsync(user); - role.ToList().ForEach(f => + foreach (var subClaim in context.Subject.Claims) { - claims.Add(new Claim(JwtClaimTypes.Role, f)); - }); + if (allowedScopes.Contains(subClaim.Type)) + claims.Add(subClaim); + } + AddClaims(allowedScopes, claims, JwtClaimTypes.Email, user.Email); + AddClaims(allowedScopes, claims, JwtClaimTypes.PreferredUserName, user.FullName); + + foreach (var scope in context.Client.AllowedScopes) + { + claims.Add(new Claim("scope", scope)); + } return claims; } + private static void AddClaims(List allowedScopes, List claims, + string claimType, string claimValue + ) + { + if (allowedScopes.Contains(claimType)) + if (!claims.Any(c => c.Type == claimType)) + claims.Add(new Claim(claimType, claimValue)); + } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var subjectId = context.Subject.Claims.FirstOrDefault(c => c.Type == "sub").Value; var user = await _userManager.FindByIdAsync(subjectId); - context.IssuedClaims = await GetClaimsFromUserAsync(user); + context.IssuedClaims = await GetClaimsFromUserAsync(context, user); } diff --git a/src/sampleWebAsWebApiClient/Controllers/HomeController.cs b/src/sampleWebAsWebApiClient/Controllers/HomeController.cs index 8c5bfa15..de6b72f3 100755 --- a/src/sampleWebAsWebApiClient/Controllers/HomeController.cs +++ b/src/sampleWebAsWebApiClient/Controllers/HomeController.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.IO; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -36,38 +38,30 @@ namespace testOauthClient.Controllers return View(); } - public async Task CallApi() - { - var accessToken = await HttpContext.GetTokenAsync("access_token"); + public async Task CallApi() + { + var accessToken = await HttpContext.GetTokenAsync("access_token"); - var client = new HttpClient(); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - var content = await client.GetStringAsync("https://localhost:5001/api/me"); + var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + var content = await client.GetStringAsync("https://localhost:5001/api/account/me"); - ViewBag.Json = content; - return View("json"); - } + ViewBag.Json = content; + return View("json"); + } [HttpPost] public async Task GetUserInfo(CancellationToken cancellationToken) { - var accessToken = await HttpContext.GetTokenAsync("access_token"); - - using (var client = new HttpClient()) - { - var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:5001/api/me"); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - - var response = await client.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - return View("UserInfo", model: await response.Content.ReadAsStringAsync()); - } - + var accessToken = await HttpContext.GetTokenAsync("access_token"); + var client = new HttpClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + var content = await client.GetStringAsync("https://localhost:5001/api/account/me"); + var obj = JsonSerializer.Deserialize(content); + return View("UserInfo", obj.ToString()); } - public IActionResult About() { ViewData["Message"] = "Your application description page."; diff --git a/src/sampleWebAsWebApiClient/Startup.cs b/src/sampleWebAsWebApiClient/Startup.cs index ccd54c83..176c7ea6 100644 --- a/src/sampleWebAsWebApiClient/Startup.cs +++ b/src/sampleWebAsWebApiClient/Startup.cs @@ -6,67 +6,68 @@ using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; - public class Startup +public class Startup +{ + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) - { - services.AddControllersWithViews(); + services.AddControllersWithViews(); - JwtSecurityTokenHandler.DefaultMapInboundClaims = false; + JwtSecurityTokenHandler.DefaultMapInboundClaims = false; - services.AddAuthentication(options => - { - options.DefaultScheme = "Cookies"; - options.DefaultChallengeScheme = "Yavsc"; - }) - .AddCookie("Cookies") - .AddOpenIdConnect("Yavsc", options => - { - options.Authority = "https://localhost:5001"; - - options.ClientId = "mvc"; - options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0"; - options.ResponseType = "code"; - options.UsePkce = true; - options.Scope.Clear(); - options.Scope.Add("openid"); - options.Scope.Add("profile"); - options.Scope.Add("email"); + services.AddAuthentication(options => + { + options.DefaultScheme = "Cookies"; + options.DefaultChallengeScheme = "Yavsc"; + }) + .AddCookie("Cookies") + .AddOpenIdConnect("Yavsc", options => + { + options.Authority = "https://localhost:5001"; - options.GetClaimsFromUserInfoEndpoint = true; + options.ClientId = "mvc"; + options.ClientSecret = "49C1A7E1-0C79-4A89-A3D6-A37998FB86B0"; + options.ResponseType = "code"; + options.UsePkce = true; + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + + options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; + options.ClaimActions.MapUniqueJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"); - options.ClaimActions.MapUniqueJsonKey("role", "role"); - options.ClaimActions.MapUniqueJsonKey("roles", "role"); + options.ClaimActions.MapUniqueJsonKey("role", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"); + options.ClaimActions.MapUniqueJsonKey("roles", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"); options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", - RoleClaimType = "role" + RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" }; - }); - } + }); + } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - } + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } - app.UseStaticFiles(); + app.UseStaticFiles(); - app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); - app.UseEndpoints(endpoints => - { - endpoints.MapDefaultControllerRoute() - .RequireAuthorization(); - }); - } + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute() + .RequireAuthorization(); + }); } +} diff --git a/src/sampleWebAsWebApiClient/Views/Home/Index.cshtml b/src/sampleWebAsWebApiClient/Views/Home/Index.cshtml index e5a883e7..ba2d5ffc 100755 --- a/src/sampleWebAsWebApiClient/Views/Home/Index.cshtml +++ b/src/sampleWebAsWebApiClient/Views/Home/Index.cshtml @@ -4,26 +4,6 @@ } @using Microsoft.AspNetCore.Authentication -

Claims

- -
- @foreach (var claim in User.Claims) - { -
@claim.Type
-
@claim.Value
- } -
- -

Properties

- -
- @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) - { -
@prop.Key
-
@prop.Value
- } -
-
@if (User?.Identity?.IsAuthenticated ?? false) {

Welcome, @User.Identity.Name

@@ -57,25 +37,3 @@ Sign in }
- - - -

Claims

- -
- @foreach (var claim in User.Claims) - { -
@claim.Type
-
@claim.Value
- } -
- -

Properties

- -
- @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items) - { -
@prop.Key
-
@prop.Value
- } -