get more claims

This commit is contained in:
Paul Schneider
2025-02-09 16:57:10 +00:00
parent 44dfb0021a
commit d1cadd9df8
7 changed files with 140 additions and 174 deletions

View File

@ -13,7 +13,8 @@ using System.Diagnostics;
namespace Yavsc.WebApi.Controllers namespace Yavsc.WebApi.Controllers
{ {
[Authorize("ApiScope"),Route("~/api/account")] [Route("~/api/account")]
[Authorize("ApiScope")]
public class ApiAccountController : Controller public class ApiAccountController : Controller
{ {
private UserManager<ApplicationUser> _userManager; private UserManager<ApplicationUser> _userManager;
@ -22,9 +23,12 @@ namespace Yavsc.WebApi.Controllers
private readonly ILogger _logger; private readonly ILogger _logger;
public ApiAccountController(UserManager<ApplicationUser> userManager, public ApiAccountController(UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager, ILoggerFactory loggerFactory, ApplicationDbContext dbContext) SignInManager<ApplicationUser> signInManager,
RoleManager<IdentityRole> roleManager,
ILoggerFactory loggerFactory, ApplicationDbContext dbContext)
{ {
UserManager = userManager; UserManager = userManager;
this.roleManager = roleManager;
_signInManager = signInManager; _signInManager = signInManager;
_logger = loggerFactory.CreateLogger("ApiAuth"); _logger = loggerFactory.CreateLogger("ApiAuth");
_dbContext = dbContext; _dbContext = dbContext;
@ -42,9 +46,11 @@ namespace Yavsc.WebApi.Controllers
} }
} }
private readonly RoleManager<IdentityRole> roleManager;
// POST api/Account/ChangePassword // POST api/Account/ChangePassword
public async Task<IActionResult> ChangePassword(ChangePasswordBindingModel model) public async Task<IActionResult> ChangePassword(ChangePasswordBindingModel model)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
@ -122,14 +128,14 @@ namespace Yavsc.WebApi.Controllers
base.Dispose(disposing); base.Dispose(disposing);
} }
[HttpGet("~/api/otherme")] [HttpGet("me")]
public async Task<IActionResult> Me () public async Task<IActionResult> Me()
{ {
if (User==null) if (User==null)
return new BadRequestObjectResult( return new BadRequestObjectResult(
new { error = "user not found" }); new { error = "user not found" });
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier); var uid = User.GetUserId();
var uuid = User.GetUserId();
var userData = await _dbContext.Users var userData = await _dbContext.Users
.Include(u=>u.PostalAddress) .Include(u=>u.PostalAddress)
.Include(u=>u.AccountBalance) .Include(u=>u.AccountBalance)
@ -139,9 +145,9 @@ namespace Yavsc.WebApi.Controllers
userData.Avatar , userData.Avatar ,
userData.PostalAddress, userData.DedicatedGoogleCalendar ); 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(); user.Roles = roles.Select(r=>r.Name).ToArray();

View File

@ -80,7 +80,8 @@ public static class Config
AllowedScopes = { AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email } IdentityServerConstants.StandardScopes.Email,
"scope2" }
}, },
}; };

View File

@ -178,34 +178,11 @@ internal static class HostingExtensions
services.AddDbContext<ApplicationDbContext>(options => services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
services
.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy
.RequireAuthenticatedUser()
.RequireClaim("scope", "api1");
});
});
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
var identityServerBuilder = services.AddIdentityServer()
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;
})
.AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryClients(Config.Clients) .AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryApiScopes(Config.ApiScopes)
@ -230,13 +207,7 @@ services
// TODO .AddServerSideSessionStore<YavscServerSideSessionStore>() // TODO .AddServerSideSessionStore<YavscServerSideSessionStore>()
var authenticationBuilder = services.AddAuthentication("Bearer") var authenticationBuilder = services.AddAuthentication();
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters =
new() { ValidateAudience = false };
});
authenticationBuilder.AddGoogle(options => authenticationBuilder.AddGoogle(options =>
{ {
@ -326,7 +297,7 @@ services
_ = services.AddTransient<IBillingService, BillingService>(); _ = services.AddTransient<IBillingService, BillingService>();
_ = services.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false)); _ = services.AddTransient<IDataStore, FileDataStore>((sp) => new FileDataStore("googledatastore", false));
_ = services.AddTransient<ICalendarManager, CalendarManager>(); _ = services.AddTransient<ICalendarManager, CalendarManager>();
services.AddTransient<IProfileService, ProfileService>(); //services.AddTransient<IProfileService, ProfileService>();
// TODO for SMS: services.AddTransient<ISmsSender, AuthMessageSender>(); // TODO for SMS: services.AddTransient<ISmsSender, AuthMessageSender>();
@ -343,8 +314,15 @@ services
{ {
options.AddPolicy("ApiScope", policy => 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 => options.AddPolicy("AdministratorOnly", policy =>
{ {
_ = policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName); _ = policy.RequireClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", Constants.AdminGroupName);

View File

@ -2,6 +2,7 @@ using System.Security.Claims;
using IdentityModel; using IdentityModel;
using IdentityServer8.Models; using IdentityServer8.Models;
using IdentityServer8.Services; using IdentityServer8.Services;
using IdentityServer8.Stores;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Yavsc.Models; using Yavsc.Models;
@ -10,38 +11,65 @@ namespace Yavsc.Services
public class ProfileService : IProfileService public class ProfileService : IProfileService
{ {
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public ProfileService( public ProfileService(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager)
RoleManager<IdentityRole> roleManager)
{ {
_userManager = userManager; _userManager = userManager;
_roleManager = roleManager;
} }
public async Task<List<Claim>> GetClaimsFromUserAsync(ApplicationUser user) public async Task<List<Claim>> 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<Claim> { var claims = new List<Claim> {
new Claim(JwtClaimTypes.Subject,user.Id.ToString()), new Claim(JwtClaimTypes.Subject,user.Id.ToString()),
new Claim(JwtClaimTypes.PreferredUserName,user.UserName)
}; };
var role = await _userManager.GetRolesAsync(user); foreach (var subClaim in context.Subject.Claims)
role.ToList().ForEach(f =>
{ {
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; return claims;
} }
private static void AddClaims(List<string> allowedScopes, List<Claim> 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) public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{ {
var subjectId = context.Subject.Claims.FirstOrDefault(c => c.Type == "sub").Value; var subjectId = context.Subject.Claims.FirstOrDefault(c => c.Type == "sub").Value;
var user = await _userManager.FindByIdAsync(subjectId); var user = await _userManager.FindByIdAsync(subjectId);
context.IssuedClaims = await GetClaimsFromUserAsync(user); context.IssuedClaims = await GetClaimsFromUserAsync(context, user);
} }

View File

@ -4,8 +4,10 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -36,38 +38,30 @@ namespace testOauthClient.Controllers
return View(); return View();
} }
public async Task<IActionResult> CallApi() public async Task<IActionResult> CallApi()
{ {
var accessToken = await HttpContext.GetTokenAsync("access_token"); var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient(); var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("https://localhost:5001/api/me"); var content = await client.GetStringAsync("https://localhost:5001/api/account/me");
ViewBag.Json = content; ViewBag.Json = content;
return View("json"); return View("json");
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> GetUserInfo(CancellationToken cancellationToken) public async Task<IActionResult> GetUserInfo(CancellationToken cancellationToken)
{ {
var accessToken = await HttpContext.GetTokenAsync("access_token"); var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
using (var client = new HttpClient()) client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
{ client.DefaultRequestHeaders.Add("Accept", "application/json");
var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:5001/api/me"); var content = await client.GetStringAsync("https://localhost:5001/api/account/me");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var obj = JsonSerializer.Deserialize<JsonElement>(content);
return View("UserInfo", obj.ToString());
var response = await client.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
return View("UserInfo", model: await response.Content.ReadAsStringAsync());
}
} }
public IActionResult About() public IActionResult About()
{ {
ViewData["Message"] = "Your application description page."; ViewData["Message"] = "Your application description page.";

View File

@ -6,67 +6,68 @@ using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
public class Startup public class Startup
{
public void ConfigureServices(IServiceCollection services)
{ {
public void ConfigureServices(IServiceCollection services) services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{ {
services.AddControllersWithViews(); options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "Yavsc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("Yavsc", options =>
{
options.Authority = "https://localhost:5001";
JwtSecurityTokenHandler.DefaultMapInboundClaims = false; 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.GetClaimsFromUserInfoEndpoint = true;
{
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");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = 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("role", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
options.ClaimActions.MapUniqueJsonKey("roles", "role"); options.ClaimActions.MapUniqueJsonKey("roles", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
NameClaimType = "name", NameClaimType = "name",
RoleClaimType = "role" RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
}; };
}); });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute()
.RequireAuthorization();
});
}
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute()
.RequireAuthorization();
});
}
}

View File

@ -4,26 +4,6 @@
} }
@using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Authentication
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>
<div class="jumbotron"> <div class="jumbotron">
@if (User?.Identity?.IsAuthenticated ?? false) { @if (User?.Identity?.IsAuthenticated ?? false) {
<h1>Welcome, @User.Identity.Name</h1> <h1>Welcome, @User.Identity.Name</h1>
@ -57,25 +37,3 @@
<a class="btn btn-lg btn-success" href="/signin">Sign in</a> <a class="btn btn-lg btn-success" href="/signin">Sign in</a>
} }
</div> </div>
<h2>Claims</h2>
<dl>
@foreach (var claim in User.Claims)
{
<dt>@claim.Type</dt>
<dd>@claim.Value</dd>
}
</dl>
<h2>Properties</h2>
<dl>
@foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
{
<dt>@prop.Key</dt>
<dd>@prop.Value</dd>
}
</dl>